@nexcraft-dev/r2d1 0.0.0-stage → 1.0.0

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.
Files changed (48) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +169 -2
  3. package/dist/core/codec.d.ts +7 -0
  4. package/dist/core/codec.js +16 -0
  5. package/dist/core/codec.js.map +1 -0
  6. package/dist/core/collection.d.ts +32 -0
  7. package/dist/core/collection.js +193 -0
  8. package/dist/core/collection.js.map +1 -0
  9. package/dist/core/cursor.d.ts +5 -0
  10. package/dist/core/cursor.js +139 -0
  11. package/dist/core/cursor.js.map +1 -0
  12. package/dist/core/errors.d.ts +27 -0
  13. package/dist/core/errors.js +42 -0
  14. package/dist/core/errors.js.map +1 -0
  15. package/dist/core/model.d.ts +91 -0
  16. package/dist/core/model.js +54 -0
  17. package/dist/core/model.js.map +1 -0
  18. package/dist/core/query.d.ts +29 -0
  19. package/dist/core/query.js +65 -0
  20. package/dist/core/query.js.map +1 -0
  21. package/dist/core/schema.d.ts +21 -0
  22. package/dist/core/schema.js +61 -0
  23. package/dist/core/schema.js.map +1 -0
  24. package/dist/index.d.ts +12 -0
  25. package/dist/index.js +8 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/node/d1.d.ts +10 -0
  28. package/dist/node/d1.js +57 -0
  29. package/dist/node/d1.js.map +1 -0
  30. package/dist/node/index.d.ts +4 -0
  31. package/dist/node/index.js +3 -0
  32. package/dist/node/index.js.map +1 -0
  33. package/dist/node/r2.d.ts +23 -0
  34. package/dist/node/r2.js +115 -0
  35. package/dist/node/r2.js.map +1 -0
  36. package/dist/storage/d1.d.ts +28 -0
  37. package/dist/storage/d1.js +301 -0
  38. package/dist/storage/d1.js.map +1 -0
  39. package/dist/storage/r2-key.d.ts +4 -0
  40. package/dist/storage/r2-key.js +36 -0
  41. package/dist/storage/r2-key.js.map +1 -0
  42. package/dist/workers/index.d.ts +47 -0
  43. package/dist/workers/index.js +93 -0
  44. package/dist/workers/index.js.map +1 -0
  45. package/examples/common.ts +54 -0
  46. package/examples/node.ts +36 -0
  47. package/examples/workers.ts +22 -0
  48. package/package.json +61 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection.js","sourceRoot":"","sources":["../../src/core/collection.ts"],"names":[],"mappings":"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","sourcesContent":["import { encodeCursor } from './cursor.js';\nimport {\n AdmissionRejectedError,\n DocumentNotFoundError,\n InconsistentStateError,\n PartialFailureError,\n PersistenceError,\n} from './errors.js';\nimport { key, positive, StoredDocument } from './model.js';\nimport type {\n DocumentKey,\n DocumentStore,\n IndexEntry,\n IndexQuery,\n IndexStore,\n Page,\n} from './model.js';\nimport type { DocumentCodec } from './codec.js';\nimport type { CollectionSchema } from './schema.js';\nimport { Query } from './query.js';\n\nexport interface CollectionOptions {\n readonly hydrationConcurrency?: number;\n readonly maxConcurrentOperations?: number;\n}\n\nasync function mapBounded<A, B>(\n items: readonly A[],\n concurrency: number,\n map: (value: A) => Promise<B>,\n): Promise<B[]> {\n const output = new Array<B>(items.length);\n let next = 0;\n let failed = false;\n let failure: unknown;\n await Promise.all(\n Array.from({ length: Math.min(items.length, concurrency) }, async () => {\n while (!failed && next < items.length) {\n const index = next++;\n try {\n output[index] = await map(items[index]!);\n } catch (cause) {\n if (!failed) {\n failed = true;\n failure = cause;\n }\n }\n }\n }),\n );\n // Drain in-flight reads before releasing the admission slot.\n if (failed) throw failure;\n return output;\n}\n\nexport class R2D1Collection<T extends object> {\n readonly #concurrency: number;\n readonly #maxOperations: number;\n #active = 0;\n constructor(\n readonly schema: CollectionSchema<T>,\n private readonly documents: DocumentStore,\n private readonly indexes: IndexStore,\n private readonly codec: DocumentCodec<T>,\n options: CollectionOptions = {},\n ) {\n this.#concurrency = positive(options.hydrationConcurrency ?? 16, 'hydrationConcurrency');\n this.#maxOperations =\n options.maxConcurrentOperations === undefined\n ? Infinity\n : positive(options.maxConcurrentOperations, 'maxConcurrentOperations');\n }\n async put(document: T): Promise<void> {\n return this.run(async () => {\n const entry = this.schema.entry(document);\n const stored = this.codec.serialize(document);\n if (!(stored instanceof StoredDocument))\n throw new TypeError('codec must return StoredDocument');\n await this.documents.put(entry.documentKey, stored);\n try {\n await this.indexes.upsert(entry);\n } catch (cause) {\n throw new PartialFailureError(\n 'Document write succeeded but index upsert failed',\n entry.documentKey,\n cause,\n );\n }\n });\n }\n async get(id: string): Promise<T | undefined> {\n return this.run(async () => {\n let stored: StoredDocument;\n try {\n stored = await this.documents.get(key(this.schema.metadata.collection, id));\n } catch (cause) {\n if (cause instanceof DocumentNotFoundError) return undefined;\n throw cause;\n }\n return this.deserialize(stored);\n });\n }\n async delete(id: string): Promise<void> {\n return this.run(async () => {\n const documentKey = key(this.schema.metadata.collection, id);\n await this.documents.delete(documentKey);\n try {\n await this.indexes.delete(documentKey);\n } catch (cause) {\n throw new PartialFailureError(\n 'Document delete succeeded but index cleanup failed',\n documentKey,\n cause,\n );\n }\n });\n }\n query(): Query<T> {\n return new Query(this.schema, (query) => this.fetch(query));\n }\n async rebuildIndex(): Promise<void> {\n return this.run(async () => {\n const prepare = async (\n cursor?: string,\n ): Promise<{ entries: IndexEntry[]; nextCursor?: string }> => {\n const page = await this.documents.list(this.schema.metadata.collection, cursor, 100);\n if (page.documentKeys.length > 100)\n throw new PersistenceError('Document listing exceeded page limit');\n const entries = await mapBounded(\n page.documentKeys,\n this.#concurrency,\n async (documentKey) => {\n this.validateKey(documentKey);\n const entry = this.schema.entry(await this.hydrate(documentKey));\n if (entry.documentKey.id !== documentKey.id)\n throw new InconsistentStateError(\n 'Document key does not match its content',\n documentKey,\n );\n return entry;\n },\n );\n return {\n entries,\n ...(page.nextCursor !== undefined ? { nextCursor: page.nextCursor } : {}),\n };\n };\n let page = await prepare();\n await this.indexes.clear(this.schema.metadata.collection);\n let cursor: string | undefined;\n while (true) {\n for (const entry of page.entries) await this.indexes.upsert(entry);\n if (page.nextCursor === undefined) break;\n if (!page.nextCursor || page.nextCursor === cursor)\n throw new PersistenceError('Document listing did not advance');\n cursor = page.nextCursor;\n page = await prepare(cursor);\n }\n });\n }\n private async fetch(query: IndexQuery): Promise<Page<T>> {\n return this.run(async () => {\n const page = await this.indexes.query(query);\n if (page.documentKeys.length > query.limit)\n throw new PersistenceError('Index exceeded page limit');\n const items = await mapBounded(page.documentKeys, this.#concurrency, (documentKey) => {\n this.validateKey(documentKey);\n return this.hydrate(documentKey);\n });\n if (page.nextCursor) this.validateKey(page.nextCursor.lastDocumentKey);\n return Object.freeze({\n items: Object.freeze(items),\n ...(page.nextCursor ? { nextCursor: encodeCursor(page.nextCursor) } : {}),\n });\n });\n }\n private validateKey(documentKey: DocumentKey): void {\n key(documentKey.collection, documentKey.id);\n if (documentKey.collection !== this.schema.metadata.collection)\n throw new PersistenceError('Storage returned a key outside the collection');\n }\n private async hydrate(documentKey: DocumentKey): Promise<T> {\n let stored: StoredDocument;\n try {\n stored = await this.documents.get(documentKey);\n } catch (cause) {\n if (cause instanceof DocumentNotFoundError)\n throw new InconsistentStateError(\n 'Index or listing references a missing document',\n documentKey,\n cause,\n );\n throw cause;\n }\n return this.deserialize(stored);\n }\n private deserialize(stored: StoredDocument): T {\n const value = this.codec.deserialize(stored);\n if (value === null || value === undefined) throw new TypeError('codec returned no document');\n return value;\n }\n private async run<R>(operation: () => Promise<R>): Promise<R> {\n if (this.#active >= this.#maxOperations)\n throw new AdmissionRejectedError('Collection operation limit reached');\n this.#active++;\n try {\n return await operation();\n } finally {\n this.#active--;\n }\n }\n}\n\nexport class R2D1 {\n constructor(\n private readonly documents: DocumentStore,\n private readonly indexes: IndexStore,\n ) {}\n async collection<T extends object>(\n schema: CollectionSchema<T>,\n codec: DocumentCodec<T>,\n options?: CollectionOptions,\n ): Promise<R2D1Collection<T>> {\n if (!schema || !codec) throw new TypeError('schema and codec are required');\n // Validate options before any schema I/O.\n const collection = new R2D1Collection(schema, this.documents, this.indexes, codec, options);\n await this.indexes.initialize(schema.metadata);\n return collection;\n }\n}\n"]}
@@ -0,0 +1,5 @@
1
+ import type { IndexCursor } from './model.js';
2
+ /** Java UTF-8 replaces unpaired UTF-16 surrogates with '?'. */
3
+ export declare function javaUtf8(value: string): Uint8Array;
4
+ export declare function encodeCursor(cursor: IndexCursor): string;
5
+ export declare function decodeCursor(cursor: string): IndexCursor;
@@ -0,0 +1,139 @@
1
+ import { key, long } from './model.js';
2
+ /** Java UTF-8 replaces unpaired UTF-16 surrogates with '?'. */
3
+ export function javaUtf8(value) {
4
+ return new TextEncoder().encode(value.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDFFF]/g, (part) => part.length === 2 ? part : '?'));
5
+ }
6
+ function base64(bytes) {
7
+ let binary = '';
8
+ for (let offset = 0; offset < bytes.length; offset += 8192)
9
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + 8192));
10
+ return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
11
+ }
12
+ export function encodeCursor(cursor) {
13
+ const documentKey = key(cursor.lastDocumentKey.collection, cursor.lastDocumentKey.id);
14
+ const collection = javaUtf8(documentKey.collection);
15
+ const id = javaUtf8(documentKey.id);
16
+ const sort = cursor.sortValue;
17
+ let payload = new Uint8Array();
18
+ let tag = 0;
19
+ if (sort) {
20
+ switch (sort.type) {
21
+ case 'string': {
22
+ tag = 1;
23
+ const text = javaUtf8(sort.value);
24
+ payload = new Uint8Array(4 + text.length);
25
+ new DataView(payload.buffer).setInt32(0, text.length);
26
+ payload.set(text, 4);
27
+ break;
28
+ }
29
+ case 'long':
30
+ tag = 2;
31
+ payload = new Uint8Array(8);
32
+ new DataView(payload.buffer).setBigInt64(0, long(sort.value));
33
+ break;
34
+ case 'double':
35
+ tag = 3;
36
+ payload = new Uint8Array(8);
37
+ new DataView(payload.buffer).setFloat64(0, sort.value);
38
+ break;
39
+ case 'boolean':
40
+ tag = 4;
41
+ payload = new Uint8Array([sort.value ? 1 : 0]);
42
+ break;
43
+ case 'timestamp':
44
+ if (sort.seconds < -31557014167219200n ||
45
+ sort.seconds > 31556889864403199n ||
46
+ !Number.isInteger(sort.nanos) ||
47
+ sort.nanos < 0 ||
48
+ sort.nanos > 999999999)
49
+ throw new TypeError('invalid timestamp');
50
+ tag = 5;
51
+ payload = new Uint8Array(12);
52
+ new DataView(payload.buffer).setBigInt64(0, sort.seconds);
53
+ new DataView(payload.buffer).setInt32(8, sort.nanos);
54
+ break;
55
+ }
56
+ }
57
+ const bytes = new Uint8Array(10 + collection.length + id.length + payload.length);
58
+ const view = new DataView(bytes.buffer);
59
+ bytes[0] = 1;
60
+ view.setInt32(1, collection.length);
61
+ bytes.set(collection, 5);
62
+ view.setInt32(5 + collection.length, id.length);
63
+ bytes.set(id, 9 + collection.length);
64
+ bytes[9 + collection.length + id.length] = tag;
65
+ bytes.set(payload, 10 + collection.length + id.length);
66
+ return base64(bytes);
67
+ }
68
+ export function decodeCursor(cursor) {
69
+ try {
70
+ if (typeof cursor !== 'string' || !/^[A-Za-z0-9_-]+={0,2}$/.test(cursor))
71
+ throw new TypeError();
72
+ const binary = atob(cursor.replaceAll('-', '+').replaceAll('_', '/'));
73
+ const bytes = Uint8Array.from(binary, (ch) => ch.charCodeAt(0));
74
+ const view = new DataView(bytes.buffer);
75
+ let offset = 0;
76
+ const byte = () => view.getUint8(offset++);
77
+ const string = () => {
78
+ const length = view.getInt32(offset);
79
+ offset += 4;
80
+ if (length < 0 || length > bytes.length - offset)
81
+ throw new TypeError();
82
+ const value = new TextDecoder('utf-8', { ignoreBOM: true }).decode(bytes.subarray(offset, offset + length));
83
+ offset += length;
84
+ return value;
85
+ };
86
+ if (byte() !== 1)
87
+ throw new TypeError();
88
+ const lastDocumentKey = key(string(), string());
89
+ let sortValue;
90
+ switch (byte()) {
91
+ case 0:
92
+ break;
93
+ case 1:
94
+ sortValue = { type: 'string', value: string() };
95
+ break;
96
+ case 2:
97
+ sortValue = { type: 'long', value: view.getBigInt64(offset) };
98
+ offset += 8;
99
+ break;
100
+ case 3:
101
+ sortValue = { type: 'double', value: view.getFloat64(offset) };
102
+ offset += 8;
103
+ break;
104
+ case 4: {
105
+ const value = byte();
106
+ if (value > 1)
107
+ throw new TypeError();
108
+ sortValue = { type: 'boolean', value: value === 1 };
109
+ break;
110
+ }
111
+ case 5: {
112
+ let seconds = view.getBigInt64(offset);
113
+ offset += 8;
114
+ let nanos = view.getInt32(offset);
115
+ offset += 4;
116
+ // Instant.ofEpochSecond normalizes nanosecond adjustments.
117
+ const carry = Math.floor(nanos / 1_000_000_000);
118
+ seconds += BigInt(carry);
119
+ nanos -= carry * 1_000_000_000;
120
+ if (seconds < -31557014167219200n || seconds > 31556889864403199n)
121
+ throw new TypeError();
122
+ sortValue = { type: 'timestamp', seconds, nanos };
123
+ break;
124
+ }
125
+ default:
126
+ throw new TypeError();
127
+ }
128
+ if (offset !== bytes.length)
129
+ throw new TypeError();
130
+ return Object.freeze({
131
+ lastDocumentKey,
132
+ ...(sortValue ? { sortValue: Object.freeze(sortValue) } : {}),
133
+ });
134
+ }
135
+ catch (cause) {
136
+ throw new TypeError('invalid cursor', { cause });
137
+ }
138
+ }
139
+ //# sourceMappingURL=cursor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../src/core/cursor.ts"],"names":[],"mappings":"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","sourcesContent":["import { key, long } from './model.js';\nimport type { IndexCursor, IndexValue } from './model.js';\n\n/** Java UTF-8 replaces unpaired UTF-16 surrogates with '?'. */\nexport function javaUtf8(value: string): Uint8Array {\n return new TextEncoder().encode(\n value.replace(/[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDFFF]/g, (part) =>\n part.length === 2 ? part : '?',\n ),\n );\n}\nfunction base64(bytes: Uint8Array): string {\n let binary = '';\n for (let offset = 0; offset < bytes.length; offset += 8192)\n binary += String.fromCharCode(...bytes.subarray(offset, offset + 8192));\n return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');\n}\nexport function encodeCursor(cursor: IndexCursor): string {\n const documentKey = key(cursor.lastDocumentKey.collection, cursor.lastDocumentKey.id);\n const collection = javaUtf8(documentKey.collection);\n const id = javaUtf8(documentKey.id);\n const sort = cursor.sortValue;\n let payload = new Uint8Array();\n let tag = 0;\n if (sort) {\n switch (sort.type) {\n case 'string': {\n tag = 1;\n const text = javaUtf8(sort.value);\n payload = new Uint8Array(4 + text.length);\n new DataView(payload.buffer).setInt32(0, text.length);\n payload.set(text, 4);\n break;\n }\n case 'long':\n tag = 2;\n payload = new Uint8Array(8);\n new DataView(payload.buffer).setBigInt64(0, long(sort.value));\n break;\n case 'double':\n tag = 3;\n payload = new Uint8Array(8);\n new DataView(payload.buffer).setFloat64(0, sort.value);\n break;\n case 'boolean':\n tag = 4;\n payload = new Uint8Array([sort.value ? 1 : 0]);\n break;\n case 'timestamp':\n if (\n sort.seconds < -31557014167219200n ||\n sort.seconds > 31556889864403199n ||\n !Number.isInteger(sort.nanos) ||\n sort.nanos < 0 ||\n sort.nanos > 999999999\n )\n throw new TypeError('invalid timestamp');\n tag = 5;\n payload = new Uint8Array(12);\n new DataView(payload.buffer).setBigInt64(0, sort.seconds);\n new DataView(payload.buffer).setInt32(8, sort.nanos);\n break;\n }\n }\n const bytes = new Uint8Array(10 + collection.length + id.length + payload.length);\n const view = new DataView(bytes.buffer);\n bytes[0] = 1;\n view.setInt32(1, collection.length);\n bytes.set(collection, 5);\n view.setInt32(5 + collection.length, id.length);\n bytes.set(id, 9 + collection.length);\n bytes[9 + collection.length + id.length] = tag;\n bytes.set(payload, 10 + collection.length + id.length);\n return base64(bytes);\n}\nexport function decodeCursor(cursor: string): IndexCursor {\n try {\n if (typeof cursor !== 'string' || !/^[A-Za-z0-9_-]+={0,2}$/.test(cursor)) throw new TypeError();\n const binary = atob(cursor.replaceAll('-', '+').replaceAll('_', '/'));\n const bytes = Uint8Array.from(binary, (ch) => ch.charCodeAt(0));\n const view = new DataView(bytes.buffer);\n let offset = 0;\n const byte = (): number => view.getUint8(offset++);\n const string = (): string => {\n const length = view.getInt32(offset);\n offset += 4;\n if (length < 0 || length > bytes.length - offset) throw new TypeError();\n const value = new TextDecoder('utf-8', { ignoreBOM: true }).decode(\n bytes.subarray(offset, offset + length),\n );\n offset += length;\n return value;\n };\n if (byte() !== 1) throw new TypeError();\n const lastDocumentKey = key(string(), string());\n let sortValue: IndexValue | undefined;\n switch (byte()) {\n case 0:\n break;\n case 1:\n sortValue = { type: 'string', value: string() };\n break;\n case 2:\n sortValue = { type: 'long', value: view.getBigInt64(offset) };\n offset += 8;\n break;\n case 3:\n sortValue = { type: 'double', value: view.getFloat64(offset) };\n offset += 8;\n break;\n case 4: {\n const value = byte();\n if (value > 1) throw new TypeError();\n sortValue = { type: 'boolean', value: value === 1 };\n break;\n }\n case 5: {\n let seconds = view.getBigInt64(offset);\n offset += 8;\n let nanos = view.getInt32(offset);\n offset += 4;\n // Instant.ofEpochSecond normalizes nanosecond adjustments.\n const carry = Math.floor(nanos / 1_000_000_000);\n seconds += BigInt(carry);\n nanos -= carry * 1_000_000_000;\n if (seconds < -31557014167219200n || seconds > 31556889864403199n) throw new TypeError();\n sortValue = { type: 'timestamp', seconds, nanos };\n break;\n }\n default:\n throw new TypeError();\n }\n if (offset !== bytes.length) throw new TypeError();\n return Object.freeze({\n lastDocumentKey,\n ...(sortValue ? { sortValue: Object.freeze(sortValue) } : {}),\n });\n } catch (cause) {\n throw new TypeError('invalid cursor', { cause });\n }\n}\n"]}
@@ -0,0 +1,27 @@
1
+ import type { DocumentKey } from './model.js';
2
+ export declare class StorageError extends Error {
3
+ constructor(message: string, options?: ErrorOptions);
4
+ }
5
+ export declare class StorageAccessError extends StorageError {
6
+ }
7
+ export declare class StorageUnavailableError extends StorageError {
8
+ }
9
+ export declare class StorageOperationError extends StorageError {
10
+ }
11
+ export declare class DocumentNotFoundError extends StorageError {
12
+ readonly documentKey: DocumentKey;
13
+ constructor(documentKey: DocumentKey);
14
+ }
15
+ export declare class PersistenceError extends Error {
16
+ constructor(message: string, options?: ErrorOptions);
17
+ }
18
+ export declare class PartialFailureError extends PersistenceError {
19
+ readonly documentKey: DocumentKey;
20
+ constructor(message: string, documentKey: DocumentKey, cause: unknown);
21
+ }
22
+ export declare class InconsistentStateError extends PersistenceError {
23
+ readonly documentKey: DocumentKey;
24
+ constructor(message: string, documentKey: DocumentKey, cause?: unknown);
25
+ }
26
+ export declare class AdmissionRejectedError extends PersistenceError {
27
+ }
@@ -0,0 +1,42 @@
1
+ export class StorageError extends Error {
2
+ constructor(message, options) {
3
+ super(message, options);
4
+ this.name = new.target.name;
5
+ }
6
+ }
7
+ export class StorageAccessError extends StorageError {
8
+ }
9
+ export class StorageUnavailableError extends StorageError {
10
+ }
11
+ export class StorageOperationError extends StorageError {
12
+ }
13
+ export class DocumentNotFoundError extends StorageError {
14
+ documentKey;
15
+ constructor(documentKey) {
16
+ super(`Document not found: ${documentKey.collection}/${documentKey.id}`);
17
+ this.documentKey = documentKey;
18
+ }
19
+ }
20
+ export class PersistenceError extends Error {
21
+ constructor(message, options) {
22
+ super(message, options);
23
+ this.name = new.target.name;
24
+ }
25
+ }
26
+ export class PartialFailureError extends PersistenceError {
27
+ documentKey;
28
+ constructor(message, documentKey, cause) {
29
+ super(message, { cause });
30
+ this.documentKey = documentKey;
31
+ }
32
+ }
33
+ export class InconsistentStateError extends PersistenceError {
34
+ documentKey;
35
+ constructor(message, documentKey, cause) {
36
+ super(message, { cause });
37
+ this.documentKey = documentKey;
38
+ }
39
+ }
40
+ export class AdmissionRejectedError extends PersistenceError {
41
+ }
42
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,YAAa,SAAQ,KAAK;IACrC,YAAY,OAAe,EAAE,OAAsB;QACjD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC;IAC9B,CAAC;CACF;AACD,MAAM,OAAO,kBAAmB,SAAQ,YAAY;CAAG;AACvD,MAAM,OAAO,uBAAwB,SAAQ,YAAY;CAAG;AAC5D,MAAM,OAAO,qBAAsB,SAAQ,YAAY;CAAG;AAC1D,MAAM,OAAO,qBAAsB,SAAQ,YAAY;IAChC,WAAW;IAAhC,YAAqB,WAAwB;QAC3C,KAAK,CAAC,uBAAuB,WAAW,CAAC,UAAU,IAAI,WAAW,CAAC,EAAE,EAAE,CAAC,CAAC;2BADtD,WAAW;IAEhC,CAAC;CACF;AACD,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IACzC,YAAY,OAAe,EAAE,OAAsB;QACjD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC;IAC9B,CAAC;CACF;AACD,MAAM,OAAO,mBAAoB,SAAQ,gBAAgB;IAG5C,WAAW;IAFtB,YACE,OAAe,EACN,WAAwB,EACjC,KAAc;QAEd,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;2BAHjB,WAAW;IAItB,CAAC;CACF;AACD,MAAM,OAAO,sBAAuB,SAAQ,gBAAgB;IAG/C,WAAW;IAFtB,YACE,OAAe,EACN,WAAwB,EACjC,KAAe;QAEf,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;2BAHjB,WAAW;IAItB,CAAC;CACF;AACD,MAAM,OAAO,sBAAuB,SAAQ,gBAAgB;CAAG","sourcesContent":["import type { DocumentKey } from './model.js';\n\nexport class StorageError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = new.target.name;\n }\n}\nexport class StorageAccessError extends StorageError {}\nexport class StorageUnavailableError extends StorageError {}\nexport class StorageOperationError extends StorageError {}\nexport class DocumentNotFoundError extends StorageError {\n constructor(readonly documentKey: DocumentKey) {\n super(`Document not found: ${documentKey.collection}/${documentKey.id}`);\n }\n}\nexport class PersistenceError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = new.target.name;\n }\n}\nexport class PartialFailureError extends PersistenceError {\n constructor(\n message: string,\n readonly documentKey: DocumentKey,\n cause: unknown,\n ) {\n super(message, { cause });\n }\n}\nexport class InconsistentStateError extends PersistenceError {\n constructor(\n message: string,\n readonly documentKey: DocumentKey,\n cause?: unknown,\n ) {\n super(message, { cause });\n }\n}\nexport class AdmissionRejectedError extends PersistenceError {}\n"]}
@@ -0,0 +1,91 @@
1
+ export interface DocumentKey {
2
+ readonly collection: string;
3
+ readonly id: string;
4
+ }
5
+ export type IndexType = 'string' | 'long' | 'double' | 'boolean';
6
+ export type IndexValue = {
7
+ readonly type: 'string';
8
+ readonly value: string;
9
+ } | {
10
+ readonly type: 'long';
11
+ readonly value: bigint;
12
+ } | {
13
+ readonly type: 'double';
14
+ readonly value: number;
15
+ } | {
16
+ readonly type: 'boolean';
17
+ readonly value: boolean;
18
+ } | {
19
+ readonly type: 'timestamp';
20
+ readonly seconds: bigint;
21
+ readonly nanos: number;
22
+ };
23
+ export interface IndexCursor {
24
+ readonly lastDocumentKey: DocumentKey;
25
+ readonly sortValue?: IndexValue;
26
+ }
27
+ export interface IndexEntry {
28
+ readonly documentKey: DocumentKey;
29
+ readonly values: Readonly<Record<string, IndexValue>>;
30
+ }
31
+ export type ComparisonOperator = '=' | '<>' | '>' | '>=' | '<' | '<=';
32
+ export type SortDirection = 'ASC' | 'DESC';
33
+ export interface Filter {
34
+ readonly field: string;
35
+ readonly operator: ComparisonOperator;
36
+ readonly value: IndexValue;
37
+ }
38
+ export interface Sort {
39
+ readonly field: string;
40
+ readonly direction: SortDirection;
41
+ }
42
+ export interface IndexQuery {
43
+ readonly collection: string;
44
+ readonly filters: readonly Filter[];
45
+ readonly limit: number;
46
+ readonly sort?: Sort;
47
+ readonly cursor?: IndexCursor;
48
+ }
49
+ export interface IndexPage {
50
+ readonly documentKeys: readonly DocumentKey[];
51
+ readonly nextCursor?: IndexCursor;
52
+ }
53
+ export interface DocumentPage {
54
+ readonly documentKeys: readonly DocumentKey[];
55
+ readonly nextCursor?: string;
56
+ }
57
+ export interface Page<T> {
58
+ readonly items: readonly T[];
59
+ readonly nextCursor?: string;
60
+ }
61
+ /** Serialized bytes with defensive copies on input and output. */
62
+ export declare class StoredDocument {
63
+ #private;
64
+ constructor(content: Uint8Array);
65
+ content(): Uint8Array;
66
+ }
67
+ export interface DocumentStore {
68
+ put(key: DocumentKey, document: StoredDocument): Promise<void>;
69
+ get(key: DocumentKey): Promise<StoredDocument>;
70
+ delete(key: DocumentKey): Promise<void>;
71
+ list(collection: string, cursor: string | undefined, limit: number): Promise<DocumentPage>;
72
+ }
73
+ export interface CollectionMetadata {
74
+ readonly collection: string;
75
+ readonly fields: readonly {
76
+ readonly name: string;
77
+ readonly type: IndexType;
78
+ }[];
79
+ }
80
+ export interface IndexStore {
81
+ initialize(metadata: CollectionMetadata): Promise<void>;
82
+ upsert(entry: IndexEntry): Promise<void>;
83
+ delete(key: DocumentKey): Promise<void>;
84
+ query(query: IndexQuery): Promise<IndexPage>;
85
+ clear(collection: string): Promise<void>;
86
+ }
87
+ export declare function text(value: unknown, name: string): string;
88
+ export declare function key(collection: string, id: string): DocumentKey;
89
+ export declare function positive(value: number, name: string, max?: number): number;
90
+ export declare function long(value: unknown): bigint;
91
+ export declare function indexValue(type: IndexType, value: unknown): IndexValue;
@@ -0,0 +1,54 @@
1
+ /** Serialized bytes with defensive copies on input and output. */
2
+ export class StoredDocument {
3
+ #bytes;
4
+ constructor(content) {
5
+ if (!(content instanceof Uint8Array))
6
+ throw new TypeError('content must be Uint8Array');
7
+ this.#bytes = new Uint8Array(content);
8
+ }
9
+ content() {
10
+ return this.#bytes.slice();
11
+ }
12
+ }
13
+ // Java String.isBlank excludes nonbreaking spaces.
14
+ export function text(value, name) {
15
+ if (typeof value !== 'string' ||
16
+ /^[\u0009-\u000D\u001C-\u0020\u1680\u2000-\u2006\u2008-\u200A\u2028\u2029\u205F\u3000]*$/u.test(value)) {
17
+ throw new TypeError(`${name} must be a nonblank string`);
18
+ }
19
+ return value;
20
+ }
21
+ export function key(collection, id) {
22
+ return Object.freeze({ collection: text(collection, 'collection'), id: text(id, 'id') });
23
+ }
24
+ export function positive(value, name, max = 2_147_483_647) {
25
+ if (!Number.isSafeInteger(value) || value <= 0 || value > max)
26
+ throw new RangeError(`${name} must be a positive integer <= ${max}`);
27
+ return value;
28
+ }
29
+ export function long(value) {
30
+ if (typeof value !== 'bigint' || value < -(1n << 63n) || value > (1n << 63n) - 1n) {
31
+ throw new TypeError('long must be a signed 64-bit bigint');
32
+ }
33
+ return value;
34
+ }
35
+ export function indexValue(type, value) {
36
+ switch (type) {
37
+ case 'string':
38
+ if (typeof value === 'string')
39
+ return Object.freeze({ type, value });
40
+ break;
41
+ case 'long':
42
+ return Object.freeze({ type, value: long(value) });
43
+ case 'double':
44
+ if (typeof value === 'number' && Number.isFinite(value))
45
+ return Object.freeze({ type, value });
46
+ break;
47
+ case 'boolean':
48
+ if (typeof value === 'boolean')
49
+ return Object.freeze({ type, value });
50
+ break;
51
+ }
52
+ throw new TypeError(`index value must have type ${type}`);
53
+ }
54
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../../src/core/model.ts"],"names":[],"mappings":"AAkDA,kEAAkE;AAClE,MAAM,OAAO,cAAc;IAChB,MAAM,CAAa;IAC5B,YAAY,OAAmB;QAC7B,IAAI,CAAC,CAAC,OAAO,YAAY,UAAU,CAAC;YAAE,MAAM,IAAI,SAAS,CAAC,4BAA4B,CAAC,CAAC;QACxF,IAAI,CAAC,MAAM,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC;IACxC,CAAC;IACD,OAAO;QACL,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IAC7B,CAAC;CACF;AAmBD,mDAAmD;AACnD,MAAM,UAAU,IAAI,CAAC,KAAc,EAAE,IAAY;IAC/C,IACE,OAAO,KAAK,KAAK,QAAQ;QACzB,0FAA0F,CAAC,IAAI,CAC7F,KAAK,CACN,EACD,CAAC;QACD,MAAM,IAAI,SAAS,CAAC,GAAG,IAAI,4BAA4B,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AACD,MAAM,UAAU,GAAG,CAAC,UAAkB,EAAE,EAAU;IAChD,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;AAC3F,CAAC;AACD,MAAM,UAAU,QAAQ,CAAC,KAAa,EAAE,IAAY,EAAE,GAAG,GAAG,aAAa;IACvE,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,GAAG,GAAG;QAC3D,MAAM,IAAI,UAAU,CAAC,GAAG,IAAI,kCAAkC,GAAG,EAAE,CAAC,CAAC;IACvE,OAAO,KAAK,CAAC;AACf,CAAC;AACD,MAAM,UAAU,IAAI,CAAC,KAAc;IACjC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,GAAG,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC;QAClF,MAAM,IAAI,SAAS,CAAC,qCAAqC,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AACD,MAAM,UAAU,UAAU,CAAC,IAAe,EAAE,KAAc;IACxD,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,QAAQ;YACX,IAAI,OAAO,KAAK,KAAK,QAAQ;gBAAE,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACrE,MAAM;QACR,KAAK,MAAM;YACT,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACrD,KAAK,QAAQ;YACX,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;gBACrD,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACxC,MAAM;QACR,KAAK,SAAS;YACZ,IAAI,OAAO,KAAK,KAAK,SAAS;gBAAE,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACtE,MAAM;IACV,CAAC;IACD,MAAM,IAAI,SAAS,CAAC,8BAA8B,IAAI,EAAE,CAAC,CAAC;AAC5D,CAAC","sourcesContent":["export interface DocumentKey {\n readonly collection: string;\n readonly id: string;\n}\nexport type IndexType = 'string' | 'long' | 'double' | 'boolean';\nexport type IndexValue =\n | { readonly type: 'string'; readonly value: string }\n | { readonly type: 'long'; readonly value: bigint }\n | { readonly type: 'double'; readonly value: number }\n | { readonly type: 'boolean'; readonly value: boolean }\n | { readonly type: 'timestamp'; readonly seconds: bigint; readonly nanos: number };\nexport interface IndexCursor {\n readonly lastDocumentKey: DocumentKey;\n readonly sortValue?: IndexValue;\n}\nexport interface IndexEntry {\n readonly documentKey: DocumentKey;\n readonly values: Readonly<Record<string, IndexValue>>;\n}\nexport type ComparisonOperator = '=' | '<>' | '>' | '>=' | '<' | '<=';\nexport type SortDirection = 'ASC' | 'DESC';\nexport interface Filter {\n readonly field: string;\n readonly operator: ComparisonOperator;\n readonly value: IndexValue;\n}\nexport interface Sort {\n readonly field: string;\n readonly direction: SortDirection;\n}\nexport interface IndexQuery {\n readonly collection: string;\n readonly filters: readonly Filter[];\n readonly limit: number;\n readonly sort?: Sort;\n readonly cursor?: IndexCursor;\n}\nexport interface IndexPage {\n readonly documentKeys: readonly DocumentKey[];\n readonly nextCursor?: IndexCursor;\n}\nexport interface DocumentPage {\n readonly documentKeys: readonly DocumentKey[];\n readonly nextCursor?: string;\n}\nexport interface Page<T> {\n readonly items: readonly T[];\n readonly nextCursor?: string;\n}\n\n/** Serialized bytes with defensive copies on input and output. */\nexport class StoredDocument {\n readonly #bytes: Uint8Array;\n constructor(content: Uint8Array) {\n if (!(content instanceof Uint8Array)) throw new TypeError('content must be Uint8Array');\n this.#bytes = new Uint8Array(content);\n }\n content(): Uint8Array {\n return this.#bytes.slice();\n }\n}\nexport interface DocumentStore {\n put(key: DocumentKey, document: StoredDocument): Promise<void>;\n get(key: DocumentKey): Promise<StoredDocument>;\n delete(key: DocumentKey): Promise<void>;\n list(collection: string, cursor: string | undefined, limit: number): Promise<DocumentPage>;\n}\nexport interface CollectionMetadata {\n readonly collection: string;\n readonly fields: readonly { readonly name: string; readonly type: IndexType }[];\n}\nexport interface IndexStore {\n initialize(metadata: CollectionMetadata): Promise<void>;\n upsert(entry: IndexEntry): Promise<void>;\n delete(key: DocumentKey): Promise<void>;\n query(query: IndexQuery): Promise<IndexPage>;\n clear(collection: string): Promise<void>;\n}\n\n// Java String.isBlank excludes nonbreaking spaces.\nexport function text(value: unknown, name: string): string {\n if (\n typeof value !== 'string' ||\n /^[\\u0009-\\u000D\\u001C-\\u0020\\u1680\\u2000-\\u2006\\u2008-\\u200A\\u2028\\u2029\\u205F\\u3000]*$/u.test(\n value,\n )\n ) {\n throw new TypeError(`${name} must be a nonblank string`);\n }\n return value;\n}\nexport function key(collection: string, id: string): DocumentKey {\n return Object.freeze({ collection: text(collection, 'collection'), id: text(id, 'id') });\n}\nexport function positive(value: number, name: string, max = 2_147_483_647): number {\n if (!Number.isSafeInteger(value) || value <= 0 || value > max)\n throw new RangeError(`${name} must be a positive integer <= ${max}`);\n return value;\n}\nexport function long(value: unknown): bigint {\n if (typeof value !== 'bigint' || value < -(1n << 63n) || value > (1n << 63n) - 1n) {\n throw new TypeError('long must be a signed 64-bit bigint');\n }\n return value;\n}\nexport function indexValue(type: IndexType, value: unknown): IndexValue {\n switch (type) {\n case 'string':\n if (typeof value === 'string') return Object.freeze({ type, value });\n break;\n case 'long':\n return Object.freeze({ type, value: long(value) });\n case 'double':\n if (typeof value === 'number' && Number.isFinite(value))\n return Object.freeze({ type, value });\n break;\n case 'boolean':\n if (typeof value === 'boolean') return Object.freeze({ type, value });\n break;\n }\n throw new TypeError(`index value must have type ${type}`);\n}\n"]}
@@ -0,0 +1,29 @@
1
+ import type { Filter, IndexQuery, Page, Sort, SortDirection } from './model.js';
2
+ import type { CollectionSchema } from './schema.js';
3
+ interface State {
4
+ readonly filters: readonly Filter[];
5
+ readonly sort?: Sort;
6
+ readonly limit?: number;
7
+ readonly cursor?: string;
8
+ }
9
+ export declare class Query<T extends object> {
10
+ #private;
11
+ readonly schema: CollectionSchema<T>;
12
+ readonly execute: (query: IndexQuery) => Promise<Page<T>>;
13
+ constructor(schema: CollectionSchema<T>, execute: (query: IndexQuery) => Promise<Page<T>>, state?: State);
14
+ where<K extends keyof T & string>(field: K): Comparison<T, T[K]>;
15
+ sortBy(field: keyof T & string, direction?: SortDirection): Query<T>;
16
+ limit(limit: number): Query<T>;
17
+ after(cursor: string): Query<T>;
18
+ fetch(): Promise<Page<T>>;
19
+ private copy;
20
+ }
21
+ export interface Comparison<T extends object, V> {
22
+ eq(value: V): Query<T>;
23
+ notEq(value: V): Query<T>;
24
+ gt(value: V): Query<T>;
25
+ gte(value: V): Query<T>;
26
+ lt(value: V): Query<T>;
27
+ lte(value: V): Query<T>;
28
+ }
29
+ export {};
@@ -0,0 +1,65 @@
1
+ import { decodeCursor } from './cursor.js';
2
+ import { positive, text } from './model.js';
3
+ export class Query {
4
+ schema;
5
+ execute;
6
+ #state;
7
+ constructor(schema, execute, state = { filters: [] }) {
8
+ this.schema = schema;
9
+ this.execute = execute;
10
+ this.#state = Object.freeze({ ...state, filters: Object.freeze([...state.filters]) });
11
+ Object.freeze(this);
12
+ }
13
+ where(field) {
14
+ text(field, 'field');
15
+ const compare = (operator, value) => {
16
+ const indexed = this.schema.value(field, value);
17
+ if (indexed.type === 'boolean' && operator !== '=' && operator !== '<>')
18
+ throw new TypeError('boolean supports only equality comparisons');
19
+ const filter = Object.freeze({ field, operator, value: indexed });
20
+ return this.copy({ filters: [...this.#state.filters, filter] });
21
+ };
22
+ return Object.freeze({
23
+ eq: (v) => compare('=', v),
24
+ notEq: (v) => compare('<>', v),
25
+ gt: (v) => compare('>', v),
26
+ gte: (v) => compare('>=', v),
27
+ lt: (v) => compare('<', v),
28
+ lte: (v) => compare('<=', v),
29
+ });
30
+ }
31
+ sortBy(field, direction = 'ASC') {
32
+ if (this.#state.sort)
33
+ throw new TypeError('sort is already configured');
34
+ if (!this.schema.metadata.fields.some((f) => f.name === field))
35
+ throw new TypeError(`field is not indexed: ${field}`);
36
+ if (direction !== 'ASC' && direction !== 'DESC')
37
+ throw new TypeError('invalid sort direction');
38
+ return this.copy({ sort: Object.freeze({ field, direction }) });
39
+ }
40
+ limit(limit) {
41
+ if (this.#state.limit !== undefined)
42
+ throw new TypeError('limit is already configured');
43
+ return this.copy({ limit: positive(limit, 'limit') });
44
+ }
45
+ after(cursor) {
46
+ if (this.#state.cursor !== undefined)
47
+ throw new TypeError('cursor is already configured');
48
+ return this.copy({ cursor: text(cursor, 'cursor') });
49
+ }
50
+ async fetch() {
51
+ if (this.#state.limit === undefined)
52
+ throw new TypeError('limit must be configured before fetch');
53
+ return this.execute({
54
+ collection: this.schema.metadata.collection,
55
+ filters: this.#state.filters,
56
+ limit: this.#state.limit,
57
+ ...(this.#state.sort ? { sort: this.#state.sort } : {}),
58
+ ...(this.#state.cursor ? { cursor: decodeCursor(this.#state.cursor) } : {}),
59
+ });
60
+ }
61
+ copy(update) {
62
+ return new Query(this.schema, this.execute, { ...this.#state, ...update });
63
+ }
64
+ }
65
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.js","sourceRoot":"","sources":["../../src/core/query.ts"],"names":[],"mappings":"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","sourcesContent":["import { decodeCursor } from './cursor.js';\nimport { positive, text } from './model.js';\nimport type { ComparisonOperator, Filter, IndexQuery, Page, Sort, SortDirection } from './model.js';\nimport type { CollectionSchema } from './schema.js';\n\ninterface State {\n readonly filters: readonly Filter[];\n readonly sort?: Sort;\n readonly limit?: number;\n readonly cursor?: string;\n}\nexport class Query<T extends object> {\n readonly #state: State;\n constructor(\n readonly schema: CollectionSchema<T>,\n readonly execute: (query: IndexQuery) => Promise<Page<T>>,\n state: State = { filters: [] },\n ) {\n this.#state = Object.freeze({ ...state, filters: Object.freeze([...state.filters]) });\n Object.freeze(this);\n }\n where<K extends keyof T & string>(field: K): Comparison<T, T[K]> {\n text(field, 'field');\n const compare = (operator: ComparisonOperator, value: T[K]): Query<T> => {\n const indexed = this.schema.value(field, value);\n if (indexed.type === 'boolean' && operator !== '=' && operator !== '<>')\n throw new TypeError('boolean supports only equality comparisons');\n const filter = Object.freeze({ field, operator, value: indexed });\n return this.copy({ filters: [...this.#state.filters, filter] });\n };\n return Object.freeze({\n eq: (v: T[K]) => compare('=', v),\n notEq: (v: T[K]) => compare('<>', v),\n gt: (v: T[K]) => compare('>', v),\n gte: (v: T[K]) => compare('>=', v),\n lt: (v: T[K]) => compare('<', v),\n lte: (v: T[K]) => compare('<=', v),\n });\n }\n sortBy(field: keyof T & string, direction: SortDirection = 'ASC'): Query<T> {\n if (this.#state.sort) throw new TypeError('sort is already configured');\n if (!this.schema.metadata.fields.some((f) => f.name === field))\n throw new TypeError(`field is not indexed: ${field}`);\n if (direction !== 'ASC' && direction !== 'DESC') throw new TypeError('invalid sort direction');\n return this.copy({ sort: Object.freeze({ field, direction }) });\n }\n limit(limit: number): Query<T> {\n if (this.#state.limit !== undefined) throw new TypeError('limit is already configured');\n return this.copy({ limit: positive(limit, 'limit') });\n }\n after(cursor: string): Query<T> {\n if (this.#state.cursor !== undefined) throw new TypeError('cursor is already configured');\n return this.copy({ cursor: text(cursor, 'cursor') });\n }\n async fetch(): Promise<Page<T>> {\n if (this.#state.limit === undefined)\n throw new TypeError('limit must be configured before fetch');\n return this.execute({\n collection: this.schema.metadata.collection,\n filters: this.#state.filters,\n limit: this.#state.limit,\n ...(this.#state.sort ? { sort: this.#state.sort } : {}),\n ...(this.#state.cursor ? { cursor: decodeCursor(this.#state.cursor) } : {}),\n });\n }\n private copy(update: Partial<State>): Query<T> {\n return new Query(this.schema, this.execute, { ...this.#state, ...update });\n }\n}\nexport interface Comparison<T extends object, V> {\n eq(value: V): Query<T>;\n notEq(value: V): Query<T>;\n gt(value: V): Query<T>;\n gte(value: V): Query<T>;\n lt(value: V): Query<T>;\n lte(value: V): Query<T>;\n}\n"]}
@@ -0,0 +1,21 @@
1
+ import type { CollectionMetadata, IndexEntry, IndexValue } from './model.js';
2
+ export type IndexedFields<T> = Partial<{
3
+ readonly [K in keyof T & string]: T[K] extends string ? 'string' : T[K] extends bigint ? 'long' : T[K] extends number ? 'double' : T[K] extends boolean ? 'boolean' : never;
4
+ }>;
5
+ export interface SchemaOptions<T extends object> {
6
+ readonly collection: string;
7
+ readonly id: {
8
+ [K in keyof T & string]: T[K] extends string ? K : never;
9
+ }[keyof T & string];
10
+ readonly indexes: IndexedFields<T>;
11
+ }
12
+ export declare function identifier(value: string): string;
13
+ export declare function validateMetadata(metadata: CollectionMetadata): CollectionMetadata;
14
+ export declare class CollectionSchema<T extends object> {
15
+ readonly metadata: CollectionMetadata;
16
+ readonly id: keyof T & string;
17
+ constructor(options: SchemaOptions<T>);
18
+ value(field: string, value: unknown): IndexValue;
19
+ entry(document: T): IndexEntry;
20
+ }
21
+ export declare function defineSchema<T extends object>(options: SchemaOptions<T>): CollectionSchema<T>;
@@ -0,0 +1,61 @@
1
+ import { indexValue, key } from './model.js';
2
+ export function identifier(value) {
3
+ if (typeof value !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(value))
4
+ throw new TypeError('identifier must match [A-Za-z_][A-Za-z0-9_]*');
5
+ return value;
6
+ }
7
+ export function validateMetadata(metadata) {
8
+ const collection = identifier(metadata.collection);
9
+ if (metadata.fields.length > 5)
10
+ throw new RangeError('at most five indexed fields are supported');
11
+ const seen = new Set();
12
+ const fields = metadata.fields
13
+ .map((field) => {
14
+ const name = identifier(field.name);
15
+ if (name === 'document_id' || seen.has(name))
16
+ throw new TypeError('reserved or duplicate indexed field');
17
+ if (!['string', 'long', 'double', 'boolean'].includes(field.type))
18
+ throw new TypeError('unsupported index type');
19
+ seen.add(name);
20
+ return Object.freeze({ name, type: field.type });
21
+ })
22
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
23
+ return Object.freeze({ collection, fields: Object.freeze(fields) });
24
+ }
25
+ export class CollectionSchema {
26
+ metadata;
27
+ id;
28
+ constructor(options) {
29
+ identifier(options.id);
30
+ this.id = options.id;
31
+ this.metadata = validateMetadata({
32
+ collection: options.collection,
33
+ fields: Object.entries(options.indexes).map(([name, type]) => ({
34
+ name,
35
+ type: type,
36
+ })),
37
+ });
38
+ Object.freeze(this);
39
+ }
40
+ value(field, value) {
41
+ const definition = this.metadata.fields.find((f) => f.name === field);
42
+ if (!definition)
43
+ throw new TypeError(`field is not indexed: ${field}`);
44
+ return indexValue(definition.type, value);
45
+ }
46
+ entry(document) {
47
+ if (document === null || typeof document !== 'object')
48
+ throw new TypeError('document must be an object');
49
+ const values = Object.create(null);
50
+ for (const field of this.metadata.fields)
51
+ values[field.name] = this.value(field.name, document[field.name]);
52
+ return Object.freeze({
53
+ documentKey: key(this.metadata.collection, document[this.id]),
54
+ values: Object.freeze(values),
55
+ });
56
+ }
57
+ }
58
+ export function defineSchema(options) {
59
+ return new CollectionSchema(options);
60
+ }
61
+ //# sourceMappingURL=schema.js.map