import { test, expect } from "@playwright/test";
const MUSIC = {
TableName: "Music",
KeySchema: [
{ AttributeName: "artist", KeyType: "HASH" },
{ AttributeName: "song", KeyType: "RANGE" },
],
AttributeDefinitions: [
{ AttributeName: "artist", AttributeType: "S" },
{ AttributeName: "song", AttributeType: "S" },
],
BillingMode: "PAY_PER_REQUEST",
};
test.beforeEach(async ({ page }) => {
await page.goto("/harness/engine-harness.html");
await page.waitForFunction(() => globalThis.__HARNESS_READY__ === true);
});
test("CRUD round-trip: persists to OPFS, and a filtered scan reads more than it counts", async ({ page }) => {
const result = await page.evaluate(async (table) => {
const client = globalThis.dynoxide.makeClient({ name: `crud-${crypto.randomUUID()}` });
await client.ready();
await client.execute("CreateTable", table);
for (const [song, genre] of [["s1", "rock"], ["s2", "jazz"], ["s3", "rock"]]) {
await client.execute("PutItem", {
TableName: table.TableName,
Item: { artist: { S: "a" }, song: { S: song }, genre: { S: genre } },
});
}
const query = await client.execute("Query", {
TableName: table.TableName,
KeyConditionExpression: "artist = :a",
ExpressionAttributeValues: { ":a": { S: "a" } },
});
const scan = await client.execute("Scan", {
TableName: table.TableName,
FilterExpression: "genre = :g",
ExpressionAttributeValues: { ":g": { S: "rock" } },
});
const out = {
persistenceMode: client.persistenceMode,
queryCount: query.Count,
scanCount: scan.Count,
scannedCount: scan.ScannedCount,
};
client.terminate();
return out;
}, MUSIC);
expect(result.persistenceMode).toBe("opfs");
expect(result.queryCount).toBe(3);
expect(result.scanCount).toBe(2);
expect(result.scannedCount).toBe(3);
});
test("a body-less op (ListTables) round-trips instead of a SerializationException (#65)", async ({ page }) => {
const result = await page.evaluate(async (table) => {
const client = globalThis.dynoxide.makeClient({ name: `list-${crypto.randomUUID()}` });
await client.ready();
await client.execute("CreateTable", table);
const listed = await client.execute("ListTables");
client.terminate();
return listed;
}, MUSIC);
expect(result.TableNames).toContain(MUSIC.TableName);
});
test("data survives a reload: a fresh client on the same name sees the writes (#64)", async ({ page }) => {
const name = `persist-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
const firstMode = await page.evaluate(async ({ name, table }) => {
const client = globalThis.dynoxide.makeClient({ name });
await client.ready();
await client.execute("CreateTable", table);
await client.execute("PutItem", {
TableName: table.TableName,
Item: { artist: { S: "a" }, song: { S: "s1" } },
});
const mode = client.persistenceMode;
client.terminate(); return mode;
}, { name, table: MUSIC });
expect(firstMode).toBe("opfs");
await page.waitForTimeout(150);
const reopened = await page.evaluate(async ({ name, table }) => {
const client = globalThis.dynoxide.makeClient({ name });
await client.ready();
const scan = await client.execute("Scan", { TableName: table.TableName });
const out = { mode: client.persistenceMode, count: scan.Count };
client.terminate();
return out;
}, { name, table: MUSIC });
expect(reopened.mode).toBe("opfs");
expect(reopened.count).toBe(1);
});
test("a second client on a busy OPFS database fails clearly instead of silently forking to memory (#64)", async ({ page }) => {
const result = await page.evaluate(async () => {
const name = `busy-${crypto.randomUUID()}`;
const a = globalThis.dynoxide.makeClient({ name });
await a.ready();
const b = globalThis.dynoxide.makeClient({ name });
let bError = null;
let bMode = null;
try {
await b.ready();
bMode = b.persistenceMode; } catch (e) {
bError = { type: e.type, message: e.message };
}
const aMode = a.persistenceMode;
a.terminate();
b.terminate();
return { aMode, bError, bMode };
});
expect(result.aMode).toBe("opfs");
expect(result.bMode).not.toBe("memory");
expect(result.bError).not.toBeNull();
expect(result.bError.message).toMatch(/busy|OPFS/i);
expect(result.bError.type).toBe("com.dynoxide.wasm#OpfsUnavailable");
});
const TABLE_T = {
TableName: "Reopens",
KeySchema: [{ AttributeName: "pk", KeyType: "HASH" }],
AttributeDefinitions: [{ AttributeName: "pk", AttributeType: "S" }],
BillingMode: "PAY_PER_REQUEST",
};
test("a failed re-open leaves the previous database open and usable (#64)", async ({ page }) => {
const result = await page.evaluate(async (table) => {
const nameA = `reopenA-${crypto.randomUUID()}`;
const nameB = `reopenB-${crypto.randomUUID()}`;
const w1 = globalThis.dynoxide.makeRawWorker();
await w1.open(nameA);
await w1.execute("CreateTable", table);
await w1.execute("PutItem", { TableName: "Reopens", Item: { pk: { S: "a1" } } });
const w2 = globalThis.dynoxide.makeRawWorker();
await w2.open(nameB);
let reopenErr = null;
try {
await w1.open(nameB);
} catch (e) {
try {
reopenErr = JSON.parse(e.message);
} catch {
reopenErr = { message: e.message };
}
}
const scan = await w1.execute("Scan", { TableName: "Reopens" });
w1.terminate();
w2.terminate();
return { reopenErr, count: scan.Count };
}, TABLE_T);
expect(result.reopenErr).not.toBeNull();
expect(result.reopenErr.__type).toBe("com.dynoxide.wasm#OpfsUnavailable");
expect(result.count).toBe(1); });
test("re-open keeps same-name data and frees the old database when switching names (#64)", async ({ page }) => {
const nameA = `switchA-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
const nameB = `switchB-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
const out = await page.evaluate(async ({ nameA, nameB, table }) => {
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const w1 = globalThis.dynoxide.makeRawWorker();
const d1 = await w1.open(nameA);
await w1.execute("CreateTable", table);
await w1.execute("PutItem", { TableName: "Reopens", Item: { pk: { S: "a1" } } });
await w1.open(nameA);
const sameNameScan = await w1.execute("Scan", { TableName: "Reopens" });
await w1.open(nameB);
await w1.execute("CreateTable", table);
await w1.execute("PutItem", { TableName: "Reopens", Item: { pk: { S: "b1" } } });
const bScan = await w1.execute("Scan", { TableName: "Reopens" });
let aReopen = null;
for (let attempt = 0; attempt < 10 && !aReopen; attempt += 1) {
const w = globalThis.dynoxide.makeRawWorker();
try {
const d = await w.open(nameA);
const scan = await w.execute("Scan", { TableName: "Reopens" });
aReopen = { mode: d.persistenceMode, count: scan.Count };
w.terminate();
} catch {
w.terminate();
await sleep(50);
}
}
w1.terminate();
return { mode: d1.persistenceMode, sameNameCount: sameNameScan.Count, bCount: bScan.Count, aReopen };
}, { nameA, nameB, table: TABLE_T });
expect(out.mode).toBe("opfs");
expect(out.sameNameCount).toBe(1); expect(out.bCount).toBe(1); expect(out.aReopen).not.toBeNull(); expect(out.aReopen.mode).toBe("opfs");
expect(out.aReopen.count).toBe(1); });
test("the shipping worker rejects a stripped harness op as unknown (#69)", async ({ page }) => {
const err = await page.evaluate(async () => {
const w = globalThis.dynoxide.makeRawWorker();
let parsed = null;
try {
await w.call("smoke", {});
} catch (e) {
try {
parsed = JSON.parse(e.message);
} catch {
parsed = { message: e.message };
}
}
w.terminate();
return parsed;
});
expect(err).not.toBeNull();
expect(err.__type).toBe("com.dynoxide.wasm#UnsupportedOperation");
expect(err.message).toMatch(/unknown op/);
});
test("OPFS persistence works with no cross-origin isolation (no COOP/COEP)", async ({ page }) => {
const result = await page.evaluate(async (table) => {
const isolated = globalThis.crossOriginIsolated;
const client = globalThis.dynoxide.makeClient({ name: `noiso-${crypto.randomUUID()}` });
await client.ready();
await client.execute("CreateTable", table);
await client.execute("PutItem", {
TableName: table.TableName,
Item: { artist: { S: "a" }, song: { S: "s1" } },
});
const scan = await client.execute("Scan", { TableName: table.TableName });
const out = { isolated, mode: client.persistenceMode, count: scan.Count };
client.terminate();
return out;
}, MUSIC);
expect(result.isolated).toBe(false);
expect(result.mode).toBe("opfs");
expect(result.count).toBe(1);
});
test("persistence mode reports opfs for a persistent open and memory for an ephemeral one", async ({ page }) => {
const result = await page.evaluate(async () => {
const w1 = globalThis.dynoxide.makeRawWorker();
const persistent = await w1.open(`mode-opfs-${crypto.randomUUID()}`); w1.terminate();
const w2 = globalThis.dynoxide.makeRawWorker();
const ephemeral = await w2.open(`mode-mem-${crypto.randomUUID()}`, true);
w2.terminate();
return { persistent: persistent.persistenceMode, ephemeral: ephemeral.persistenceMode };
});
expect(result.persistent).toBe("opfs");
expect(result.ephemeral).toBe("memory");
});
test("a busy OPFS database recovers once the holder releases, not sticky until reload", async ({ page }) => {
const result = await page.evaluate(async () => {
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const name = `recover-${crypto.randomUUID()}`;
const a = globalThis.dynoxide.makeRawWorker();
await a.open(name);
const b = globalThis.dynoxide.makeRawWorker();
let firstErr = null;
try {
await b.open(name);
} catch (e) {
try {
firstErr = JSON.parse(e.message);
} catch {
firstErr = { message: e.message };
}
}
a.terminate();
let recovered = null;
for (let attempt = 0; attempt < 20 && !recovered; attempt += 1) {
try {
const d = await b.open(name);
recovered = d.persistenceMode;
} catch {
await sleep(50);
}
}
b.terminate();
return { firstErr, recovered };
});
expect(result.firstErr).not.toBeNull();
expect(result.firstErr.__type).toBe("com.dynoxide.wasm#OpfsUnavailable");
expect(result.recovered).toBe("opfs");
});
const BIG_N = "9007199254740993";
test("a Number attribute beyond 2^53 round-trips bit-identical through put and read (sign-off gate)", async ({ page }) => {
const result = await page.evaluate(async ({ table, big }) => {
const client = globalThis.dynoxide.makeClient({ name: `bign-${crypto.randomUUID()}` });
await client.ready();
await client.execute("CreateTable", table);
await client.execute("PutItem", {
TableName: table.TableName,
Item: { artist: { S: "a" }, song: { S: "s1" }, plays: { N: big } },
});
const read = await client.execute("Query", {
TableName: table.TableName,
KeyConditionExpression: "artist = :a",
ExpressionAttributeValues: { ":a": { S: "a" } },
});
const out = { plays: read.Items[0].plays.N };
client.terminate();
return out;
}, { table: MUSIC, big: BIG_N });
expect(result.plays).toBe(BIG_N);
});
const SEGMENTED = {
TableName: "Segmented",
KeySchema: [
{ AttributeName: "pk", KeyType: "HASH" },
{ AttributeName: "sk", KeyType: "RANGE" },
],
AttributeDefinitions: [
{ AttributeName: "pk", AttributeType: "S" },
{ AttributeName: "sk", AttributeType: "S" },
{ AttributeName: "gpk", AttributeType: "S" },
{ AttributeName: "gsk", AttributeType: "S" },
],
GlobalSecondaryIndexes: [
{
IndexName: "byG",
KeySchema: [
{ AttributeName: "gpk", KeyType: "HASH" },
{ AttributeName: "gsk", KeyType: "RANGE" },
],
Projection: { ProjectionType: "ALL" },
},
],
BillingMode: "PAY_PER_REQUEST",
};
test("a segmented parallel scan over a GSI matches a full scan, proving the fnv1a scalar", async ({ page }) => {
const result = await page.evaluate(async (table) => {
const client = globalThis.dynoxide.makeClient({ name: `seg-${crypto.randomUUID()}` });
await client.ready();
await client.execute("CreateTable", table);
for (let i = 0; i < 24; i += 1) {
await client.execute("PutItem", {
TableName: table.TableName,
Item: { pk: { S: `p${i}` }, sk: { S: "s" }, gpk: { S: `g${i % 7}` }, gsk: { S: `k${i}` } },
});
}
const keyOf = (it) => `${it.pk.S}|${it.sk.S}`;
const full = await client.execute("Scan", { TableName: table.TableName, IndexName: "byG" });
const fullKeys = full.Items.map(keyOf).sort();
const SEG = 4;
const segKeys = [];
for (let s = 0; s < SEG; s += 1) {
const part = await client.execute("Scan", {
TableName: table.TableName,
IndexName: "byG",
Segment: s,
TotalSegments: SEG,
});
segKeys.push(...part.Items.map(keyOf));
}
const out = {
fullCount: fullKeys.length,
dupes: segKeys.length !== new Set(segKeys).size,
unionMatches: JSON.stringify(segKeys.slice().sort()) === JSON.stringify(fullKeys),
};
client.terminate();
return out;
}, SEGMENTED);
expect(result.fullCount).toBe(24);
expect(result.dupes).toBe(false); expect(result.unionMatches).toBe(true); });
test("a heavy multi-table workload stays within the SAH pool without exhausting capacity", async ({ page }) => {
const result = await page.evaluate(async () => {
const client = globalThis.dynoxide.makeClient({ name: `cap-${crypto.randomUUID()}` });
await client.ready();
let created = 0;
let total = 0;
for (let t = 0; t < 6; t += 1) {
const TableName = `Cap${t}`;
await client.execute("CreateTable", {
TableName,
KeySchema: [
{ AttributeName: "pk", KeyType: "HASH" },
{ AttributeName: "sk", KeyType: "RANGE" },
],
AttributeDefinitions: [
{ AttributeName: "pk", AttributeType: "S" },
{ AttributeName: "sk", AttributeType: "S" },
],
BillingMode: "PAY_PER_REQUEST",
});
created += 1;
for (let i = 0; i < 20; i += 1) {
await client.execute("PutItem", {
TableName,
Item: { pk: { S: `p${i % 5}` }, sk: { S: `s${i}` } },
});
}
total += (await client.execute("Scan", { TableName })).Count;
}
client.terminate();
return { created, total };
});
expect(result.created).toBe(6);
expect(result.total).toBe(120); });
test("UpdateTable adds a GSI to a populated table and backfills the existing rows (OPFS)", async ({ page }) => {
const result = await page.evaluate(async (table) => {
const client = globalThis.dynoxide.makeClient({ name: `addgsi-${crypto.randomUUID()}` });
await client.ready();
await client.execute("CreateTable", table);
for (const [song, genre] of [["s1", "rock"], ["s2", "jazz"], ["s3", "rock"]]) {
await client.execute("PutItem", {
TableName: table.TableName,
Item: { artist: { S: "a" }, song: { S: song }, genre: { S: genre } },
});
}
await client.execute("UpdateTable", {
TableName: table.TableName,
AttributeDefinitions: [
{ AttributeName: "artist", AttributeType: "S" },
{ AttributeName: "song", AttributeType: "S" },
{ AttributeName: "genre", AttributeType: "S" },
],
GlobalSecondaryIndexUpdates: [
{
Create: {
IndexName: "GenreIndex",
KeySchema: [{ AttributeName: "genre", KeyType: "HASH" }],
Projection: { ProjectionType: "ALL" },
},
},
],
});
const q = await client.execute("Query", {
TableName: table.TableName,
IndexName: "GenreIndex",
KeyConditionExpression: "genre = :g",
ExpressionAttributeValues: { ":g": { S: "rock" } },
});
const desc = await client.execute("DescribeTable", { TableName: table.TableName });
const gsi = (desc.Table.GlobalSecondaryIndexes || []).find((g) => g.IndexName === "GenreIndex");
const out = {
mode: client.persistenceMode,
count: q.Count,
songs: q.Items.map((i) => i.song.S).sort(),
indexStatus: gsi && gsi.IndexStatus,
};
client.terminate();
return out;
}, MUSIC);
expect(result.mode).toBe("opfs");
expect(result.count).toBe(2); expect(result.songs).toEqual(["s1", "s3"]);
expect(result.indexStatus).toBe("ACTIVE");
});
test("UpdateTable deletes a GSI; the index stops answering and the base table survives", async ({ page }) => {
const result = await page.evaluate(async () => {
const client = globalThis.dynoxide.makeClient({ name: `delgsi-${crypto.randomUUID()}` });
await client.ready();
await client.execute("CreateTable", {
TableName: "Music",
KeySchema: [
{ AttributeName: "artist", KeyType: "HASH" },
{ AttributeName: "song", KeyType: "RANGE" },
],
AttributeDefinitions: [
{ AttributeName: "artist", AttributeType: "S" },
{ AttributeName: "song", AttributeType: "S" },
{ AttributeName: "genre", AttributeType: "S" },
],
GlobalSecondaryIndexes: [
{
IndexName: "GenreIndex",
KeySchema: [{ AttributeName: "genre", KeyType: "HASH" }],
Projection: { ProjectionType: "ALL" },
},
],
BillingMode: "PAY_PER_REQUEST",
});
await client.execute("PutItem", {
TableName: "Music",
Item: { artist: { S: "a" }, song: { S: "s1" }, genre: { S: "rock" } },
});
const queryGenre = () =>
client.execute("Query", {
TableName: "Music",
IndexName: "GenreIndex",
KeyConditionExpression: "genre = :g",
ExpressionAttributeValues: { ":g": { S: "rock" } },
});
const before = await queryGenre();
await client.execute("UpdateTable", {
TableName: "Music",
GlobalSecondaryIndexUpdates: [{ Delete: { IndexName: "GenreIndex" } }],
});
const baseScan = await client.execute("Scan", { TableName: "Music" });
let indexErr = null;
try {
await queryGenre();
} catch (e) {
indexErr = e.message;
}
const out = { beforeCount: before.Count, baseCount: baseScan.Count, indexErr };
client.terminate();
return out;
});
expect(result.beforeCount).toBe(1);
expect(result.baseCount).toBe(1); expect(result.indexErr).not.toBeNull(); });
test("UpdateTable persists deletion protection and table class, reflected by DescribeTable", async ({ page }) => {
const result = await page.evaluate(async (table) => {
const client = globalThis.dynoxide.makeClient({ name: `setters-${crypto.randomUUID()}` });
await client.ready();
await client.execute("CreateTable", table);
await client.execute("UpdateTable", { TableName: table.TableName, DeletionProtectionEnabled: true });
await client.execute("UpdateTable", {
TableName: table.TableName,
TableClass: "STANDARD_INFREQUENT_ACCESS",
});
const d = await client.execute("DescribeTable", { TableName: table.TableName });
const out = {
deletionProtection: d.Table.DeletionProtectionEnabled,
tableClass: d.Table.TableClassSummary && d.Table.TableClassSummary.TableClass,
};
client.terminate();
return out;
}, MUSIC);
expect(result.deletionProtection).toBe(true);
expect(result.tableClass).toBe("STANDARD_INFREQUENT_ACCESS");
});
test("UpdateTable switches billing mode to provisioned with its throughput, reflected by DescribeTable", async ({ page }) => {
const result = await page.evaluate(async (table) => {
const client = globalThis.dynoxide.makeClient({ name: `billing-${crypto.randomUUID()}` });
await client.ready();
await client.execute("CreateTable", table); await client.execute("UpdateTable", {
TableName: table.TableName,
BillingMode: "PROVISIONED",
ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 },
});
const d = await client.execute("DescribeTable", { TableName: table.TableName });
const out = {
hasBillingModeSummary: !!d.Table.BillingModeSummary,
rcu: d.Table.ProvisionedThroughput && d.Table.ProvisionedThroughput.ReadCapacityUnits,
wcu: d.Table.ProvisionedThroughput && d.Table.ProvisionedThroughput.WriteCapacityUnits,
};
client.terminate();
return out;
}, MUSIC);
expect(result.hasBillingModeSummary).toBe(false); expect(result.rcu).toBe(5); expect(result.wcu).toBe(5);
});
test("UpdateTable with a StreamSpecification is refused and leaves the table intact", async ({ page }) => {
const result = await page.evaluate(async (table) => {
const client = globalThis.dynoxide.makeClient({ name: `stream-${crypto.randomUUID()}` });
await client.ready();
await client.execute("CreateTable", table);
await client.execute("PutItem", {
TableName: table.TableName,
Item: { artist: { S: "a" }, song: { S: "s1" } },
});
let err = null;
try {
await client.execute("UpdateTable", {
TableName: table.TableName,
StreamSpecification: { StreamEnabled: true, StreamViewType: "NEW_AND_OLD_IMAGES" },
});
} catch (e) {
err = { type: e.type, message: e.message };
}
const scan = await client.execute("Scan", { TableName: table.TableName });
const d = await client.execute("DescribeTable", { TableName: table.TableName });
const out = {
err,
count: scan.Count,
streamEnabled: (d.Table.StreamSpecification && d.Table.StreamSpecification.StreamEnabled) || false,
};
client.terminate();
return out;
}, MUSIC);
expect(result.err).not.toBeNull();
expect(result.err.message).toMatch(/not supported|streams/i);
expect(result.count).toBe(1); expect(result.streamEnabled).toBe(false); });
test("a GSI added to a populated table survives a reload (OPFS)", async ({ page }) => {
const name = `addgsi-persist-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
await page.evaluate(async ({ name, table }) => {
const client = globalThis.dynoxide.makeClient({ name });
await client.ready();
await client.execute("CreateTable", table);
await client.execute("PutItem", {
TableName: table.TableName,
Item: { artist: { S: "a" }, song: { S: "s1" }, genre: { S: "rock" } },
});
await client.execute("UpdateTable", {
TableName: table.TableName,
AttributeDefinitions: [
{ AttributeName: "artist", AttributeType: "S" },
{ AttributeName: "song", AttributeType: "S" },
{ AttributeName: "genre", AttributeType: "S" },
],
GlobalSecondaryIndexUpdates: [
{
Create: {
IndexName: "GenreIndex",
KeySchema: [{ AttributeName: "genre", KeyType: "HASH" }],
Projection: { ProjectionType: "ALL" },
},
},
],
});
client.terminate();
}, { name, table: MUSIC });
await page.waitForTimeout(150);
const reopened = await page.evaluate(async ({ name }) => {
const client = globalThis.dynoxide.makeClient({ name });
await client.ready();
const q = await client.execute("Query", {
TableName: "Music",
IndexName: "GenreIndex",
KeyConditionExpression: "genre = :g",
ExpressionAttributeValues: { ":g": { S: "rock" } },
});
const out = { mode: client.persistenceMode, count: q.Count };
client.terminate();
return out;
}, { name });
expect(reopened.mode).toBe("opfs");
expect(reopened.count).toBe(1); });
test("an overwrite and a delete keep GSI and LSI in step with the fan-out batched over the bridge", async ({ page }) => {
const result = await page.evaluate(async () => {
const client = globalThis.dynoxide.makeClient({ name: `fanout-${crypto.randomUUID()}` });
await client.ready();
await client.execute("CreateTable", {
TableName: "Fanout",
KeySchema: [
{ AttributeName: "pk", KeyType: "HASH" },
{ AttributeName: "sk", KeyType: "RANGE" },
],
AttributeDefinitions: [
{ AttributeName: "pk", AttributeType: "S" },
{ AttributeName: "sk", AttributeType: "S" },
{ AttributeName: "gpk", AttributeType: "S" },
{ AttributeName: "lsk", AttributeType: "S" },
],
GlobalSecondaryIndexes: [
{
IndexName: "byG",
KeySchema: [{ AttributeName: "gpk", KeyType: "HASH" }],
Projection: { ProjectionType: "ALL" },
},
],
LocalSecondaryIndexes: [
{
IndexName: "byL",
KeySchema: [
{ AttributeName: "pk", KeyType: "HASH" },
{ AttributeName: "lsk", KeyType: "RANGE" },
],
Projection: { ProjectionType: "ALL" },
},
],
BillingMode: "PAY_PER_REQUEST",
});
const put = (gpk, lsk) =>
client.execute("PutItem", {
TableName: "Fanout",
Item: { pk: { S: "p1" }, sk: { S: "s1" }, gpk: { S: gpk }, lsk: { S: lsk } },
});
const queryGsi = (gpk) =>
client.execute("Query", {
TableName: "Fanout",
IndexName: "byG",
KeyConditionExpression: "gpk = :g",
ExpressionAttributeValues: { ":g": { S: gpk } },
});
const queryLsi = () =>
client.execute("Query", {
TableName: "Fanout",
IndexName: "byL",
KeyConditionExpression: "pk = :p",
ExpressionAttributeValues: { ":p": { S: "p1" } },
});
await put("g1", "l1");
const g1Initial = (await queryGsi("g1")).Count;
const lsiInitial = (await queryLsi()).Items.map((i) => i.lsk.S);
await put("g2", "l2");
const g1AfterOverwrite = (await queryGsi("g1")).Count;
const g2AfterOverwrite = (await queryGsi("g2")).Count;
const lsiAfterOverwrite = (await queryLsi()).Items.map((i) => i.lsk.S);
await client.execute("DeleteItem", {
TableName: "Fanout",
Key: { pk: { S: "p1" }, sk: { S: "s1" } },
});
const g2AfterDelete = (await queryGsi("g2")).Count;
const lsiAfterDelete = (await queryLsi()).Count;
const out = {
mode: client.persistenceMode,
g1Initial,
lsiInitial,
g1AfterOverwrite,
g2AfterOverwrite,
lsiAfterOverwrite,
g2AfterDelete,
lsiAfterDelete,
};
client.terminate();
return out;
});
expect(result.mode).toBe("opfs");
expect(result.g1Initial).toBe(1); expect(result.lsiInitial).toEqual(["l1"]); expect(result.g1AfterOverwrite).toBe(0); expect(result.g2AfterOverwrite).toBe(1); expect(result.lsiAfterOverwrite).toEqual(["l2"]); expect(result.g2AfterDelete).toBe(0); expect(result.lsiAfterDelete).toBe(0); });