@noy-db/to-aws-s3 0.2.0-pre.1 → 0.2.0-pre.10

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/dist/index.cjs CHANGED
@@ -20,16 +20,102 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- s3: () => s3
23
+ s3: () => s3,
24
+ s3Bundle: () => s3Bundle
24
25
  });
25
26
  module.exports = __toCommonJS(index_exports);
27
+ var import_hub2 = require("@noy-db/hub");
28
+ var import_client_s32 = require("@aws-sdk/client-s3");
29
+
30
+ // src/bundle.ts
26
31
  var import_hub = require("@noy-db/hub");
27
32
  var import_client_s3 = require("@aws-sdk/client-s3");
28
- function s3(options) {
33
+ var SUFFIX = ".noydb";
34
+ function stripQuotes(etag) {
35
+ return (etag ?? "").replace(/^"|"$/g, "");
36
+ }
37
+ function s3Bundle(options) {
29
38
  const { bucket, prefix = "" } = options;
30
39
  const client = options.client ?? new import_client_s3.S3Client({
31
40
  ...options.region ? { region: options.region } : {}
32
41
  });
42
+ const listPrefix = prefix ? `${prefix}/` : "";
43
+ function objectKey(vaultId) {
44
+ return `${listPrefix}${vaultId}${SUFFIX}`;
45
+ }
46
+ return {
47
+ kind: "bundle",
48
+ name: "s3",
49
+ async readBundle(vaultId) {
50
+ try {
51
+ const res = await client.send(new import_client_s3.GetObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }));
52
+ if (!res.Body) return null;
53
+ const bytes = await res.Body.transformToByteArray();
54
+ return { bytes, version: stripQuotes(res.ETag) };
55
+ } catch (err) {
56
+ if (err instanceof Error && (err.name === "NoSuchKey" || err.name === "NotFound")) return null;
57
+ throw err;
58
+ }
59
+ },
60
+ async writeBundle(vaultId, bytes, expectedVersion) {
61
+ try {
62
+ const res = await client.send(new import_client_s3.PutObjectCommand({
63
+ Bucket: bucket,
64
+ Key: objectKey(vaultId),
65
+ Body: bytes,
66
+ ContentType: "application/octet-stream",
67
+ ...expectedVersion !== null ? { IfMatch: expectedVersion } : {}
68
+ }));
69
+ let version = stripQuotes(res.ETag);
70
+ if (!version) {
71
+ const head = await client.send(new import_client_s3.HeadObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }));
72
+ version = stripQuotes(head.ETag);
73
+ }
74
+ return { version };
75
+ } catch (err) {
76
+ const status = err.$metadata?.httpStatusCode;
77
+ if (err instanceof Error && (err.name === "PreconditionFailed" || status === 412)) {
78
+ throw new import_hub.BundleVersionConflictError(
79
+ `S3 bundle "${vaultId}" changed since expectedVersion="${expectedVersion}".`
80
+ );
81
+ }
82
+ throw err;
83
+ }
84
+ },
85
+ async deleteBundle(vaultId) {
86
+ await client.send(new import_client_s3.DeleteObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }));
87
+ },
88
+ async listBundles() {
89
+ const out = [];
90
+ let token;
91
+ do {
92
+ const res = await client.send(new import_client_s3.ListObjectsV2Command({
93
+ Bucket: bucket,
94
+ Prefix: listPrefix,
95
+ ...token ? { ContinuationToken: token } : {}
96
+ }));
97
+ for (const obj of res.Contents ?? []) {
98
+ const key = obj.Key ?? "";
99
+ if (!key.endsWith(SUFFIX)) continue;
100
+ out.push({
101
+ vaultId: key.slice(listPrefix.length, -SUFFIX.length),
102
+ version: stripQuotes(obj.ETag),
103
+ size: obj.Size ?? 0
104
+ });
105
+ }
106
+ token = res.IsTruncated ? res.NextContinuationToken : void 0;
107
+ } while (token);
108
+ return out;
109
+ }
110
+ };
111
+ }
112
+
113
+ // src/index.ts
114
+ function s3(options) {
115
+ const { bucket, prefix = "" } = options;
116
+ const client = options.client ?? new import_client_s32.S3Client({
117
+ ...options.region ? { region: options.region } : {}
118
+ });
33
119
  function objectKey(vault, collection, id) {
34
120
  const parts = [vault, collection, `${id}.json`];
35
121
  return prefix ? `${prefix}/${parts.join("/")}` : parts.join("/");
@@ -45,7 +131,7 @@ function s3(options) {
45
131
  name: "s3",
46
132
  async get(vault, collection, id) {
47
133
  try {
48
- const result = await client.send(new import_client_s3.GetObjectCommand({
134
+ const result = await client.send(new import_client_s32.GetObjectCommand({
49
135
  Bucket: bucket,
50
136
  Key: objectKey(vault, collection, id)
51
137
  }));
@@ -63,10 +149,10 @@ function s3(options) {
63
149
  if (expectedVersion !== void 0) {
64
150
  const existing = await this.get(vault, collection, id);
65
151
  if (existing && existing._v !== expectedVersion) {
66
- throw new import_hub.ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`);
152
+ throw new import_hub2.ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`);
67
153
  }
68
154
  }
69
- await client.send(new import_client_s3.PutObjectCommand({
155
+ await client.send(new import_client_s32.PutObjectCommand({
70
156
  Bucket: bucket,
71
157
  Key: objectKey(vault, collection, id),
72
158
  Body: JSON.stringify(envelope),
@@ -74,14 +160,14 @@ function s3(options) {
74
160
  }));
75
161
  },
76
162
  async delete(vault, collection, id) {
77
- await client.send(new import_client_s3.DeleteObjectCommand({
163
+ await client.send(new import_client_s32.DeleteObjectCommand({
78
164
  Bucket: bucket,
79
165
  Key: objectKey(vault, collection, id)
80
166
  }));
81
167
  },
82
168
  async list(vault, collection) {
83
169
  const pfx = collPrefix(vault, collection);
84
- const result = await client.send(new import_client_s3.ListObjectsV2Command({
170
+ const result = await client.send(new import_client_s32.ListObjectsV2Command({
85
171
  Bucket: bucket,
86
172
  Prefix: pfx
87
173
  }));
@@ -89,7 +175,7 @@ function s3(options) {
89
175
  },
90
176
  async loadAll(vault) {
91
177
  const pfx = compPrefix(vault);
92
- const listResult = await client.send(new import_client_s3.ListObjectsV2Command({
178
+ const listResult = await client.send(new import_client_s32.ListObjectsV2Command({
93
179
  Bucket: bucket,
94
180
  Prefix: pfx
95
181
  }));
@@ -103,7 +189,7 @@ function s3(options) {
103
189
  const collection = parts[0];
104
190
  const id = parts[1].slice(0, -5);
105
191
  if (collection.startsWith("_")) continue;
106
- const getResult = await client.send(new import_client_s3.GetObjectCommand({
192
+ const getResult = await client.send(new import_client_s32.GetObjectCommand({
107
193
  Bucket: bucket,
108
194
  Key: key
109
195
  }));
@@ -123,7 +209,7 @@ function s3(options) {
123
209
  },
124
210
  async ping() {
125
211
  try {
126
- await client.send(new import_client_s3.HeadBucketCommand({ Bucket: bucket }));
212
+ await client.send(new import_client_s32.HeadBucketCommand({ Bucket: bucket }));
127
213
  return true;
128
214
  } catch {
129
215
  return false;
@@ -143,7 +229,7 @@ function s3(options) {
143
229
  */
144
230
  async listPage(vault, collection, cursor, limit = 100) {
145
231
  const pfx = collPrefix(vault, collection);
146
- const listResult = await client.send(new import_client_s3.ListObjectsV2Command({
232
+ const listResult = await client.send(new import_client_s32.ListObjectsV2Command({
147
233
  Bucket: bucket,
148
234
  Prefix: pfx,
149
235
  MaxKeys: limit,
@@ -152,7 +238,7 @@ function s3(options) {
152
238
  const keys = (listResult.Contents ?? []).map((obj) => obj.Key ?? "").filter((k) => k.endsWith(".json"));
153
239
  const items = await Promise.all(keys.map(async (key) => {
154
240
  const id = key.slice(pfx.length, -5);
155
- const getResult = await client.send(new import_client_s3.GetObjectCommand({
241
+ const getResult = await client.send(new import_client_s32.GetObjectCommand({
156
242
  Bucket: bucket,
157
243
  Key: key
158
244
  }));
@@ -169,6 +255,7 @@ function s3(options) {
169
255
  }
170
256
  // Annotate the CommonJS export names for ESM import in node:
171
257
  0 && (module.exports = {
172
- s3
258
+ s3,
259
+ s3Bundle
173
260
  });
174
261
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/to-aws-s3** — S3 object store for NOYDB.\n *\n * Each record is stored as a JSON object at\n * `{prefix}/{vault}/{collection}/{id}.json`. The `loadAll()` method uses\n * `ListObjectsV2` to enumerate keys then fetches them in parallel.\n *\n * ## When to use\n *\n * - **Blob / attachment storage** — pair with `@noy-db/to-aws-dynamo` via\n * `routeStore({ default: dynamo(...), blobs: s3(...) })` to route\n * encrypted binary chunks to S3.\n * - **Archive tier** — configure `routeStore` age-based tiering so old\n * records migrate to S3 while hot records stay in DynamoDB.\n * - **Large vaults** — S3 has no item size limit, unlike DynamoDB's 400 KB cap.\n *\n * ## Limitations\n *\n * - **`casAtomic: false`** — S3 has no server-side conditional write on\n * arbitrary metadata. Concurrent puts may result in last-write-wins.\n * Use DynamoDB for records that need conflict-safe writes.\n * - **`loadAll()` is O(N) requests** — listing + fetching every object in a\n * vault. Suitable for vaults up to ~10K records; beyond that, prefer\n * DynamoDB for indexed stores and S3 only for append-heavy blob storage.\n *\n * ## IAM minimum permissions\n *\n * ```json\n * { \"Action\": [\"s3:GetObject\", \"s3:PutObject\", \"s3:DeleteObject\",\n * \"s3:ListBucket\"] }\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadBucketCommand,\n} from '@aws-sdk/client-s3'\n\n/**\n * Options for `s3()`.\n *\n * Objects are stored at `{prefix}/{vault}/{collection}/{id}.json`.\n * `loadAll()` uses `ListObjectsV2` over the vault prefix followed by parallel\n * `GetObject` calls — suitable for vaults with up to ~10K records. For larger\n * vaults, use DynamoDB or pair with `routeStore` age-tiering so S3 only\n * holds archived records.\n *\n * Note: S3 does not support atomic CAS (`casAtomic: false`). Last-write-wins\n * on concurrent puts.\n */\nexport interface S3Options {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default: ''. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default: 'us-east-1'. */\n region?: string\n /**\n * Pre-built S3Client from `@aws-sdk/client-s3`. If provided, the adapter\n * uses this client directly and ignores `region`. Useful for apps that want\n * to share a client across adapters or supply custom middleware.\n */\n client?: S3Client\n}\n\n/**\n * Create an S3 adapter.\n * Key scheme: `{prefix}/{vault}/{collection}/{id}.json`\n */\nexport function s3(options: S3Options): NoydbStore {\n const { bucket, prefix = '' } = options\n\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n function objectKey(vault: string, collection: string, id: string): string {\n const parts = [vault, collection, `${id}.json`]\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function collPrefix(vault: string, collection: string): string {\n const parts = [vault, collection, '']\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function compPrefix(vault: string): string {\n return prefix ? `${prefix}/${vault}/` : `${vault}/`\n }\n\n return {\n name: 's3',\n\n async get(vault, collection, id) {\n try {\n const result = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n\n if (!result.Body) return null\n const body = await result.Body.transformToString()\n return JSON.parse(body) as EncryptedEnvelope\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) {\n return null\n }\n throw err\n }\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n if (expectedVersion !== undefined) {\n const existing = await this.get(vault, collection, id)\n if (existing && existing._v !== expectedVersion) {\n throw new ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`)\n }\n }\n\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n Body: JSON.stringify(envelope),\n ContentType: 'application/json',\n }))\n },\n\n async delete(vault, collection, id) {\n await client.send(new DeleteObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n },\n\n async list(vault, collection) {\n const pfx = collPrefix(vault, collection)\n const result = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n return (result.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n .map(k => k.slice(pfx.length, -5))\n },\n\n async loadAll(vault) {\n const pfx = compPrefix(vault)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n const snapshot: VaultSnapshot = {}\n\n for (const obj of listResult.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith('.json')) continue\n\n const relativePath = key.slice(pfx.length)\n const parts = relativePath.split('/')\n if (parts.length !== 2) continue\n\n const collection = parts[0]!\n const id = parts[1]!.slice(0, -5)\n if (collection.startsWith('_')) continue\n\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n\n if (!getResult.Body) continue\n const body = await getResult.Body.transformToString()\n\n if (!snapshot[collection]) snapshot[collection] = {}\n snapshot[collection][id] = JSON.parse(body) as EncryptedEnvelope\n }\n\n return snapshot\n },\n\n async saveAll(vault, data) {\n for (const [collection, records] of Object.entries(data)) {\n for (const [id, envelope] of Object.entries(records)) {\n await this.put(vault, collection, id, envelope)\n }\n }\n },\n\n async ping() {\n try {\n await client.send(new HeadBucketCommand({ Bucket: bucket }))\n return true\n } catch {\n return false\n }\n },\n\n /**\n * Paginate over a collection using S3's native `ContinuationToken`.\n *\n * Each page does:\n * 1. ListObjectsV2 with MaxKeys = limit and the previous token\n * 2. GetObject for every key on the page (in parallel)\n *\n * The 2-step pattern is necessary because S3 list responses don't\n * include object bodies. For very large collections this is N+1 — but\n * the parallel GETs amortize well, and consumers willing to pay for\n * stronger pagination should use a different adapter (Dynamo).\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const pfx = collPrefix(vault, collection)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n MaxKeys: limit,\n ...(cursor ? { ContinuationToken: cursor } : {}),\n }))\n\n const keys = (listResult.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n\n // Fetch every body in parallel — bounded by `limit` so we never\n // fan out beyond the page size.\n const items = await Promise.all(keys.map(async (key) => {\n const id = key.slice(pfx.length, -5)\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n if (!getResult.Body) return null\n const body = await getResult.Body.transformToString()\n return { id, envelope: JSON.parse(body) as EncryptedEnvelope }\n }))\n\n return {\n items: items.filter((x): x is { id: string; envelope: EncryptedEnvelope } => x !== null),\n nextCursor: listResult.IsTruncated && listResult.NextContinuationToken\n ? listResult.NextContinuationToken\n : null,\n }\n },\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/bundle.ts"],"sourcesContent":["/**\n * **@noy-db/to-aws-s3** — S3 object store for NOYDB.\n *\n * Each record is stored as a JSON object at\n * `{prefix}/{vault}/{collection}/{id}.json`. The `loadAll()` method uses\n * `ListObjectsV2` to enumerate keys then fetches them in parallel.\n *\n * ## When to use\n *\n * - **Blob / attachment storage** — pair with `@noy-db/to-aws-dynamo` via\n * `routeStore({ default: dynamo(...), blobs: s3(...) })` to route\n * encrypted binary chunks to S3.\n * - **Archive tier** — configure `routeStore` age-based tiering so old\n * records migrate to S3 while hot records stay in DynamoDB.\n * - **Large vaults** — S3 has no item size limit, unlike DynamoDB's 400 KB cap.\n *\n * ## Limitations\n *\n * - **`casAtomic: false`** — S3 has no server-side conditional write on\n * arbitrary metadata. Concurrent puts may result in last-write-wins.\n * Use DynamoDB for records that need conflict-safe writes.\n * - **`loadAll()` is O(N) requests** — listing + fetching every object in a\n * vault. Suitable for vaults up to ~10K records; beyond that, prefer\n * DynamoDB for indexed stores and S3 only for append-heavy blob storage.\n *\n * ## IAM minimum permissions\n *\n * ```json\n * { \"Action\": [\"s3:GetObject\", \"s3:PutObject\", \"s3:DeleteObject\",\n * \"s3:ListBucket\"] }\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadBucketCommand,\n} from '@aws-sdk/client-s3'\n\n/**\n * Options for `s3()`.\n *\n * Objects are stored at `{prefix}/{vault}/{collection}/{id}.json`.\n * `loadAll()` uses `ListObjectsV2` over the vault prefix followed by parallel\n * `GetObject` calls — suitable for vaults with up to ~10K records. For larger\n * vaults, use DynamoDB or pair with `routeStore` age-tiering so S3 only\n * holds archived records.\n *\n * Note: S3 does not support atomic CAS (`casAtomic: false`). Last-write-wins\n * on concurrent puts.\n */\nexport interface S3Options {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default: ''. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default: 'us-east-1'. */\n region?: string\n /**\n * Pre-built S3Client from `@aws-sdk/client-s3`. If provided, the adapter\n * uses this client directly and ignores `region`. Useful for apps that want\n * to share a client across adapters or supply custom middleware.\n */\n client?: S3Client\n}\n\n/**\n * Create an S3 adapter.\n * Key scheme: `{prefix}/{vault}/{collection}/{id}.json`\n */\nexport function s3(options: S3Options): NoydbStore {\n const { bucket, prefix = '' } = options\n\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n function objectKey(vault: string, collection: string, id: string): string {\n const parts = [vault, collection, `${id}.json`]\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function collPrefix(vault: string, collection: string): string {\n const parts = [vault, collection, '']\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function compPrefix(vault: string): string {\n return prefix ? `${prefix}/${vault}/` : `${vault}/`\n }\n\n return {\n name: 's3',\n\n async get(vault, collection, id) {\n try {\n const result = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n\n if (!result.Body) return null\n const body = await result.Body.transformToString()\n return JSON.parse(body) as EncryptedEnvelope\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) {\n return null\n }\n throw err\n }\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n if (expectedVersion !== undefined) {\n const existing = await this.get(vault, collection, id)\n if (existing && existing._v !== expectedVersion) {\n throw new ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`)\n }\n }\n\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n Body: JSON.stringify(envelope),\n ContentType: 'application/json',\n }))\n },\n\n async delete(vault, collection, id) {\n await client.send(new DeleteObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n },\n\n async list(vault, collection) {\n const pfx = collPrefix(vault, collection)\n const result = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n return (result.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n .map(k => k.slice(pfx.length, -5))\n },\n\n async loadAll(vault) {\n const pfx = compPrefix(vault)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n const snapshot: VaultSnapshot = {}\n\n for (const obj of listResult.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith('.json')) continue\n\n const relativePath = key.slice(pfx.length)\n const parts = relativePath.split('/')\n if (parts.length !== 2) continue\n\n const collection = parts[0]!\n const id = parts[1]!.slice(0, -5)\n if (collection.startsWith('_')) continue\n\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n\n if (!getResult.Body) continue\n const body = await getResult.Body.transformToString()\n\n if (!snapshot[collection]) snapshot[collection] = {}\n snapshot[collection][id] = JSON.parse(body) as EncryptedEnvelope\n }\n\n return snapshot\n },\n\n async saveAll(vault, data) {\n for (const [collection, records] of Object.entries(data)) {\n for (const [id, envelope] of Object.entries(records)) {\n await this.put(vault, collection, id, envelope)\n }\n }\n },\n\n async ping() {\n try {\n await client.send(new HeadBucketCommand({ Bucket: bucket }))\n return true\n } catch {\n return false\n }\n },\n\n /**\n * Paginate over a collection using S3's native `ContinuationToken`.\n *\n * Each page does:\n * 1. ListObjectsV2 with MaxKeys = limit and the previous token\n * 2. GetObject for every key on the page (in parallel)\n *\n * The 2-step pattern is necessary because S3 list responses don't\n * include object bodies. For very large collections this is N+1 — but\n * the parallel GETs amortize well, and consumers willing to pay for\n * stronger pagination should use a different adapter (Dynamo).\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const pfx = collPrefix(vault, collection)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n MaxKeys: limit,\n ...(cursor ? { ContinuationToken: cursor } : {}),\n }))\n\n const keys = (listResult.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n\n // Fetch every body in parallel — bounded by `limit` so we never\n // fan out beyond the page size.\n const items = await Promise.all(keys.map(async (key) => {\n const id = key.slice(pfx.length, -5)\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n if (!getResult.Body) return null\n const body = await getResult.Body.transformToString()\n return { id, envelope: JSON.parse(body) as EncryptedEnvelope }\n }))\n\n return {\n items: items.filter((x): x is { id: string; envelope: EncryptedEnvelope } => x !== null),\n nextCursor: listResult.IsTruncated && listResult.NextContinuationToken\n ? listResult.NextContinuationToken\n : null,\n }\n },\n }\n}\n\nexport { s3Bundle } from './bundle.js'\nexport type { S3BundleOptions } from './bundle.js'\n","/**\n * **s3Bundle** — whole-vault bundle store for noy-db over Amazon S3.\n *\n * Implements the `NoydbBundleStore` contract (read/write/delete/list of whole\n * `.noydb` blobs) with optimistic concurrency via S3 conditional writes. Pairs\n * with `@noy-db/hub` snapshots (`withSnapshots({ store: s3Bundle(...) })`) and\n * with bundle-mode sync.\n *\n * Key scheme: `{prefix}/{vaultId}.noydb`. The version token is the object ETag.\n *\n * **OCC:** `writeBundle(id, bytes, expectedVersion)` —\n * - `expectedVersion === null` → unconditional `PutObject` (first write / rolling overwrite).\n * - `expectedVersion = <etag>` → `PutObject` with `IfMatch`; a 412 becomes `BundleVersionConflictError`.\n *\n * Requires `@aws-sdk/client-s3` ≥ 3.696 (conditional-write `IfMatch` on PutObject, GA Nov 2024).\n *\n * @packageDocumentation\n */\nimport type { NoydbBundleStore } from '@noy-db/hub'\nimport { BundleVersionConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadObjectCommand,\n} from '@aws-sdk/client-s3'\n\nexport interface S3BundleOptions {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default ''. Keys are `{prefix}/{vaultId}.noydb`. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default 'us-east-1'. */\n region?: string\n /** Pre-built S3Client. If provided, `region` is ignored. */\n client?: S3Client\n}\n\nconst SUFFIX = '.noydb'\n\nfunction stripQuotes(etag: string | undefined): string {\n return (etag ?? '').replace(/^\"|\"$/g, '')\n}\n\nexport function s3Bundle(options: S3BundleOptions): NoydbBundleStore {\n const { bucket, prefix = '' } = options\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n const listPrefix = prefix ? `${prefix}/` : ''\n function objectKey(vaultId: string): string {\n return `${listPrefix}${vaultId}${SUFFIX}`\n }\n\n return {\n kind: 'bundle',\n name: 's3',\n\n async readBundle(vaultId) {\n try {\n const res = await client.send(new GetObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n if (!res.Body) return null\n const bytes = await (res.Body as { transformToByteArray: () => Promise<Uint8Array> }).transformToByteArray()\n return { bytes, version: stripQuotes(res.ETag) }\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) return null\n throw err\n }\n },\n\n async writeBundle(vaultId, bytes, expectedVersion) {\n try {\n const res = await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: objectKey(vaultId),\n Body: bytes,\n ContentType: 'application/octet-stream',\n ...(expectedVersion !== null ? { IfMatch: expectedVersion } : {}),\n }))\n let version = stripQuotes(res.ETag)\n if (!version) {\n const head = await client.send(new HeadObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n version = stripQuotes(head.ETag)\n }\n return { version }\n } catch (err: unknown) {\n const status = (err as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode\n if (err instanceof Error && (err.name === 'PreconditionFailed' || status === 412)) {\n throw new BundleVersionConflictError(\n `S3 bundle \"${vaultId}\" changed since expectedVersion=\"${expectedVersion}\".`,\n )\n }\n throw err\n }\n },\n\n async deleteBundle(vaultId) {\n await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n },\n\n async listBundles() {\n const out: Array<{ vaultId: string; version: string; size: number }> = []\n let token: string | undefined\n do {\n const res = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: listPrefix,\n ...(token ? { ContinuationToken: token } : {}),\n }))\n for (const obj of res.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith(SUFFIX)) continue\n out.push({\n vaultId: key.slice(listPrefix.length, -SUFFIX.length),\n version: stripQuotes(obj.ETag),\n size: obj.Size ?? 0,\n })\n }\n token = res.IsTruncated ? res.NextContinuationToken : undefined\n } while (token)\n return out\n },\n }\n}\n"],"mappings":"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","names":["import_hub","import_client_s3"]}
package/dist/index.d.cts CHANGED
@@ -1,6 +1,37 @@
1
- import { NoydbStore } from '@noy-db/hub';
1
+ import { NoydbBundleStore, NoydbStore } from '@noy-db/hub';
2
2
  import { S3Client } from '@aws-sdk/client-s3';
3
3
 
4
+ /**
5
+ * **s3Bundle** — whole-vault bundle store for noy-db over Amazon S3.
6
+ *
7
+ * Implements the `NoydbBundleStore` contract (read/write/delete/list of whole
8
+ * `.noydb` blobs) with optimistic concurrency via S3 conditional writes. Pairs
9
+ * with `@noy-db/hub` snapshots (`withSnapshots({ store: s3Bundle(...) })`) and
10
+ * with bundle-mode sync.
11
+ *
12
+ * Key scheme: `{prefix}/{vaultId}.noydb`. The version token is the object ETag.
13
+ *
14
+ * **OCC:** `writeBundle(id, bytes, expectedVersion)` —
15
+ * - `expectedVersion === null` → unconditional `PutObject` (first write / rolling overwrite).
16
+ * - `expectedVersion = <etag>` → `PutObject` with `IfMatch`; a 412 becomes `BundleVersionConflictError`.
17
+ *
18
+ * Requires `@aws-sdk/client-s3` ≥ 3.696 (conditional-write `IfMatch` on PutObject, GA Nov 2024).
19
+ *
20
+ * @packageDocumentation
21
+ */
22
+
23
+ interface S3BundleOptions {
24
+ /** S3 bucket name. */
25
+ bucket: string;
26
+ /** Key prefix within the bucket. Default ''. Keys are `{prefix}/{vaultId}.noydb`. */
27
+ prefix?: string;
28
+ /** AWS region. Used only when `client` is not provided. Default 'us-east-1'. */
29
+ region?: string;
30
+ /** Pre-built S3Client. If provided, `region` is ignored. */
31
+ client?: S3Client;
32
+ }
33
+ declare function s3Bundle(options: S3BundleOptions): NoydbBundleStore;
34
+
4
35
  /**
5
36
  * **@noy-db/to-aws-s3** — S3 object store for NOYDB.
6
37
  *
@@ -68,4 +99,4 @@ interface S3Options {
68
99
  */
69
100
  declare function s3(options: S3Options): NoydbStore;
70
101
 
71
- export { type S3Options, s3 };
102
+ export { type S3BundleOptions, type S3Options, s3, s3Bundle };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,37 @@
1
- import { NoydbStore } from '@noy-db/hub';
1
+ import { NoydbBundleStore, NoydbStore } from '@noy-db/hub';
2
2
  import { S3Client } from '@aws-sdk/client-s3';
3
3
 
4
+ /**
5
+ * **s3Bundle** — whole-vault bundle store for noy-db over Amazon S3.
6
+ *
7
+ * Implements the `NoydbBundleStore` contract (read/write/delete/list of whole
8
+ * `.noydb` blobs) with optimistic concurrency via S3 conditional writes. Pairs
9
+ * with `@noy-db/hub` snapshots (`withSnapshots({ store: s3Bundle(...) })`) and
10
+ * with bundle-mode sync.
11
+ *
12
+ * Key scheme: `{prefix}/{vaultId}.noydb`. The version token is the object ETag.
13
+ *
14
+ * **OCC:** `writeBundle(id, bytes, expectedVersion)` —
15
+ * - `expectedVersion === null` → unconditional `PutObject` (first write / rolling overwrite).
16
+ * - `expectedVersion = <etag>` → `PutObject` with `IfMatch`; a 412 becomes `BundleVersionConflictError`.
17
+ *
18
+ * Requires `@aws-sdk/client-s3` ≥ 3.696 (conditional-write `IfMatch` on PutObject, GA Nov 2024).
19
+ *
20
+ * @packageDocumentation
21
+ */
22
+
23
+ interface S3BundleOptions {
24
+ /** S3 bucket name. */
25
+ bucket: string;
26
+ /** Key prefix within the bucket. Default ''. Keys are `{prefix}/{vaultId}.noydb`. */
27
+ prefix?: string;
28
+ /** AWS region. Used only when `client` is not provided. Default 'us-east-1'. */
29
+ region?: string;
30
+ /** Pre-built S3Client. If provided, `region` is ignored. */
31
+ client?: S3Client;
32
+ }
33
+ declare function s3Bundle(options: S3BundleOptions): NoydbBundleStore;
34
+
4
35
  /**
5
36
  * **@noy-db/to-aws-s3** — S3 object store for NOYDB.
6
37
  *
@@ -68,4 +99,4 @@ interface S3Options {
68
99
  */
69
100
  declare function s3(options: S3Options): NoydbStore;
70
101
 
71
- export { type S3Options, s3 };
102
+ export { type S3BundleOptions, type S3Options, s3, s3Bundle };
package/dist/index.js CHANGED
@@ -1,18 +1,110 @@
1
1
  // src/index.ts
2
2
  import { ConflictError } from "@noy-db/hub";
3
+ import {
4
+ S3Client as S3Client2,
5
+ GetObjectCommand as GetObjectCommand2,
6
+ PutObjectCommand as PutObjectCommand2,
7
+ DeleteObjectCommand as DeleteObjectCommand2,
8
+ ListObjectsV2Command as ListObjectsV2Command2,
9
+ HeadBucketCommand
10
+ } from "@aws-sdk/client-s3";
11
+
12
+ // src/bundle.ts
13
+ import { BundleVersionConflictError } from "@noy-db/hub";
3
14
  import {
4
15
  S3Client,
5
16
  GetObjectCommand,
6
17
  PutObjectCommand,
7
18
  DeleteObjectCommand,
8
19
  ListObjectsV2Command,
9
- HeadBucketCommand
20
+ HeadObjectCommand
10
21
  } from "@aws-sdk/client-s3";
11
- function s3(options) {
22
+ var SUFFIX = ".noydb";
23
+ function stripQuotes(etag) {
24
+ return (etag ?? "").replace(/^"|"$/g, "");
25
+ }
26
+ function s3Bundle(options) {
12
27
  const { bucket, prefix = "" } = options;
13
28
  const client = options.client ?? new S3Client({
14
29
  ...options.region ? { region: options.region } : {}
15
30
  });
31
+ const listPrefix = prefix ? `${prefix}/` : "";
32
+ function objectKey(vaultId) {
33
+ return `${listPrefix}${vaultId}${SUFFIX}`;
34
+ }
35
+ return {
36
+ kind: "bundle",
37
+ name: "s3",
38
+ async readBundle(vaultId) {
39
+ try {
40
+ const res = await client.send(new GetObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }));
41
+ if (!res.Body) return null;
42
+ const bytes = await res.Body.transformToByteArray();
43
+ return { bytes, version: stripQuotes(res.ETag) };
44
+ } catch (err) {
45
+ if (err instanceof Error && (err.name === "NoSuchKey" || err.name === "NotFound")) return null;
46
+ throw err;
47
+ }
48
+ },
49
+ async writeBundle(vaultId, bytes, expectedVersion) {
50
+ try {
51
+ const res = await client.send(new PutObjectCommand({
52
+ Bucket: bucket,
53
+ Key: objectKey(vaultId),
54
+ Body: bytes,
55
+ ContentType: "application/octet-stream",
56
+ ...expectedVersion !== null ? { IfMatch: expectedVersion } : {}
57
+ }));
58
+ let version = stripQuotes(res.ETag);
59
+ if (!version) {
60
+ const head = await client.send(new HeadObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }));
61
+ version = stripQuotes(head.ETag);
62
+ }
63
+ return { version };
64
+ } catch (err) {
65
+ const status = err.$metadata?.httpStatusCode;
66
+ if (err instanceof Error && (err.name === "PreconditionFailed" || status === 412)) {
67
+ throw new BundleVersionConflictError(
68
+ `S3 bundle "${vaultId}" changed since expectedVersion="${expectedVersion}".`
69
+ );
70
+ }
71
+ throw err;
72
+ }
73
+ },
74
+ async deleteBundle(vaultId) {
75
+ await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }));
76
+ },
77
+ async listBundles() {
78
+ const out = [];
79
+ let token;
80
+ do {
81
+ const res = await client.send(new ListObjectsV2Command({
82
+ Bucket: bucket,
83
+ Prefix: listPrefix,
84
+ ...token ? { ContinuationToken: token } : {}
85
+ }));
86
+ for (const obj of res.Contents ?? []) {
87
+ const key = obj.Key ?? "";
88
+ if (!key.endsWith(SUFFIX)) continue;
89
+ out.push({
90
+ vaultId: key.slice(listPrefix.length, -SUFFIX.length),
91
+ version: stripQuotes(obj.ETag),
92
+ size: obj.Size ?? 0
93
+ });
94
+ }
95
+ token = res.IsTruncated ? res.NextContinuationToken : void 0;
96
+ } while (token);
97
+ return out;
98
+ }
99
+ };
100
+ }
101
+
102
+ // src/index.ts
103
+ function s3(options) {
104
+ const { bucket, prefix = "" } = options;
105
+ const client = options.client ?? new S3Client2({
106
+ ...options.region ? { region: options.region } : {}
107
+ });
16
108
  function objectKey(vault, collection, id) {
17
109
  const parts = [vault, collection, `${id}.json`];
18
110
  return prefix ? `${prefix}/${parts.join("/")}` : parts.join("/");
@@ -28,7 +120,7 @@ function s3(options) {
28
120
  name: "s3",
29
121
  async get(vault, collection, id) {
30
122
  try {
31
- const result = await client.send(new GetObjectCommand({
123
+ const result = await client.send(new GetObjectCommand2({
32
124
  Bucket: bucket,
33
125
  Key: objectKey(vault, collection, id)
34
126
  }));
@@ -49,7 +141,7 @@ function s3(options) {
49
141
  throw new ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`);
50
142
  }
51
143
  }
52
- await client.send(new PutObjectCommand({
144
+ await client.send(new PutObjectCommand2({
53
145
  Bucket: bucket,
54
146
  Key: objectKey(vault, collection, id),
55
147
  Body: JSON.stringify(envelope),
@@ -57,14 +149,14 @@ function s3(options) {
57
149
  }));
58
150
  },
59
151
  async delete(vault, collection, id) {
60
- await client.send(new DeleteObjectCommand({
152
+ await client.send(new DeleteObjectCommand2({
61
153
  Bucket: bucket,
62
154
  Key: objectKey(vault, collection, id)
63
155
  }));
64
156
  },
65
157
  async list(vault, collection) {
66
158
  const pfx = collPrefix(vault, collection);
67
- const result = await client.send(new ListObjectsV2Command({
159
+ const result = await client.send(new ListObjectsV2Command2({
68
160
  Bucket: bucket,
69
161
  Prefix: pfx
70
162
  }));
@@ -72,7 +164,7 @@ function s3(options) {
72
164
  },
73
165
  async loadAll(vault) {
74
166
  const pfx = compPrefix(vault);
75
- const listResult = await client.send(new ListObjectsV2Command({
167
+ const listResult = await client.send(new ListObjectsV2Command2({
76
168
  Bucket: bucket,
77
169
  Prefix: pfx
78
170
  }));
@@ -86,7 +178,7 @@ function s3(options) {
86
178
  const collection = parts[0];
87
179
  const id = parts[1].slice(0, -5);
88
180
  if (collection.startsWith("_")) continue;
89
- const getResult = await client.send(new GetObjectCommand({
181
+ const getResult = await client.send(new GetObjectCommand2({
90
182
  Bucket: bucket,
91
183
  Key: key
92
184
  }));
@@ -126,7 +218,7 @@ function s3(options) {
126
218
  */
127
219
  async listPage(vault, collection, cursor, limit = 100) {
128
220
  const pfx = collPrefix(vault, collection);
129
- const listResult = await client.send(new ListObjectsV2Command({
221
+ const listResult = await client.send(new ListObjectsV2Command2({
130
222
  Bucket: bucket,
131
223
  Prefix: pfx,
132
224
  MaxKeys: limit,
@@ -135,7 +227,7 @@ function s3(options) {
135
227
  const keys = (listResult.Contents ?? []).map((obj) => obj.Key ?? "").filter((k) => k.endsWith(".json"));
136
228
  const items = await Promise.all(keys.map(async (key) => {
137
229
  const id = key.slice(pfx.length, -5);
138
- const getResult = await client.send(new GetObjectCommand({
230
+ const getResult = await client.send(new GetObjectCommand2({
139
231
  Bucket: bucket,
140
232
  Key: key
141
233
  }));
@@ -151,6 +243,7 @@ function s3(options) {
151
243
  };
152
244
  }
153
245
  export {
154
- s3
246
+ s3,
247
+ s3Bundle
155
248
  };
156
249
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/to-aws-s3** — S3 object store for NOYDB.\n *\n * Each record is stored as a JSON object at\n * `{prefix}/{vault}/{collection}/{id}.json`. The `loadAll()` method uses\n * `ListObjectsV2` to enumerate keys then fetches them in parallel.\n *\n * ## When to use\n *\n * - **Blob / attachment storage** — pair with `@noy-db/to-aws-dynamo` via\n * `routeStore({ default: dynamo(...), blobs: s3(...) })` to route\n * encrypted binary chunks to S3.\n * - **Archive tier** — configure `routeStore` age-based tiering so old\n * records migrate to S3 while hot records stay in DynamoDB.\n * - **Large vaults** — S3 has no item size limit, unlike DynamoDB's 400 KB cap.\n *\n * ## Limitations\n *\n * - **`casAtomic: false`** — S3 has no server-side conditional write on\n * arbitrary metadata. Concurrent puts may result in last-write-wins.\n * Use DynamoDB for records that need conflict-safe writes.\n * - **`loadAll()` is O(N) requests** — listing + fetching every object in a\n * vault. Suitable for vaults up to ~10K records; beyond that, prefer\n * DynamoDB for indexed stores and S3 only for append-heavy blob storage.\n *\n * ## IAM minimum permissions\n *\n * ```json\n * { \"Action\": [\"s3:GetObject\", \"s3:PutObject\", \"s3:DeleteObject\",\n * \"s3:ListBucket\"] }\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadBucketCommand,\n} from '@aws-sdk/client-s3'\n\n/**\n * Options for `s3()`.\n *\n * Objects are stored at `{prefix}/{vault}/{collection}/{id}.json`.\n * `loadAll()` uses `ListObjectsV2` over the vault prefix followed by parallel\n * `GetObject` calls — suitable for vaults with up to ~10K records. For larger\n * vaults, use DynamoDB or pair with `routeStore` age-tiering so S3 only\n * holds archived records.\n *\n * Note: S3 does not support atomic CAS (`casAtomic: false`). Last-write-wins\n * on concurrent puts.\n */\nexport interface S3Options {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default: ''. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default: 'us-east-1'. */\n region?: string\n /**\n * Pre-built S3Client from `@aws-sdk/client-s3`. If provided, the adapter\n * uses this client directly and ignores `region`. Useful for apps that want\n * to share a client across adapters or supply custom middleware.\n */\n client?: S3Client\n}\n\n/**\n * Create an S3 adapter.\n * Key scheme: `{prefix}/{vault}/{collection}/{id}.json`\n */\nexport function s3(options: S3Options): NoydbStore {\n const { bucket, prefix = '' } = options\n\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n function objectKey(vault: string, collection: string, id: string): string {\n const parts = [vault, collection, `${id}.json`]\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function collPrefix(vault: string, collection: string): string {\n const parts = [vault, collection, '']\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function compPrefix(vault: string): string {\n return prefix ? `${prefix}/${vault}/` : `${vault}/`\n }\n\n return {\n name: 's3',\n\n async get(vault, collection, id) {\n try {\n const result = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n\n if (!result.Body) return null\n const body = await result.Body.transformToString()\n return JSON.parse(body) as EncryptedEnvelope\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) {\n return null\n }\n throw err\n }\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n if (expectedVersion !== undefined) {\n const existing = await this.get(vault, collection, id)\n if (existing && existing._v !== expectedVersion) {\n throw new ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`)\n }\n }\n\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n Body: JSON.stringify(envelope),\n ContentType: 'application/json',\n }))\n },\n\n async delete(vault, collection, id) {\n await client.send(new DeleteObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n },\n\n async list(vault, collection) {\n const pfx = collPrefix(vault, collection)\n const result = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n return (result.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n .map(k => k.slice(pfx.length, -5))\n },\n\n async loadAll(vault) {\n const pfx = compPrefix(vault)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n const snapshot: VaultSnapshot = {}\n\n for (const obj of listResult.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith('.json')) continue\n\n const relativePath = key.slice(pfx.length)\n const parts = relativePath.split('/')\n if (parts.length !== 2) continue\n\n const collection = parts[0]!\n const id = parts[1]!.slice(0, -5)\n if (collection.startsWith('_')) continue\n\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n\n if (!getResult.Body) continue\n const body = await getResult.Body.transformToString()\n\n if (!snapshot[collection]) snapshot[collection] = {}\n snapshot[collection][id] = JSON.parse(body) as EncryptedEnvelope\n }\n\n return snapshot\n },\n\n async saveAll(vault, data) {\n for (const [collection, records] of Object.entries(data)) {\n for (const [id, envelope] of Object.entries(records)) {\n await this.put(vault, collection, id, envelope)\n }\n }\n },\n\n async ping() {\n try {\n await client.send(new HeadBucketCommand({ Bucket: bucket }))\n return true\n } catch {\n return false\n }\n },\n\n /**\n * Paginate over a collection using S3's native `ContinuationToken`.\n *\n * Each page does:\n * 1. ListObjectsV2 with MaxKeys = limit and the previous token\n * 2. GetObject for every key on the page (in parallel)\n *\n * The 2-step pattern is necessary because S3 list responses don't\n * include object bodies. For very large collections this is N+1 — but\n * the parallel GETs amortize well, and consumers willing to pay for\n * stronger pagination should use a different adapter (Dynamo).\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const pfx = collPrefix(vault, collection)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n MaxKeys: limit,\n ...(cursor ? { ContinuationToken: cursor } : {}),\n }))\n\n const keys = (listResult.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n\n // Fetch every body in parallel — bounded by `limit` so we never\n // fan out beyond the page size.\n const items = await Promise.all(keys.map(async (key) => {\n const id = key.slice(pfx.length, -5)\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n if (!getResult.Body) return null\n const body = await getResult.Body.transformToString()\n return { id, envelope: JSON.parse(body) as EncryptedEnvelope }\n }))\n\n return {\n items: items.filter((x): x is { id: string; envelope: EncryptedEnvelope } => x !== null),\n nextCursor: listResult.IsTruncated && listResult.NextContinuationToken\n ? listResult.NextContinuationToken\n : null,\n }\n },\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/bundle.ts"],"sourcesContent":["/**\n * **@noy-db/to-aws-s3** — S3 object store for NOYDB.\n *\n * Each record is stored as a JSON object at\n * `{prefix}/{vault}/{collection}/{id}.json`. The `loadAll()` method uses\n * `ListObjectsV2` to enumerate keys then fetches them in parallel.\n *\n * ## When to use\n *\n * - **Blob / attachment storage** — pair with `@noy-db/to-aws-dynamo` via\n * `routeStore({ default: dynamo(...), blobs: s3(...) })` to route\n * encrypted binary chunks to S3.\n * - **Archive tier** — configure `routeStore` age-based tiering so old\n * records migrate to S3 while hot records stay in DynamoDB.\n * - **Large vaults** — S3 has no item size limit, unlike DynamoDB's 400 KB cap.\n *\n * ## Limitations\n *\n * - **`casAtomic: false`** — S3 has no server-side conditional write on\n * arbitrary metadata. Concurrent puts may result in last-write-wins.\n * Use DynamoDB for records that need conflict-safe writes.\n * - **`loadAll()` is O(N) requests** — listing + fetching every object in a\n * vault. Suitable for vaults up to ~10K records; beyond that, prefer\n * DynamoDB for indexed stores and S3 only for append-heavy blob storage.\n *\n * ## IAM minimum permissions\n *\n * ```json\n * { \"Action\": [\"s3:GetObject\", \"s3:PutObject\", \"s3:DeleteObject\",\n * \"s3:ListBucket\"] }\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadBucketCommand,\n} from '@aws-sdk/client-s3'\n\n/**\n * Options for `s3()`.\n *\n * Objects are stored at `{prefix}/{vault}/{collection}/{id}.json`.\n * `loadAll()` uses `ListObjectsV2` over the vault prefix followed by parallel\n * `GetObject` calls — suitable for vaults with up to ~10K records. For larger\n * vaults, use DynamoDB or pair with `routeStore` age-tiering so S3 only\n * holds archived records.\n *\n * Note: S3 does not support atomic CAS (`casAtomic: false`). Last-write-wins\n * on concurrent puts.\n */\nexport interface S3Options {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default: ''. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default: 'us-east-1'. */\n region?: string\n /**\n * Pre-built S3Client from `@aws-sdk/client-s3`. If provided, the adapter\n * uses this client directly and ignores `region`. Useful for apps that want\n * to share a client across adapters or supply custom middleware.\n */\n client?: S3Client\n}\n\n/**\n * Create an S3 adapter.\n * Key scheme: `{prefix}/{vault}/{collection}/{id}.json`\n */\nexport function s3(options: S3Options): NoydbStore {\n const { bucket, prefix = '' } = options\n\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n function objectKey(vault: string, collection: string, id: string): string {\n const parts = [vault, collection, `${id}.json`]\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function collPrefix(vault: string, collection: string): string {\n const parts = [vault, collection, '']\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function compPrefix(vault: string): string {\n return prefix ? `${prefix}/${vault}/` : `${vault}/`\n }\n\n return {\n name: 's3',\n\n async get(vault, collection, id) {\n try {\n const result = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n\n if (!result.Body) return null\n const body = await result.Body.transformToString()\n return JSON.parse(body) as EncryptedEnvelope\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) {\n return null\n }\n throw err\n }\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n if (expectedVersion !== undefined) {\n const existing = await this.get(vault, collection, id)\n if (existing && existing._v !== expectedVersion) {\n throw new ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`)\n }\n }\n\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n Body: JSON.stringify(envelope),\n ContentType: 'application/json',\n }))\n },\n\n async delete(vault, collection, id) {\n await client.send(new DeleteObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n },\n\n async list(vault, collection) {\n const pfx = collPrefix(vault, collection)\n const result = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n return (result.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n .map(k => k.slice(pfx.length, -5))\n },\n\n async loadAll(vault) {\n const pfx = compPrefix(vault)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n const snapshot: VaultSnapshot = {}\n\n for (const obj of listResult.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith('.json')) continue\n\n const relativePath = key.slice(pfx.length)\n const parts = relativePath.split('/')\n if (parts.length !== 2) continue\n\n const collection = parts[0]!\n const id = parts[1]!.slice(0, -5)\n if (collection.startsWith('_')) continue\n\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n\n if (!getResult.Body) continue\n const body = await getResult.Body.transformToString()\n\n if (!snapshot[collection]) snapshot[collection] = {}\n snapshot[collection][id] = JSON.parse(body) as EncryptedEnvelope\n }\n\n return snapshot\n },\n\n async saveAll(vault, data) {\n for (const [collection, records] of Object.entries(data)) {\n for (const [id, envelope] of Object.entries(records)) {\n await this.put(vault, collection, id, envelope)\n }\n }\n },\n\n async ping() {\n try {\n await client.send(new HeadBucketCommand({ Bucket: bucket }))\n return true\n } catch {\n return false\n }\n },\n\n /**\n * Paginate over a collection using S3's native `ContinuationToken`.\n *\n * Each page does:\n * 1. ListObjectsV2 with MaxKeys = limit and the previous token\n * 2. GetObject for every key on the page (in parallel)\n *\n * The 2-step pattern is necessary because S3 list responses don't\n * include object bodies. For very large collections this is N+1 — but\n * the parallel GETs amortize well, and consumers willing to pay for\n * stronger pagination should use a different adapter (Dynamo).\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const pfx = collPrefix(vault, collection)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n MaxKeys: limit,\n ...(cursor ? { ContinuationToken: cursor } : {}),\n }))\n\n const keys = (listResult.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n\n // Fetch every body in parallel — bounded by `limit` so we never\n // fan out beyond the page size.\n const items = await Promise.all(keys.map(async (key) => {\n const id = key.slice(pfx.length, -5)\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n if (!getResult.Body) return null\n const body = await getResult.Body.transformToString()\n return { id, envelope: JSON.parse(body) as EncryptedEnvelope }\n }))\n\n return {\n items: items.filter((x): x is { id: string; envelope: EncryptedEnvelope } => x !== null),\n nextCursor: listResult.IsTruncated && listResult.NextContinuationToken\n ? listResult.NextContinuationToken\n : null,\n }\n },\n }\n}\n\nexport { s3Bundle } from './bundle.js'\nexport type { S3BundleOptions } from './bundle.js'\n","/**\n * **s3Bundle** — whole-vault bundle store for noy-db over Amazon S3.\n *\n * Implements the `NoydbBundleStore` contract (read/write/delete/list of whole\n * `.noydb` blobs) with optimistic concurrency via S3 conditional writes. Pairs\n * with `@noy-db/hub` snapshots (`withSnapshots({ store: s3Bundle(...) })`) and\n * with bundle-mode sync.\n *\n * Key scheme: `{prefix}/{vaultId}.noydb`. The version token is the object ETag.\n *\n * **OCC:** `writeBundle(id, bytes, expectedVersion)` —\n * - `expectedVersion === null` → unconditional `PutObject` (first write / rolling overwrite).\n * - `expectedVersion = <etag>` → `PutObject` with `IfMatch`; a 412 becomes `BundleVersionConflictError`.\n *\n * Requires `@aws-sdk/client-s3` ≥ 3.696 (conditional-write `IfMatch` on PutObject, GA Nov 2024).\n *\n * @packageDocumentation\n */\nimport type { NoydbBundleStore } from '@noy-db/hub'\nimport { BundleVersionConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadObjectCommand,\n} from '@aws-sdk/client-s3'\n\nexport interface S3BundleOptions {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default ''. Keys are `{prefix}/{vaultId}.noydb`. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default 'us-east-1'. */\n region?: string\n /** Pre-built S3Client. If provided, `region` is ignored. */\n client?: S3Client\n}\n\nconst SUFFIX = '.noydb'\n\nfunction stripQuotes(etag: string | undefined): string {\n return (etag ?? '').replace(/^\"|\"$/g, '')\n}\n\nexport function s3Bundle(options: S3BundleOptions): NoydbBundleStore {\n const { bucket, prefix = '' } = options\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n const listPrefix = prefix ? `${prefix}/` : ''\n function objectKey(vaultId: string): string {\n return `${listPrefix}${vaultId}${SUFFIX}`\n }\n\n return {\n kind: 'bundle',\n name: 's3',\n\n async readBundle(vaultId) {\n try {\n const res = await client.send(new GetObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n if (!res.Body) return null\n const bytes = await (res.Body as { transformToByteArray: () => Promise<Uint8Array> }).transformToByteArray()\n return { bytes, version: stripQuotes(res.ETag) }\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) return null\n throw err\n }\n },\n\n async writeBundle(vaultId, bytes, expectedVersion) {\n try {\n const res = await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: objectKey(vaultId),\n Body: bytes,\n ContentType: 'application/octet-stream',\n ...(expectedVersion !== null ? { IfMatch: expectedVersion } : {}),\n }))\n let version = stripQuotes(res.ETag)\n if (!version) {\n const head = await client.send(new HeadObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n version = stripQuotes(head.ETag)\n }\n return { version }\n } catch (err: unknown) {\n const status = (err as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode\n if (err instanceof Error && (err.name === 'PreconditionFailed' || status === 412)) {\n throw new BundleVersionConflictError(\n `S3 bundle \"${vaultId}\" changed since expectedVersion=\"${expectedVersion}\".`,\n )\n }\n throw err\n }\n },\n\n async deleteBundle(vaultId) {\n await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n },\n\n async listBundles() {\n const out: Array<{ vaultId: string; version: string; size: number }> = []\n let token: string | undefined\n do {\n const res = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: listPrefix,\n ...(token ? { ContinuationToken: token } : {}),\n }))\n for (const obj of res.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith(SUFFIX)) continue\n out.push({\n vaultId: key.slice(listPrefix.length, -SUFFIX.length),\n version: stripQuotes(obj.ETag),\n size: obj.Size ?? 0,\n })\n }\n token = res.IsTruncated ? res.NextContinuationToken : undefined\n } while (token)\n return out\n },\n }\n}\n"],"mappings":"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","names":["S3Client","GetObjectCommand","PutObjectCommand","DeleteObjectCommand","ListObjectsV2Command","S3Client","GetObjectCommand","PutObjectCommand","DeleteObjectCommand","ListObjectsV2Command"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noy-db/to-aws-s3",
3
- "version": "0.2.0-pre.1",
3
+ "version": "0.2.0-pre.10",
4
4
  "description": "AWS S3 adapter for noy-db — encrypted object storage with zero-knowledge cloud sync",
5
5
  "license": "MIT",
6
6
  "author": "vLannaAi <vicio@lanna.ai>",
@@ -40,11 +40,11 @@
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@aws-sdk/client-s3": "^3.0.0",
43
- "@noy-db/hub": "0.2.0-pre.1"
43
+ "@noy-db/hub": "0.2.0-pre.10"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@aws-sdk/client-s3": "^3.0.0",
47
- "@noy-db/hub": "0.2.0-pre.1"
47
+ "@noy-db/hub": "0.2.0-pre.10"
48
48
  },
49
49
  "keywords": [
50
50
  "noy-db",