@noy-db/to-aws-dynamo 0.1.0-pre.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vLannaAi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @noy-db/to-aws-dynamo
2
+
3
+ > AWS DynamoDB adapter for [noy-db](https://github.com/vLannaAi/noy-db) — single-table design, zero-knowledge cloud sync.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@noy-db/to-aws-dynamo.svg)](https://www.npmjs.com/package/@noy-db/to-aws-dynamo)
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @noy-db/hub @noy-db/to-aws-dynamo @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
11
+ ```
12
+
13
+ `@aws-sdk/*` packages are peer dependencies — install them in your app.
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import { createNoydb } from '@noy-db/hub'
19
+ import { dynamo } from '@noy-db/to-aws-dynamo'
20
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
21
+ import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'
22
+
23
+ const client = DynamoDBDocumentClient.from(new DynamoDBClient({ region: 'ap-southeast-1' }))
24
+
25
+ const db = await createNoydb({
26
+ adapter: dynamo({ client, tableName: 'noydb-prod' }),
27
+ userId: 'alice',
28
+ passphrase: process.env.NOYDB_PASSPHRASE!,
29
+ })
30
+ ```
31
+
32
+ Uses a single-table design with composite keys `(PK=compartment, SK=collection#id)`. DynamoDB only ever sees encrypted envelopes — the ciphertext is useless without the user's passphrase.
33
+
34
+ ## DynamoDB table schema
35
+
36
+ - Partition key: `PK` (String) — compartment name
37
+ - Sort key: `SK` (String) — `collection#id`
38
+ - Attributes: `_v`, `_ts`, `_iv`, `_data`, `_by` — the encrypted envelope
39
+
40
+ ## License
41
+
42
+ MIT © vLannaAi — see the [noy-db repo](https://github.com/vLannaAi/noy-db) for full documentation.
package/dist/index.cjs ADDED
@@ -0,0 +1,226 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ dynamo: () => dynamo
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+ var import_hub = require("@noy-db/hub");
37
+ function dynamo(options) {
38
+ const { table } = options;
39
+ let clientPromise = null;
40
+ async function getClient() {
41
+ if (options.client) return options.client;
42
+ if (!clientPromise) {
43
+ clientPromise = (async () => {
44
+ const { DynamoDBClient } = await import("@aws-sdk/client-dynamodb");
45
+ const { DynamoDBDocumentClient } = await import("@aws-sdk/lib-dynamodb");
46
+ const config = {};
47
+ if (options.region) config["region"] = options.region;
48
+ if (options.endpoint) config["endpoint"] = options.endpoint;
49
+ const ddbClient = new DynamoDBClient(config);
50
+ return DynamoDBDocumentClient.from(ddbClient);
51
+ })();
52
+ }
53
+ return clientPromise;
54
+ }
55
+ function sk(collection, id) {
56
+ return `${collection}#${id}`;
57
+ }
58
+ function parseSk(sortKey) {
59
+ const idx = sortKey.indexOf("#");
60
+ return {
61
+ collection: sortKey.slice(0, idx),
62
+ id: sortKey.slice(idx + 1)
63
+ };
64
+ }
65
+ function itemToEnvelope(item) {
66
+ return {
67
+ _noydb: 1,
68
+ _v: item["_v"],
69
+ _ts: item["_ts"],
70
+ _iv: item["_iv"],
71
+ _data: item["_data"]
72
+ };
73
+ }
74
+ return {
75
+ name: "dynamo",
76
+ async get(vault, collection, id) {
77
+ const client = await getClient();
78
+ const { GetCommand } = await import("@aws-sdk/lib-dynamodb");
79
+ const result = await client.send(new GetCommand({
80
+ TableName: table,
81
+ Key: { pk: vault, sk: sk(collection, id) }
82
+ }));
83
+ if (!result.Item) return null;
84
+ return itemToEnvelope(result.Item);
85
+ },
86
+ async put(vault, collection, id, envelope, expectedVersion) {
87
+ const client = await getClient();
88
+ const { PutCommand } = await import("@aws-sdk/lib-dynamodb");
89
+ const item = {
90
+ pk: vault,
91
+ sk: sk(collection, id),
92
+ _noydb: envelope._noydb,
93
+ _v: envelope._v,
94
+ _ts: envelope._ts,
95
+ _iv: envelope._iv,
96
+ _data: envelope._data
97
+ };
98
+ const input = { TableName: table, Item: item };
99
+ if (expectedVersion !== void 0) {
100
+ input.ConditionExpression = "#v = :expected OR attribute_not_exists(pk)";
101
+ input.ExpressionAttributeNames = { "#v": "_v" };
102
+ input.ExpressionAttributeValues = { ":expected": expectedVersion };
103
+ }
104
+ try {
105
+ await client.send(new PutCommand(input));
106
+ } catch (err) {
107
+ if (err instanceof Error && err.name === "ConditionalCheckFailedException") {
108
+ const current = await this.get(vault, collection, id);
109
+ throw new import_hub.ConflictError(
110
+ current?._v ?? 0,
111
+ `Version conflict: expected ${expectedVersion}, found ${current?._v}`
112
+ );
113
+ }
114
+ throw err;
115
+ }
116
+ },
117
+ async delete(vault, collection, id) {
118
+ const client = await getClient();
119
+ const { DeleteCommand } = await import("@aws-sdk/lib-dynamodb");
120
+ await client.send(new DeleteCommand({
121
+ TableName: table,
122
+ Key: { pk: vault, sk: sk(collection, id) }
123
+ }));
124
+ },
125
+ async list(vault, collection) {
126
+ const client = await getClient();
127
+ const { QueryCommand } = await import("@aws-sdk/lib-dynamodb");
128
+ const result = await client.send(new QueryCommand({
129
+ TableName: table,
130
+ KeyConditionExpression: "pk = :pk AND begins_with(sk, :prefix)",
131
+ ExpressionAttributeValues: {
132
+ ":pk": vault,
133
+ ":prefix": `${collection}#`
134
+ }
135
+ }));
136
+ return (result.Items ?? []).map((item) => {
137
+ const { id } = parseSk(item["sk"]);
138
+ return id;
139
+ });
140
+ },
141
+ async loadAll(vault) {
142
+ const client = await getClient();
143
+ const { QueryCommand } = await import("@aws-sdk/lib-dynamodb");
144
+ const result = await client.send(new QueryCommand({
145
+ TableName: table,
146
+ KeyConditionExpression: "pk = :pk",
147
+ ExpressionAttributeValues: { ":pk": vault }
148
+ }));
149
+ const snapshot = {};
150
+ for (const item of result.Items ?? []) {
151
+ const sortKey = item["sk"];
152
+ const { collection, id } = parseSk(sortKey);
153
+ if (collection.startsWith("_")) continue;
154
+ if (!snapshot[collection]) {
155
+ snapshot[collection] = {};
156
+ }
157
+ snapshot[collection][id] = itemToEnvelope(item);
158
+ }
159
+ return snapshot;
160
+ },
161
+ async saveAll(vault, data) {
162
+ for (const [collName, records] of Object.entries(data)) {
163
+ for (const [id, envelope] of Object.entries(records)) {
164
+ await this.put(vault, collName, id, envelope);
165
+ }
166
+ }
167
+ },
168
+ async ping() {
169
+ try {
170
+ const client = await getClient();
171
+ const { QueryCommand } = await import("@aws-sdk/lib-dynamodb");
172
+ await client.send(new QueryCommand({
173
+ TableName: table,
174
+ KeyConditionExpression: "pk = :pk",
175
+ ExpressionAttributeValues: { ":pk": "__ping__" }
176
+ }));
177
+ return true;
178
+ } catch {
179
+ return false;
180
+ }
181
+ },
182
+ /**
183
+ * Paginate over a collection using DynamoDB's native `LastEvaluatedKey`
184
+ * cursor. The cursor is base64-encoded JSON of the LastEvaluatedKey
185
+ * object so it round-trips through any caller transport.
186
+ *
187
+ * Each page is a single Query call against the partition key, so the
188
+ * read cost is `pageSize ÷ 4 KB` RCUs (eventually consistent) per page.
189
+ */
190
+ async listPage(vault, collection, cursor, limit = 100) {
191
+ const client = await getClient();
192
+ const { QueryCommand } = await import("@aws-sdk/lib-dynamodb");
193
+ const input = {
194
+ TableName: table,
195
+ KeyConditionExpression: "pk = :pk AND begins_with(sk, :prefix)",
196
+ ExpressionAttributeValues: {
197
+ ":pk": vault,
198
+ ":prefix": `${collection}#`
199
+ },
200
+ Limit: limit
201
+ };
202
+ if (cursor) {
203
+ input.ExclusiveStartKey = JSON.parse(b64decode(cursor));
204
+ }
205
+ const result = await client.send(new QueryCommand(input));
206
+ const items = [];
207
+ for (const item of result.Items ?? []) {
208
+ const { id } = parseSk(item["sk"]);
209
+ items.push({ id, envelope: itemToEnvelope(item) });
210
+ }
211
+ const nextCursor = result.LastEvaluatedKey ? b64encode(JSON.stringify(result.LastEvaluatedKey)) : null;
212
+ return { items, nextCursor };
213
+ }
214
+ };
215
+ }
216
+ function b64encode(input) {
217
+ return btoa(unescape(encodeURIComponent(input)));
218
+ }
219
+ function b64decode(input) {
220
+ return decodeURIComponent(escape(atob(input)));
221
+ }
222
+ // Annotate the CommonJS export names for ESM import in node:
223
+ 0 && (module.exports = {
224
+ dynamo
225
+ });
226
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/to-aws-dynamo** — DynamoDB single-table store for NOYDB.\n *\n * Uses a single DynamoDB table with a composite key:\n * - **`pk`** (partition key, String) — vault name.\n * - **`sk`** (sort key, String) — `{collection}#{id}`.\n *\n * This layout keeps every record of a vault in one partition, making\n * `loadAll()` a single `Query` call with no scatter-gather. Individual\n * reads and writes use `GetItem` / `PutItem` with optional\n * `ConditionExpression` for atomic compare-and-swap.\n *\n * ## When to use\n *\n * - **Cloud-synced vaults** — DynamoDB's per-item CAS\n * (`casAtomic: true`) is safe under concurrent writes from multiple\n * clients (multi-user, multi-tab).\n * - **Serverless / Lambda** — no connection pool to manage; each\n * invocation opens fresh HTTP connections via the AWS SDK.\n * - **Pair with S3 for blobs** — keep structured records in DynamoDB\n * and route binary attachments to `@noy-db/to-aws-s3` via `routeStore`.\n *\n * ## IAM minimum permissions\n *\n * ```json\n * { \"Action\": [\"dynamodb:GetItem\", \"dynamodb:PutItem\",\n * \"dynamodb:DeleteItem\", \"dynamodb:Query\"] }\n * ```\n *\n * ## Capabilities\n *\n * | Capability | Value |\n * |---|---|\n * | `casAtomic` | `true` — DynamoDB `ConditionExpression` on `_v` |\n * | `ping` | ✓ — `DescribeTable` |\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\n\n/**\n * Options for `dynamo()`.\n *\n * The adapter uses a single-table design with a composite primary key\n * `pk = {vault}` (partition) and `sk = {collection}#{id}` (sort). This keeps\n * all records for a vault in a single DynamoDB partition for efficient\n * `loadAll()` via a single Query. For DynamoDB Local development, set\n * `endpoint: 'http://localhost:8000'`.\n */\nexport interface DynamoOptions {\n /** DynamoDB table name. */\n table: string\n /** AWS region. Default: 'us-east-1'. */\n region?: string\n /** Custom endpoint (e.g., 'http://localhost:8000' for DynamoDB Local). */\n endpoint?: string\n /** DynamoDB document client instance (for advanced configuration). */\n client?: DynamoDocClient\n}\n\n/**\n * Minimal interface for DynamoDB document client operations.\n * Compatible with @aws-sdk/lib-dynamodb's DynamoDBDocumentClient.\n */\nexport interface DynamoDocClient {\n send(command: unknown): Promise<unknown>\n}\n\n// Command types matching @aws-sdk/lib-dynamodb\ninterface GetCommandInput { TableName: string; Key: Record<string, unknown> }\ninterface PutCommandInput { TableName: string; Item: Record<string, unknown>; ConditionExpression?: string; ExpressionAttributeNames?: Record<string, string>; ExpressionAttributeValues?: Record<string, unknown> }\ninterface DeleteCommandInput { TableName: string; Key: Record<string, unknown> }\ninterface QueryCommandInput {\n TableName: string\n KeyConditionExpression: string\n ExpressionAttributeNames?: Record<string, string>\n ExpressionAttributeValues?: Record<string, unknown>\n Limit?: number\n ExclusiveStartKey?: Record<string, unknown>\n}\n\n/**\n * Create a DynamoDB adapter using single-table design.\n *\n * Table schema:\n * - pk (String, partition key): vault name\n * - sk (String, sort key): `{collection}#{id}` or `_keyring#{userId}` or `_sync#meta`\n * - _v (Number): record version\n * - _ts (String): timestamp\n * - _iv (String): base64 IV\n * - _data (String): base64 ciphertext\n */\nexport function dynamo(options: DynamoOptions): NoydbStore {\n const { table } = options\n\n // Lazy client initialization — only creates the client when first used\n let clientPromise: Promise<DynamoDocClient> | null = null\n\n async function getClient(): Promise<DynamoDocClient> {\n if (options.client) return options.client\n\n if (!clientPromise) {\n clientPromise = (async () => {\n // Dynamic import to keep @aws-sdk as a peer dep\n const { DynamoDBClient } = await import('@aws-sdk/client-dynamodb') as { DynamoDBClient: new (config: Record<string, unknown>) => unknown }\n const { DynamoDBDocumentClient } = await import('@aws-sdk/lib-dynamodb') as { DynamoDBDocumentClient: { from: (client: unknown) => DynamoDocClient } }\n\n const config: Record<string, unknown> = {}\n if (options.region) config['region'] = options.region\n if (options.endpoint) config['endpoint'] = options.endpoint\n\n const ddbClient = new DynamoDBClient(config)\n return DynamoDBDocumentClient.from(ddbClient)\n })()\n }\n\n return clientPromise\n }\n\n function sk(collection: string, id: string): string {\n return `${collection}#${id}`\n }\n\n function parseSk(sortKey: string): { collection: string; id: string } {\n const idx = sortKey.indexOf('#')\n return {\n collection: sortKey.slice(0, idx),\n id: sortKey.slice(idx + 1),\n }\n }\n\n function itemToEnvelope(item: Record<string, unknown>): EncryptedEnvelope {\n return {\n _noydb: 1,\n _v: item['_v'] as number,\n _ts: item['_ts'] as string,\n _iv: item['_iv'] as string,\n _data: item['_data'] as string,\n }\n }\n\n return {\n name: 'dynamo',\n\n async get(vault, collection, id) {\n const client = await getClient()\n const { GetCommand } = await import('@aws-sdk/lib-dynamodb') as { GetCommand: new (input: GetCommandInput) => unknown }\n\n const result = await client.send(new GetCommand({\n TableName: table,\n Key: { pk: vault, sk: sk(collection, id) },\n })) as { Item?: Record<string, unknown> }\n\n if (!result.Item) return null\n return itemToEnvelope(result.Item)\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n const client = await getClient()\n const { PutCommand } = await import('@aws-sdk/lib-dynamodb') as { PutCommand: new (input: PutCommandInput) => unknown }\n\n const item: Record<string, unknown> = {\n pk: vault,\n sk: sk(collection, id),\n _noydb: envelope._noydb,\n _v: envelope._v,\n _ts: envelope._ts,\n _iv: envelope._iv,\n _data: envelope._data,\n }\n\n const input: PutCommandInput = { TableName: table, Item: item }\n\n if (expectedVersion !== undefined) {\n input.ConditionExpression = '#v = :expected OR attribute_not_exists(pk)'\n input.ExpressionAttributeNames = { '#v': '_v' }\n input.ExpressionAttributeValues = { ':expected': expectedVersion }\n }\n\n try {\n await client.send(new PutCommand(input))\n } catch (err: unknown) {\n if (err instanceof Error && err.name === 'ConditionalCheckFailedException') {\n // Fetch current version for error\n const current = await this.get(vault, collection, id)\n throw new ConflictError(\n current?._v ?? 0,\n `Version conflict: expected ${expectedVersion}, found ${current?._v}`,\n )\n }\n throw err\n }\n },\n\n async delete(vault, collection, id) {\n const client = await getClient()\n const { DeleteCommand } = await import('@aws-sdk/lib-dynamodb') as { DeleteCommand: new (input: DeleteCommandInput) => unknown }\n\n await client.send(new DeleteCommand({\n TableName: table,\n Key: { pk: vault, sk: sk(collection, id) },\n }))\n },\n\n async list(vault, collection) {\n const client = await getClient()\n const { QueryCommand } = await import('@aws-sdk/lib-dynamodb') as { QueryCommand: new (input: QueryCommandInput) => unknown }\n\n const result = await client.send(new QueryCommand({\n TableName: table,\n KeyConditionExpression: 'pk = :pk AND begins_with(sk, :prefix)',\n ExpressionAttributeValues: {\n ':pk': vault,\n ':prefix': `${collection}#`,\n },\n })) as { Items?: Record<string, unknown>[] }\n\n return (result.Items ?? []).map(item => {\n const { id } = parseSk(item['sk'] as string)\n return id\n })\n },\n\n async loadAll(vault) {\n const client = await getClient()\n const { QueryCommand } = await import('@aws-sdk/lib-dynamodb') as { QueryCommand: new (input: QueryCommandInput) => unknown }\n\n const result = await client.send(new QueryCommand({\n TableName: table,\n KeyConditionExpression: 'pk = :pk',\n ExpressionAttributeValues: { ':pk': vault },\n })) as { Items?: Record<string, unknown>[] }\n\n const snapshot: VaultSnapshot = {}\n\n for (const item of result.Items ?? []) {\n const sortKey = item['sk'] as string\n const { collection, id } = parseSk(sortKey)\n\n if (collection.startsWith('_')) continue // skip _keyring, _sync\n\n if (!snapshot[collection]) {\n snapshot[collection] = {}\n }\n snapshot[collection][id] = itemToEnvelope(item)\n }\n\n return snapshot\n },\n\n async saveAll(vault, data) {\n // Use individual puts (DynamoDB batch write has limitations with conditions)\n for (const [collName, records] of Object.entries(data)) {\n for (const [id, envelope] of Object.entries(records)) {\n await this.put(vault, collName, id, envelope)\n }\n }\n },\n\n async ping() {\n try {\n const client = await getClient()\n const { QueryCommand } = await import('@aws-sdk/lib-dynamodb') as { QueryCommand: new (input: QueryCommandInput) => unknown }\n\n await client.send(new QueryCommand({\n TableName: table,\n KeyConditionExpression: 'pk = :pk',\n ExpressionAttributeValues: { ':pk': '__ping__' },\n }))\n return true\n } catch {\n return false\n }\n },\n\n /**\n * Paginate over a collection using DynamoDB's native `LastEvaluatedKey`\n * cursor. The cursor is base64-encoded JSON of the LastEvaluatedKey\n * object so it round-trips through any caller transport.\n *\n * Each page is a single Query call against the partition key, so the\n * read cost is `pageSize ÷ 4 KB` RCUs (eventually consistent) per page.\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const client = await getClient()\n const { QueryCommand } = await import('@aws-sdk/lib-dynamodb') as { QueryCommand: new (input: QueryCommandInput) => unknown }\n\n const input: QueryCommandInput = {\n TableName: table,\n KeyConditionExpression: 'pk = :pk AND begins_with(sk, :prefix)',\n ExpressionAttributeValues: {\n ':pk': vault,\n ':prefix': `${collection}#`,\n },\n Limit: limit,\n }\n if (cursor) {\n input.ExclusiveStartKey = JSON.parse(b64decode(cursor)) as Record<string, unknown>\n }\n\n const result = await client.send(new QueryCommand(input)) as {\n Items?: Record<string, unknown>[]\n LastEvaluatedKey?: Record<string, unknown>\n }\n\n const items: Array<{ id: string; envelope: EncryptedEnvelope }> = []\n for (const item of result.Items ?? []) {\n const { id } = parseSk(item['sk'] as string)\n items.push({ id, envelope: itemToEnvelope(item) })\n }\n\n const nextCursor = result.LastEvaluatedKey\n ? b64encode(JSON.stringify(result.LastEvaluatedKey))\n : null\n\n return { items, nextCursor }\n },\n }\n}\n\n/**\n * Tiny base64 helpers that work in both Node 20+ and any modern browser\n * without pulling in @types/node or relying on a Buffer polyfill. The\n * dynamo adapter has zero non-AWS dependencies and we want to keep it\n * that way — listPage cursors are short JSON blobs so the per-call cost\n * of these helpers is negligible.\n */\nfunction b64encode(input: string): string {\n // btoa expects a Latin-1 string; encodeURIComponent + unescape is the\n // canonical trick for utf-8 → btoa-safe payloads.\n return btoa(unescape(encodeURIComponent(input)))\n}\n\nfunction b64decode(input: string): string {\n return decodeURIComponent(escape(atob(input)))\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,81 @@
1
+ import { NoydbStore } from '@noy-db/hub';
2
+
3
+ /**
4
+ * **@noy-db/to-aws-dynamo** — DynamoDB single-table store for NOYDB.
5
+ *
6
+ * Uses a single DynamoDB table with a composite key:
7
+ * - **`pk`** (partition key, String) — vault name.
8
+ * - **`sk`** (sort key, String) — `{collection}#{id}`.
9
+ *
10
+ * This layout keeps every record of a vault in one partition, making
11
+ * `loadAll()` a single `Query` call with no scatter-gather. Individual
12
+ * reads and writes use `GetItem` / `PutItem` with optional
13
+ * `ConditionExpression` for atomic compare-and-swap.
14
+ *
15
+ * ## When to use
16
+ *
17
+ * - **Cloud-synced vaults** — DynamoDB's per-item CAS
18
+ * (`casAtomic: true`) is safe under concurrent writes from multiple
19
+ * clients (multi-user, multi-tab).
20
+ * - **Serverless / Lambda** — no connection pool to manage; each
21
+ * invocation opens fresh HTTP connections via the AWS SDK.
22
+ * - **Pair with S3 for blobs** — keep structured records in DynamoDB
23
+ * and route binary attachments to `@noy-db/to-aws-s3` via `routeStore`.
24
+ *
25
+ * ## IAM minimum permissions
26
+ *
27
+ * ```json
28
+ * { "Action": ["dynamodb:GetItem", "dynamodb:PutItem",
29
+ * "dynamodb:DeleteItem", "dynamodb:Query"] }
30
+ * ```
31
+ *
32
+ * ## Capabilities
33
+ *
34
+ * | Capability | Value |
35
+ * |---|---|
36
+ * | `casAtomic` | `true` — DynamoDB `ConditionExpression` on `_v` |
37
+ * | `ping` | ✓ — `DescribeTable` |
38
+ *
39
+ * @packageDocumentation
40
+ */
41
+
42
+ /**
43
+ * Options for `dynamo()`.
44
+ *
45
+ * The adapter uses a single-table design with a composite primary key
46
+ * `pk = {vault}` (partition) and `sk = {collection}#{id}` (sort). This keeps
47
+ * all records for a vault in a single DynamoDB partition for efficient
48
+ * `loadAll()` via a single Query. For DynamoDB Local development, set
49
+ * `endpoint: 'http://localhost:8000'`.
50
+ */
51
+ interface DynamoOptions {
52
+ /** DynamoDB table name. */
53
+ table: string;
54
+ /** AWS region. Default: 'us-east-1'. */
55
+ region?: string;
56
+ /** Custom endpoint (e.g., 'http://localhost:8000' for DynamoDB Local). */
57
+ endpoint?: string;
58
+ /** DynamoDB document client instance (for advanced configuration). */
59
+ client?: DynamoDocClient;
60
+ }
61
+ /**
62
+ * Minimal interface for DynamoDB document client operations.
63
+ * Compatible with @aws-sdk/lib-dynamodb's DynamoDBDocumentClient.
64
+ */
65
+ interface DynamoDocClient {
66
+ send(command: unknown): Promise<unknown>;
67
+ }
68
+ /**
69
+ * Create a DynamoDB adapter using single-table design.
70
+ *
71
+ * Table schema:
72
+ * - pk (String, partition key): vault name
73
+ * - sk (String, sort key): `{collection}#{id}` or `_keyring#{userId}` or `_sync#meta`
74
+ * - _v (Number): record version
75
+ * - _ts (String): timestamp
76
+ * - _iv (String): base64 IV
77
+ * - _data (String): base64 ciphertext
78
+ */
79
+ declare function dynamo(options: DynamoOptions): NoydbStore;
80
+
81
+ export { type DynamoDocClient, type DynamoOptions, dynamo };
@@ -0,0 +1,81 @@
1
+ import { NoydbStore } from '@noy-db/hub';
2
+
3
+ /**
4
+ * **@noy-db/to-aws-dynamo** — DynamoDB single-table store for NOYDB.
5
+ *
6
+ * Uses a single DynamoDB table with a composite key:
7
+ * - **`pk`** (partition key, String) — vault name.
8
+ * - **`sk`** (sort key, String) — `{collection}#{id}`.
9
+ *
10
+ * This layout keeps every record of a vault in one partition, making
11
+ * `loadAll()` a single `Query` call with no scatter-gather. Individual
12
+ * reads and writes use `GetItem` / `PutItem` with optional
13
+ * `ConditionExpression` for atomic compare-and-swap.
14
+ *
15
+ * ## When to use
16
+ *
17
+ * - **Cloud-synced vaults** — DynamoDB's per-item CAS
18
+ * (`casAtomic: true`) is safe under concurrent writes from multiple
19
+ * clients (multi-user, multi-tab).
20
+ * - **Serverless / Lambda** — no connection pool to manage; each
21
+ * invocation opens fresh HTTP connections via the AWS SDK.
22
+ * - **Pair with S3 for blobs** — keep structured records in DynamoDB
23
+ * and route binary attachments to `@noy-db/to-aws-s3` via `routeStore`.
24
+ *
25
+ * ## IAM minimum permissions
26
+ *
27
+ * ```json
28
+ * { "Action": ["dynamodb:GetItem", "dynamodb:PutItem",
29
+ * "dynamodb:DeleteItem", "dynamodb:Query"] }
30
+ * ```
31
+ *
32
+ * ## Capabilities
33
+ *
34
+ * | Capability | Value |
35
+ * |---|---|
36
+ * | `casAtomic` | `true` — DynamoDB `ConditionExpression` on `_v` |
37
+ * | `ping` | ✓ — `DescribeTable` |
38
+ *
39
+ * @packageDocumentation
40
+ */
41
+
42
+ /**
43
+ * Options for `dynamo()`.
44
+ *
45
+ * The adapter uses a single-table design with a composite primary key
46
+ * `pk = {vault}` (partition) and `sk = {collection}#{id}` (sort). This keeps
47
+ * all records for a vault in a single DynamoDB partition for efficient
48
+ * `loadAll()` via a single Query. For DynamoDB Local development, set
49
+ * `endpoint: 'http://localhost:8000'`.
50
+ */
51
+ interface DynamoOptions {
52
+ /** DynamoDB table name. */
53
+ table: string;
54
+ /** AWS region. Default: 'us-east-1'. */
55
+ region?: string;
56
+ /** Custom endpoint (e.g., 'http://localhost:8000' for DynamoDB Local). */
57
+ endpoint?: string;
58
+ /** DynamoDB document client instance (for advanced configuration). */
59
+ client?: DynamoDocClient;
60
+ }
61
+ /**
62
+ * Minimal interface for DynamoDB document client operations.
63
+ * Compatible with @aws-sdk/lib-dynamodb's DynamoDBDocumentClient.
64
+ */
65
+ interface DynamoDocClient {
66
+ send(command: unknown): Promise<unknown>;
67
+ }
68
+ /**
69
+ * Create a DynamoDB adapter using single-table design.
70
+ *
71
+ * Table schema:
72
+ * - pk (String, partition key): vault name
73
+ * - sk (String, sort key): `{collection}#{id}` or `_keyring#{userId}` or `_sync#meta`
74
+ * - _v (Number): record version
75
+ * - _ts (String): timestamp
76
+ * - _iv (String): base64 IV
77
+ * - _data (String): base64 ciphertext
78
+ */
79
+ declare function dynamo(options: DynamoOptions): NoydbStore;
80
+
81
+ export { type DynamoDocClient, type DynamoOptions, dynamo };
package/dist/index.js ADDED
@@ -0,0 +1,191 @@
1
+ // src/index.ts
2
+ import { ConflictError } from "@noy-db/hub";
3
+ function dynamo(options) {
4
+ const { table } = options;
5
+ let clientPromise = null;
6
+ async function getClient() {
7
+ if (options.client) return options.client;
8
+ if (!clientPromise) {
9
+ clientPromise = (async () => {
10
+ const { DynamoDBClient } = await import("@aws-sdk/client-dynamodb");
11
+ const { DynamoDBDocumentClient } = await import("@aws-sdk/lib-dynamodb");
12
+ const config = {};
13
+ if (options.region) config["region"] = options.region;
14
+ if (options.endpoint) config["endpoint"] = options.endpoint;
15
+ const ddbClient = new DynamoDBClient(config);
16
+ return DynamoDBDocumentClient.from(ddbClient);
17
+ })();
18
+ }
19
+ return clientPromise;
20
+ }
21
+ function sk(collection, id) {
22
+ return `${collection}#${id}`;
23
+ }
24
+ function parseSk(sortKey) {
25
+ const idx = sortKey.indexOf("#");
26
+ return {
27
+ collection: sortKey.slice(0, idx),
28
+ id: sortKey.slice(idx + 1)
29
+ };
30
+ }
31
+ function itemToEnvelope(item) {
32
+ return {
33
+ _noydb: 1,
34
+ _v: item["_v"],
35
+ _ts: item["_ts"],
36
+ _iv: item["_iv"],
37
+ _data: item["_data"]
38
+ };
39
+ }
40
+ return {
41
+ name: "dynamo",
42
+ async get(vault, collection, id) {
43
+ const client = await getClient();
44
+ const { GetCommand } = await import("@aws-sdk/lib-dynamodb");
45
+ const result = await client.send(new GetCommand({
46
+ TableName: table,
47
+ Key: { pk: vault, sk: sk(collection, id) }
48
+ }));
49
+ if (!result.Item) return null;
50
+ return itemToEnvelope(result.Item);
51
+ },
52
+ async put(vault, collection, id, envelope, expectedVersion) {
53
+ const client = await getClient();
54
+ const { PutCommand } = await import("@aws-sdk/lib-dynamodb");
55
+ const item = {
56
+ pk: vault,
57
+ sk: sk(collection, id),
58
+ _noydb: envelope._noydb,
59
+ _v: envelope._v,
60
+ _ts: envelope._ts,
61
+ _iv: envelope._iv,
62
+ _data: envelope._data
63
+ };
64
+ const input = { TableName: table, Item: item };
65
+ if (expectedVersion !== void 0) {
66
+ input.ConditionExpression = "#v = :expected OR attribute_not_exists(pk)";
67
+ input.ExpressionAttributeNames = { "#v": "_v" };
68
+ input.ExpressionAttributeValues = { ":expected": expectedVersion };
69
+ }
70
+ try {
71
+ await client.send(new PutCommand(input));
72
+ } catch (err) {
73
+ if (err instanceof Error && err.name === "ConditionalCheckFailedException") {
74
+ const current = await this.get(vault, collection, id);
75
+ throw new ConflictError(
76
+ current?._v ?? 0,
77
+ `Version conflict: expected ${expectedVersion}, found ${current?._v}`
78
+ );
79
+ }
80
+ throw err;
81
+ }
82
+ },
83
+ async delete(vault, collection, id) {
84
+ const client = await getClient();
85
+ const { DeleteCommand } = await import("@aws-sdk/lib-dynamodb");
86
+ await client.send(new DeleteCommand({
87
+ TableName: table,
88
+ Key: { pk: vault, sk: sk(collection, id) }
89
+ }));
90
+ },
91
+ async list(vault, collection) {
92
+ const client = await getClient();
93
+ const { QueryCommand } = await import("@aws-sdk/lib-dynamodb");
94
+ const result = await client.send(new QueryCommand({
95
+ TableName: table,
96
+ KeyConditionExpression: "pk = :pk AND begins_with(sk, :prefix)",
97
+ ExpressionAttributeValues: {
98
+ ":pk": vault,
99
+ ":prefix": `${collection}#`
100
+ }
101
+ }));
102
+ return (result.Items ?? []).map((item) => {
103
+ const { id } = parseSk(item["sk"]);
104
+ return id;
105
+ });
106
+ },
107
+ async loadAll(vault) {
108
+ const client = await getClient();
109
+ const { QueryCommand } = await import("@aws-sdk/lib-dynamodb");
110
+ const result = await client.send(new QueryCommand({
111
+ TableName: table,
112
+ KeyConditionExpression: "pk = :pk",
113
+ ExpressionAttributeValues: { ":pk": vault }
114
+ }));
115
+ const snapshot = {};
116
+ for (const item of result.Items ?? []) {
117
+ const sortKey = item["sk"];
118
+ const { collection, id } = parseSk(sortKey);
119
+ if (collection.startsWith("_")) continue;
120
+ if (!snapshot[collection]) {
121
+ snapshot[collection] = {};
122
+ }
123
+ snapshot[collection][id] = itemToEnvelope(item);
124
+ }
125
+ return snapshot;
126
+ },
127
+ async saveAll(vault, data) {
128
+ for (const [collName, records] of Object.entries(data)) {
129
+ for (const [id, envelope] of Object.entries(records)) {
130
+ await this.put(vault, collName, id, envelope);
131
+ }
132
+ }
133
+ },
134
+ async ping() {
135
+ try {
136
+ const client = await getClient();
137
+ const { QueryCommand } = await import("@aws-sdk/lib-dynamodb");
138
+ await client.send(new QueryCommand({
139
+ TableName: table,
140
+ KeyConditionExpression: "pk = :pk",
141
+ ExpressionAttributeValues: { ":pk": "__ping__" }
142
+ }));
143
+ return true;
144
+ } catch {
145
+ return false;
146
+ }
147
+ },
148
+ /**
149
+ * Paginate over a collection using DynamoDB's native `LastEvaluatedKey`
150
+ * cursor. The cursor is base64-encoded JSON of the LastEvaluatedKey
151
+ * object so it round-trips through any caller transport.
152
+ *
153
+ * Each page is a single Query call against the partition key, so the
154
+ * read cost is `pageSize ÷ 4 KB` RCUs (eventually consistent) per page.
155
+ */
156
+ async listPage(vault, collection, cursor, limit = 100) {
157
+ const client = await getClient();
158
+ const { QueryCommand } = await import("@aws-sdk/lib-dynamodb");
159
+ const input = {
160
+ TableName: table,
161
+ KeyConditionExpression: "pk = :pk AND begins_with(sk, :prefix)",
162
+ ExpressionAttributeValues: {
163
+ ":pk": vault,
164
+ ":prefix": `${collection}#`
165
+ },
166
+ Limit: limit
167
+ };
168
+ if (cursor) {
169
+ input.ExclusiveStartKey = JSON.parse(b64decode(cursor));
170
+ }
171
+ const result = await client.send(new QueryCommand(input));
172
+ const items = [];
173
+ for (const item of result.Items ?? []) {
174
+ const { id } = parseSk(item["sk"]);
175
+ items.push({ id, envelope: itemToEnvelope(item) });
176
+ }
177
+ const nextCursor = result.LastEvaluatedKey ? b64encode(JSON.stringify(result.LastEvaluatedKey)) : null;
178
+ return { items, nextCursor };
179
+ }
180
+ };
181
+ }
182
+ function b64encode(input) {
183
+ return btoa(unescape(encodeURIComponent(input)));
184
+ }
185
+ function b64decode(input) {
186
+ return decodeURIComponent(escape(atob(input)));
187
+ }
188
+ export {
189
+ dynamo
190
+ };
191
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/to-aws-dynamo** — DynamoDB single-table store for NOYDB.\n *\n * Uses a single DynamoDB table with a composite key:\n * - **`pk`** (partition key, String) — vault name.\n * - **`sk`** (sort key, String) — `{collection}#{id}`.\n *\n * This layout keeps every record of a vault in one partition, making\n * `loadAll()` a single `Query` call with no scatter-gather. Individual\n * reads and writes use `GetItem` / `PutItem` with optional\n * `ConditionExpression` for atomic compare-and-swap.\n *\n * ## When to use\n *\n * - **Cloud-synced vaults** — DynamoDB's per-item CAS\n * (`casAtomic: true`) is safe under concurrent writes from multiple\n * clients (multi-user, multi-tab).\n * - **Serverless / Lambda** — no connection pool to manage; each\n * invocation opens fresh HTTP connections via the AWS SDK.\n * - **Pair with S3 for blobs** — keep structured records in DynamoDB\n * and route binary attachments to `@noy-db/to-aws-s3` via `routeStore`.\n *\n * ## IAM minimum permissions\n *\n * ```json\n * { \"Action\": [\"dynamodb:GetItem\", \"dynamodb:PutItem\",\n * \"dynamodb:DeleteItem\", \"dynamodb:Query\"] }\n * ```\n *\n * ## Capabilities\n *\n * | Capability | Value |\n * |---|---|\n * | `casAtomic` | `true` — DynamoDB `ConditionExpression` on `_v` |\n * | `ping` | ✓ — `DescribeTable` |\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\n\n/**\n * Options for `dynamo()`.\n *\n * The adapter uses a single-table design with a composite primary key\n * `pk = {vault}` (partition) and `sk = {collection}#{id}` (sort). This keeps\n * all records for a vault in a single DynamoDB partition for efficient\n * `loadAll()` via a single Query. For DynamoDB Local development, set\n * `endpoint: 'http://localhost:8000'`.\n */\nexport interface DynamoOptions {\n /** DynamoDB table name. */\n table: string\n /** AWS region. Default: 'us-east-1'. */\n region?: string\n /** Custom endpoint (e.g., 'http://localhost:8000' for DynamoDB Local). */\n endpoint?: string\n /** DynamoDB document client instance (for advanced configuration). */\n client?: DynamoDocClient\n}\n\n/**\n * Minimal interface for DynamoDB document client operations.\n * Compatible with @aws-sdk/lib-dynamodb's DynamoDBDocumentClient.\n */\nexport interface DynamoDocClient {\n send(command: unknown): Promise<unknown>\n}\n\n// Command types matching @aws-sdk/lib-dynamodb\ninterface GetCommandInput { TableName: string; Key: Record<string, unknown> }\ninterface PutCommandInput { TableName: string; Item: Record<string, unknown>; ConditionExpression?: string; ExpressionAttributeNames?: Record<string, string>; ExpressionAttributeValues?: Record<string, unknown> }\ninterface DeleteCommandInput { TableName: string; Key: Record<string, unknown> }\ninterface QueryCommandInput {\n TableName: string\n KeyConditionExpression: string\n ExpressionAttributeNames?: Record<string, string>\n ExpressionAttributeValues?: Record<string, unknown>\n Limit?: number\n ExclusiveStartKey?: Record<string, unknown>\n}\n\n/**\n * Create a DynamoDB adapter using single-table design.\n *\n * Table schema:\n * - pk (String, partition key): vault name\n * - sk (String, sort key): `{collection}#{id}` or `_keyring#{userId}` or `_sync#meta`\n * - _v (Number): record version\n * - _ts (String): timestamp\n * - _iv (String): base64 IV\n * - _data (String): base64 ciphertext\n */\nexport function dynamo(options: DynamoOptions): NoydbStore {\n const { table } = options\n\n // Lazy client initialization — only creates the client when first used\n let clientPromise: Promise<DynamoDocClient> | null = null\n\n async function getClient(): Promise<DynamoDocClient> {\n if (options.client) return options.client\n\n if (!clientPromise) {\n clientPromise = (async () => {\n // Dynamic import to keep @aws-sdk as a peer dep\n const { DynamoDBClient } = await import('@aws-sdk/client-dynamodb') as { DynamoDBClient: new (config: Record<string, unknown>) => unknown }\n const { DynamoDBDocumentClient } = await import('@aws-sdk/lib-dynamodb') as { DynamoDBDocumentClient: { from: (client: unknown) => DynamoDocClient } }\n\n const config: Record<string, unknown> = {}\n if (options.region) config['region'] = options.region\n if (options.endpoint) config['endpoint'] = options.endpoint\n\n const ddbClient = new DynamoDBClient(config)\n return DynamoDBDocumentClient.from(ddbClient)\n })()\n }\n\n return clientPromise\n }\n\n function sk(collection: string, id: string): string {\n return `${collection}#${id}`\n }\n\n function parseSk(sortKey: string): { collection: string; id: string } {\n const idx = sortKey.indexOf('#')\n return {\n collection: sortKey.slice(0, idx),\n id: sortKey.slice(idx + 1),\n }\n }\n\n function itemToEnvelope(item: Record<string, unknown>): EncryptedEnvelope {\n return {\n _noydb: 1,\n _v: item['_v'] as number,\n _ts: item['_ts'] as string,\n _iv: item['_iv'] as string,\n _data: item['_data'] as string,\n }\n }\n\n return {\n name: 'dynamo',\n\n async get(vault, collection, id) {\n const client = await getClient()\n const { GetCommand } = await import('@aws-sdk/lib-dynamodb') as { GetCommand: new (input: GetCommandInput) => unknown }\n\n const result = await client.send(new GetCommand({\n TableName: table,\n Key: { pk: vault, sk: sk(collection, id) },\n })) as { Item?: Record<string, unknown> }\n\n if (!result.Item) return null\n return itemToEnvelope(result.Item)\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n const client = await getClient()\n const { PutCommand } = await import('@aws-sdk/lib-dynamodb') as { PutCommand: new (input: PutCommandInput) => unknown }\n\n const item: Record<string, unknown> = {\n pk: vault,\n sk: sk(collection, id),\n _noydb: envelope._noydb,\n _v: envelope._v,\n _ts: envelope._ts,\n _iv: envelope._iv,\n _data: envelope._data,\n }\n\n const input: PutCommandInput = { TableName: table, Item: item }\n\n if (expectedVersion !== undefined) {\n input.ConditionExpression = '#v = :expected OR attribute_not_exists(pk)'\n input.ExpressionAttributeNames = { '#v': '_v' }\n input.ExpressionAttributeValues = { ':expected': expectedVersion }\n }\n\n try {\n await client.send(new PutCommand(input))\n } catch (err: unknown) {\n if (err instanceof Error && err.name === 'ConditionalCheckFailedException') {\n // Fetch current version for error\n const current = await this.get(vault, collection, id)\n throw new ConflictError(\n current?._v ?? 0,\n `Version conflict: expected ${expectedVersion}, found ${current?._v}`,\n )\n }\n throw err\n }\n },\n\n async delete(vault, collection, id) {\n const client = await getClient()\n const { DeleteCommand } = await import('@aws-sdk/lib-dynamodb') as { DeleteCommand: new (input: DeleteCommandInput) => unknown }\n\n await client.send(new DeleteCommand({\n TableName: table,\n Key: { pk: vault, sk: sk(collection, id) },\n }))\n },\n\n async list(vault, collection) {\n const client = await getClient()\n const { QueryCommand } = await import('@aws-sdk/lib-dynamodb') as { QueryCommand: new (input: QueryCommandInput) => unknown }\n\n const result = await client.send(new QueryCommand({\n TableName: table,\n KeyConditionExpression: 'pk = :pk AND begins_with(sk, :prefix)',\n ExpressionAttributeValues: {\n ':pk': vault,\n ':prefix': `${collection}#`,\n },\n })) as { Items?: Record<string, unknown>[] }\n\n return (result.Items ?? []).map(item => {\n const { id } = parseSk(item['sk'] as string)\n return id\n })\n },\n\n async loadAll(vault) {\n const client = await getClient()\n const { QueryCommand } = await import('@aws-sdk/lib-dynamodb') as { QueryCommand: new (input: QueryCommandInput) => unknown }\n\n const result = await client.send(new QueryCommand({\n TableName: table,\n KeyConditionExpression: 'pk = :pk',\n ExpressionAttributeValues: { ':pk': vault },\n })) as { Items?: Record<string, unknown>[] }\n\n const snapshot: VaultSnapshot = {}\n\n for (const item of result.Items ?? []) {\n const sortKey = item['sk'] as string\n const { collection, id } = parseSk(sortKey)\n\n if (collection.startsWith('_')) continue // skip _keyring, _sync\n\n if (!snapshot[collection]) {\n snapshot[collection] = {}\n }\n snapshot[collection][id] = itemToEnvelope(item)\n }\n\n return snapshot\n },\n\n async saveAll(vault, data) {\n // Use individual puts (DynamoDB batch write has limitations with conditions)\n for (const [collName, records] of Object.entries(data)) {\n for (const [id, envelope] of Object.entries(records)) {\n await this.put(vault, collName, id, envelope)\n }\n }\n },\n\n async ping() {\n try {\n const client = await getClient()\n const { QueryCommand } = await import('@aws-sdk/lib-dynamodb') as { QueryCommand: new (input: QueryCommandInput) => unknown }\n\n await client.send(new QueryCommand({\n TableName: table,\n KeyConditionExpression: 'pk = :pk',\n ExpressionAttributeValues: { ':pk': '__ping__' },\n }))\n return true\n } catch {\n return false\n }\n },\n\n /**\n * Paginate over a collection using DynamoDB's native `LastEvaluatedKey`\n * cursor. The cursor is base64-encoded JSON of the LastEvaluatedKey\n * object so it round-trips through any caller transport.\n *\n * Each page is a single Query call against the partition key, so the\n * read cost is `pageSize ÷ 4 KB` RCUs (eventually consistent) per page.\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const client = await getClient()\n const { QueryCommand } = await import('@aws-sdk/lib-dynamodb') as { QueryCommand: new (input: QueryCommandInput) => unknown }\n\n const input: QueryCommandInput = {\n TableName: table,\n KeyConditionExpression: 'pk = :pk AND begins_with(sk, :prefix)',\n ExpressionAttributeValues: {\n ':pk': vault,\n ':prefix': `${collection}#`,\n },\n Limit: limit,\n }\n if (cursor) {\n input.ExclusiveStartKey = JSON.parse(b64decode(cursor)) as Record<string, unknown>\n }\n\n const result = await client.send(new QueryCommand(input)) as {\n Items?: Record<string, unknown>[]\n LastEvaluatedKey?: Record<string, unknown>\n }\n\n const items: Array<{ id: string; envelope: EncryptedEnvelope }> = []\n for (const item of result.Items ?? []) {\n const { id } = parseSk(item['sk'] as string)\n items.push({ id, envelope: itemToEnvelope(item) })\n }\n\n const nextCursor = result.LastEvaluatedKey\n ? b64encode(JSON.stringify(result.LastEvaluatedKey))\n : null\n\n return { items, nextCursor }\n },\n }\n}\n\n/**\n * Tiny base64 helpers that work in both Node 20+ and any modern browser\n * without pulling in @types/node or relying on a Buffer polyfill. The\n * dynamo adapter has zero non-AWS dependencies and we want to keep it\n * that way — listPage cursors are short JSON blobs so the per-call cost\n * of these helpers is negligible.\n */\nfunction b64encode(input: string): string {\n // btoa expects a Latin-1 string; encodeURIComponent + unescape is the\n // canonical trick for utf-8 → btoa-safe payloads.\n return btoa(unescape(encodeURIComponent(input)))\n}\n\nfunction b64decode(input: string): string {\n return decodeURIComponent(escape(atob(input)))\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@noy-db/to-aws-dynamo",
3
+ "version": "0.1.0-pre.3",
4
+ "description": "AWS DynamoDB adapter for noy-db — single-table design, zero-knowledge cloud sync",
5
+ "license": "MIT",
6
+ "author": "vLannaAi <vicio@lanna.ai>",
7
+ "homepage": "https://github.com/vLannaAi/noy-db/tree/main/packages/to-aws-dynamo#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/vLannaAi/noy-db.git",
11
+ "directory": "packages/to-aws-dynamo"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/vLannaAi/noy-db/issues"
15
+ },
16
+ "type": "module",
17
+ "sideEffects": false,
18
+ "exports": {
19
+ ".": {
20
+ "import": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "require": {
25
+ "types": "./dist/index.d.cts",
26
+ "default": "./dist/index.cjs"
27
+ }
28
+ }
29
+ },
30
+ "main": "./dist/index.cjs",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "engines": {
39
+ "node": ">=18.0.0"
40
+ },
41
+ "peerDependencies": {
42
+ "@aws-sdk/client-dynamodb": "^3.0.0",
43
+ "@aws-sdk/lib-dynamodb": "^3.0.0",
44
+ "@noy-db/hub": "0.1.0-pre.3"
45
+ },
46
+ "devDependencies": {
47
+ "@aws-sdk/client-dynamodb": "^3.0.0",
48
+ "@aws-sdk/lib-dynamodb": "^3.0.0",
49
+ "@noy-db/hub": "0.1.0-pre.3"
50
+ },
51
+ "keywords": [
52
+ "noy-db",
53
+ "adapter",
54
+ "dynamodb",
55
+ "aws",
56
+ "single-table",
57
+ "cloud",
58
+ "sync",
59
+ "encryption",
60
+ "zero-knowledge"
61
+ ],
62
+ "publishConfig": {
63
+ "access": "public",
64
+ "tag": "latest"
65
+ },
66
+ "scripts": {
67
+ "build": "tsup",
68
+ "test": "vitest run",
69
+ "lint": "eslint src/",
70
+ "typecheck": "tsc --noEmit"
71
+ }
72
+ }