@nexcraft-dev/r2d1 0.0.0-stage → 0.1.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 +165 -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":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAElE,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAExC,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAmBjD,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC","sourcesContent":["export { R2D1, R2D1Collection } from './core/collection.js';\nexport type { CollectionOptions } from './core/collection.js';\nexport { CollectionSchema, defineSchema } from './core/schema.js';\nexport type { SchemaOptions, IndexedFields } from './core/schema.js';\nexport { Query } from './core/query.js';\nexport type { Comparison } from './core/query.js';\nexport { jsonCodec } from './core/codec.js';\nexport type { DocumentCodec } from './core/codec.js';\nexport { StoredDocument } from './core/model.js';\nexport type {\n DocumentKey,\n DocumentStore,\n DocumentPage,\n CollectionMetadata,\n IndexStore,\n IndexValue,\n IndexType,\n IndexEntry,\n IndexCursor,\n IndexPage,\n IndexQuery,\n Filter,\n Sort,\n SortDirection,\n ComparisonOperator,\n Page,\n} from './core/model.js';\nexport * from './core/errors.js';\nexport { encodeCursor, decodeCursor } from './core/cursor.js';\n"]}
@@ -0,0 +1,10 @@
1
+ import { D1IndexStore } from '../storage/d1.js';
2
+ export interface NodeD1Options {
3
+ readonly accountId: string;
4
+ readonly databaseId: string;
5
+ readonly apiToken: string;
6
+ readonly fetch?: typeof globalThis.fetch;
7
+ }
8
+ export declare class NodeD1IndexStore extends D1IndexStore {
9
+ constructor(options: NodeD1Options);
10
+ }
@@ -0,0 +1,57 @@
1
+ import { StorageAccessError, StorageOperationError, StorageUnavailableError, } from '../core/errors.js';
2
+ import { text } from '../core/model.js';
3
+ import { D1IndexStore, sqlRows } from '../storage/d1.js';
4
+ class RestD1Transport {
5
+ #endpoint;
6
+ #token;
7
+ #fetch;
8
+ constructor(options) {
9
+ this.#endpoint = `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(text(options.accountId, 'accountId'))}/d1/database/${encodeURIComponent(text(options.databaseId, 'databaseId'))}/query`;
10
+ this.#token = text(options.apiToken, 'apiToken');
11
+ this.#fetch = options.fetch ?? globalThis.fetch;
12
+ }
13
+ async execute(statement) {
14
+ let response;
15
+ try {
16
+ response = await this.#fetch(this.#endpoint, {
17
+ method: 'POST',
18
+ redirect: 'error',
19
+ headers: { Authorization: `Bearer ${this.#token}`, 'Content-Type': 'application/json' },
20
+ body: JSON.stringify({ sql: statement.sql, params: statement.params }),
21
+ });
22
+ }
23
+ catch {
24
+ throw new StorageUnavailableError('D1 request failed before receiving a response');
25
+ }
26
+ if (response.status === 401 || response.status === 403)
27
+ throw new StorageAccessError(`D1 access denied with HTTP status ${response.status}`);
28
+ if (response.status === 429 || response.status >= 500)
29
+ throw new StorageUnavailableError(`D1 unavailable with HTTP status ${response.status}`);
30
+ if (!response.ok)
31
+ throw new StorageOperationError(`D1 request failed with HTTP status ${response.status}`);
32
+ let payload;
33
+ try {
34
+ payload = await response.json();
35
+ }
36
+ catch {
37
+ throw new StorageOperationError('D1 response could not be decoded');
38
+ }
39
+ if (!payload || typeof payload !== 'object')
40
+ throw new StorageOperationError('D1 returned an invalid response');
41
+ const envelope = payload;
42
+ if (envelope['success'] !== true ||
43
+ !Array.isArray(envelope['result']) ||
44
+ envelope['result'].length !== 1)
45
+ throw new StorageOperationError('D1 API reported failure or invalid result count');
46
+ const result = envelope['result'][0];
47
+ if (!result || result['success'] !== true)
48
+ throw new StorageOperationError('D1 query reported failure');
49
+ return sqlRows(result['results'] ?? []);
50
+ }
51
+ }
52
+ export class NodeD1IndexStore extends D1IndexStore {
53
+ constructor(options) {
54
+ super(new RestD1Transport(options));
55
+ }
56
+ }
57
+ //# sourceMappingURL=d1.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d1.js","sourceRoot":"","sources":["../../src/node/d1.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n StorageAccessError,\n StorageOperationError,\n StorageUnavailableError,\n} from '../core/errors.js';\nimport { text } from '../core/model.js';\nimport { D1IndexStore, sqlRows } from '../storage/d1.js';\nimport type { D1Transport, SqlResult, SqlStatement } from '../storage/d1.js';\n\nexport interface NodeD1Options {\n readonly accountId: string;\n readonly databaseId: string;\n readonly apiToken: string;\n readonly fetch?: typeof globalThis.fetch;\n}\nclass RestD1Transport implements D1Transport {\n readonly #endpoint: string;\n readonly #token: string;\n readonly #fetch: typeof globalThis.fetch;\n constructor(options: NodeD1Options) {\n this.#endpoint = `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(text(options.accountId, 'accountId'))}/d1/database/${encodeURIComponent(text(options.databaseId, 'databaseId'))}/query`;\n this.#token = text(options.apiToken, 'apiToken');\n this.#fetch = options.fetch ?? globalThis.fetch;\n }\n async execute(statement: SqlStatement): Promise<SqlResult> {\n let response: Response;\n try {\n response = await this.#fetch(this.#endpoint, {\n method: 'POST',\n redirect: 'error',\n headers: { Authorization: `Bearer ${this.#token}`, 'Content-Type': 'application/json' },\n body: JSON.stringify({ sql: statement.sql, params: statement.params }),\n });\n } catch {\n throw new StorageUnavailableError('D1 request failed before receiving a response');\n }\n if (response.status === 401 || response.status === 403)\n throw new StorageAccessError(`D1 access denied with HTTP status ${response.status}`);\n if (response.status === 429 || response.status >= 500)\n throw new StorageUnavailableError(`D1 unavailable with HTTP status ${response.status}`);\n if (!response.ok)\n throw new StorageOperationError(`D1 request failed with HTTP status ${response.status}`);\n let payload: unknown;\n try {\n payload = await response.json();\n } catch {\n throw new StorageOperationError('D1 response could not be decoded');\n }\n if (!payload || typeof payload !== 'object')\n throw new StorageOperationError('D1 returned an invalid response');\n const envelope = payload as Record<string, unknown>;\n if (\n envelope['success'] !== true ||\n !Array.isArray(envelope['result']) ||\n envelope['result'].length !== 1\n )\n throw new StorageOperationError('D1 API reported failure or invalid result count');\n const result = envelope['result'][0] as Record<string, unknown> | null;\n if (!result || result['success'] !== true)\n throw new StorageOperationError('D1 query reported failure');\n return sqlRows(result['results'] ?? []);\n }\n}\nexport class NodeD1IndexStore extends D1IndexStore {\n constructor(options: NodeD1Options) {\n super(new RestD1Transport(options));\n }\n}\n"]}
@@ -0,0 +1,4 @@
1
+ export { NodeR2DocumentStore } from './r2.js';
2
+ export type { NodeR2Options } from './r2.js';
3
+ export { NodeD1IndexStore } from './d1.js';
4
+ export type { NodeD1Options } from './d1.js';
@@ -0,0 +1,3 @@
1
+ export { NodeR2DocumentStore } from './r2.js';
2
+ export { NodeD1IndexStore } from './d1.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAE9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC","sourcesContent":["export { NodeR2DocumentStore } from './r2.js';\nexport type { NodeR2Options } from './r2.js';\nexport { NodeD1IndexStore } from './d1.js';\nexport type { NodeD1Options } from './d1.js';\n"]}
@@ -0,0 +1,23 @@
1
+ import { S3Client } from '@aws-sdk/client-s3';
2
+ import { StoredDocument } from '../core/model.js';
3
+ import type { DocumentKey, DocumentPage, DocumentStore } from '../core/model.js';
4
+ export type NodeR2Options = {
5
+ readonly bucket: string;
6
+ } & ({
7
+ readonly client: S3Client;
8
+ } | {
9
+ readonly endpoint: string;
10
+ readonly accessKeyId: string;
11
+ readonly secretAccessKey: string;
12
+ readonly region?: string;
13
+ });
14
+ export declare class NodeR2DocumentStore implements DocumentStore {
15
+ #private;
16
+ constructor(options: NodeR2Options);
17
+ put(documentKey: DocumentKey, document: StoredDocument): Promise<void>;
18
+ get(documentKey: DocumentKey): Promise<StoredDocument>;
19
+ delete(documentKey: DocumentKey): Promise<void>;
20
+ list(collection: string, cursor: string | undefined, limit: number): Promise<DocumentPage>;
21
+ /** Destroys only an internally created client. Borrowed clients stay open. */
22
+ close(): void;
23
+ }
@@ -0,0 +1,115 @@
1
+ import { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectCommand, ListObjectsV2Command, } from '@aws-sdk/client-s3';
2
+ import { DocumentNotFoundError, StorageAccessError, StorageOperationError, StorageUnavailableError, } from '../core/errors.js';
3
+ import { positive, StoredDocument, text } from '../core/model.js';
4
+ import { collectionPrefix, documentKeyFromObject, objectKey } from '../storage/r2-key.js';
5
+ function status(cause) {
6
+ if (!cause || typeof cause !== 'object')
7
+ return undefined;
8
+ return cause.$metadata?.httpStatusCode;
9
+ }
10
+ function missing(cause) {
11
+ return status(cause) === 404 || (cause instanceof Error && cause.name === 'NoSuchKey');
12
+ }
13
+ function failure(cause) {
14
+ const code = status(cause);
15
+ if (code === 401 || code === 403)
16
+ return new StorageAccessError('R2 access denied');
17
+ if (code === 429 || (code !== undefined && code >= 500))
18
+ return new StorageUnavailableError('R2 is unavailable');
19
+ return new StorageOperationError('R2 operation failed');
20
+ }
21
+ export class NodeR2DocumentStore {
22
+ #client;
23
+ #bucket;
24
+ #ownsClient;
25
+ #closed = false;
26
+ constructor(options) {
27
+ this.#bucket = text(options.bucket, 'bucket');
28
+ this.#ownsClient = !('client' in options);
29
+ if ('client' in options)
30
+ this.#client = options.client;
31
+ else {
32
+ const endpoint = new URL(options.endpoint);
33
+ if (endpoint.protocol !== 'https:' ||
34
+ endpoint.username ||
35
+ endpoint.password ||
36
+ endpoint.search ||
37
+ endpoint.hash)
38
+ throw new TypeError('R2 endpoint must be an HTTPS URL without credentials, query or fragment');
39
+ this.#client = new S3Client({
40
+ endpoint: endpoint.href,
41
+ region: options.region ?? 'auto',
42
+ forcePathStyle: true,
43
+ credentials: {
44
+ accessKeyId: text(options.accessKeyId, 'accessKeyId'),
45
+ secretAccessKey: text(options.secretAccessKey, 'secretAccessKey'),
46
+ },
47
+ maxAttempts: 1,
48
+ requestChecksumCalculation: 'WHEN_REQUIRED',
49
+ responseChecksumValidation: 'WHEN_REQUIRED',
50
+ });
51
+ }
52
+ }
53
+ async put(documentKey, document) {
54
+ const input = { Bucket: this.#bucket, Key: objectKey(documentKey), Body: document.content() };
55
+ try {
56
+ await this.#client.send(new PutObjectCommand(input));
57
+ }
58
+ catch (cause) {
59
+ throw failure(cause);
60
+ }
61
+ }
62
+ async get(documentKey) {
63
+ const input = { Bucket: this.#bucket, Key: objectKey(documentKey) };
64
+ try {
65
+ const result = await this.#client.send(new GetObjectCommand(input));
66
+ if (!result.Body)
67
+ throw new StorageOperationError('R2 get returned no body');
68
+ return new StoredDocument(await result.Body.transformToByteArray());
69
+ }
70
+ catch (cause) {
71
+ if (missing(cause))
72
+ throw new DocumentNotFoundError(documentKey);
73
+ throw failure(cause);
74
+ }
75
+ }
76
+ async delete(documentKey) {
77
+ const input = { Bucket: this.#bucket, Key: objectKey(documentKey) };
78
+ try {
79
+ await this.#client.send(new DeleteObjectCommand(input));
80
+ }
81
+ catch (cause) {
82
+ if (!missing(cause))
83
+ throw failure(cause);
84
+ }
85
+ }
86
+ async list(collection, cursor, limit) {
87
+ const input = {
88
+ Bucket: this.#bucket,
89
+ Prefix: collectionPrefix(collection),
90
+ MaxKeys: positive(limit, 'limit'),
91
+ ...(cursor !== undefined ? { ContinuationToken: text(cursor, 'cursor') } : {}),
92
+ };
93
+ try {
94
+ const result = await this.#client.send(new ListObjectsV2Command(input));
95
+ const documentKeys = (result.Contents ?? []).map((item) => documentKeyFromObject(collection, item.Key ?? ''));
96
+ if (documentKeys.length > limit)
97
+ throw new StorageOperationError('R2 listing exceeded page limit');
98
+ const nextCursor = result.NextContinuationToken;
99
+ if (result.IsTruncated && !nextCursor)
100
+ throw new StorageOperationError('R2 listing is truncated without a cursor');
101
+ return { documentKeys, ...(nextCursor ? { nextCursor } : {}) };
102
+ }
103
+ catch (cause) {
104
+ throw failure(cause);
105
+ }
106
+ }
107
+ /** Destroys only an internally created client. Borrowed clients stay open. */
108
+ close() {
109
+ if (this.#ownsClient && !this.#closed) {
110
+ this.#closed = true;
111
+ this.#client.destroy();
112
+ }
113
+ }
114
+ }
115
+ //# sourceMappingURL=r2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"r2.js","sourceRoot":"","sources":["../../src/node/r2.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n} from '@aws-sdk/client-s3';\nimport {\n DocumentNotFoundError,\n StorageAccessError,\n StorageOperationError,\n StorageUnavailableError,\n} from '../core/errors.js';\nimport { positive, StoredDocument, text } from '../core/model.js';\nimport type { DocumentKey, DocumentPage, DocumentStore } from '../core/model.js';\nimport { collectionPrefix, documentKeyFromObject, objectKey } from '../storage/r2-key.js';\n\nexport type NodeR2Options = { readonly bucket: string } & (\n | { readonly client: S3Client }\n | {\n readonly endpoint: string;\n readonly accessKeyId: string;\n readonly secretAccessKey: string;\n readonly region?: string;\n }\n);\nfunction status(cause: unknown): number | undefined {\n if (!cause || typeof cause !== 'object') return undefined;\n return (cause as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode;\n}\nfunction missing(cause: unknown): boolean {\n return status(cause) === 404 || (cause instanceof Error && cause.name === 'NoSuchKey');\n}\nfunction failure(cause: unknown): Error {\n const code = status(cause);\n if (code === 401 || code === 403) return new StorageAccessError('R2 access denied');\n if (code === 429 || (code !== undefined && code >= 500))\n return new StorageUnavailableError('R2 is unavailable');\n return new StorageOperationError('R2 operation failed');\n}\nexport class NodeR2DocumentStore implements DocumentStore {\n readonly #client: S3Client;\n readonly #bucket: string;\n readonly #ownsClient: boolean;\n #closed = false;\n constructor(options: NodeR2Options) {\n this.#bucket = text(options.bucket, 'bucket');\n this.#ownsClient = !('client' in options);\n if ('client' in options) this.#client = options.client;\n else {\n const endpoint = new URL(options.endpoint);\n if (\n endpoint.protocol !== 'https:' ||\n endpoint.username ||\n endpoint.password ||\n endpoint.search ||\n endpoint.hash\n )\n throw new TypeError(\n 'R2 endpoint must be an HTTPS URL without credentials, query or fragment',\n );\n this.#client = new S3Client({\n endpoint: endpoint.href,\n region: options.region ?? 'auto',\n forcePathStyle: true,\n credentials: {\n accessKeyId: text(options.accessKeyId, 'accessKeyId'),\n secretAccessKey: text(options.secretAccessKey, 'secretAccessKey'),\n },\n maxAttempts: 1,\n requestChecksumCalculation: 'WHEN_REQUIRED',\n responseChecksumValidation: 'WHEN_REQUIRED',\n });\n }\n }\n async put(documentKey: DocumentKey, document: StoredDocument): Promise<void> {\n const input = { Bucket: this.#bucket, Key: objectKey(documentKey), Body: document.content() };\n try {\n await this.#client.send(new PutObjectCommand(input));\n } catch (cause) {\n throw failure(cause);\n }\n }\n async get(documentKey: DocumentKey): Promise<StoredDocument> {\n const input = { Bucket: this.#bucket, Key: objectKey(documentKey) };\n try {\n const result = await this.#client.send(new GetObjectCommand(input));\n if (!result.Body) throw new StorageOperationError('R2 get returned no body');\n return new StoredDocument(await result.Body.transformToByteArray());\n } catch (cause) {\n if (missing(cause)) throw new DocumentNotFoundError(documentKey);\n throw failure(cause);\n }\n }\n async delete(documentKey: DocumentKey): Promise<void> {\n const input = { Bucket: this.#bucket, Key: objectKey(documentKey) };\n try {\n await this.#client.send(new DeleteObjectCommand(input));\n } catch (cause) {\n if (!missing(cause)) throw failure(cause);\n }\n }\n async list(collection: string, cursor: string | undefined, limit: number): Promise<DocumentPage> {\n const input = {\n Bucket: this.#bucket,\n Prefix: collectionPrefix(collection),\n MaxKeys: positive(limit, 'limit'),\n ...(cursor !== undefined ? { ContinuationToken: text(cursor, 'cursor') } : {}),\n };\n try {\n const result = await this.#client.send(new ListObjectsV2Command(input));\n const documentKeys = (result.Contents ?? []).map((item) =>\n documentKeyFromObject(collection, item.Key ?? ''),\n );\n if (documentKeys.length > limit)\n throw new StorageOperationError('R2 listing exceeded page limit');\n const nextCursor = result.NextContinuationToken;\n if (result.IsTruncated && !nextCursor)\n throw new StorageOperationError('R2 listing is truncated without a cursor');\n return { documentKeys, ...(nextCursor ? { nextCursor } : {}) };\n } catch (cause) {\n throw failure(cause);\n }\n }\n /** Destroys only an internally created client. Borrowed clients stay open. */\n close(): void {\n if (this.#ownsClient && !this.#closed) {\n this.#closed = true;\n this.#client.destroy();\n }\n }\n}\n"]}
@@ -0,0 +1,28 @@
1
+ import type { CollectionMetadata, DocumentKey, IndexEntry, IndexQuery, IndexStore, IndexPage } from '../core/model.js';
2
+ export interface SqlStatement {
3
+ readonly sql: string;
4
+ readonly params: readonly (string | number)[];
5
+ }
6
+ export interface SqlResult {
7
+ readonly rows: readonly Readonly<Record<string, unknown>>[];
8
+ }
9
+ export interface D1Transport {
10
+ execute(statement: SqlStatement): Promise<SqlResult>;
11
+ }
12
+ export declare function compileUpsert(metadata: CollectionMetadata, entry: IndexEntry): SqlStatement;
13
+ export declare function compileQuery(metadata: CollectionMetadata, query: IndexQuery): SqlStatement;
14
+ export declare function queryPage(metadata: CollectionMetadata, query: IndexQuery, result: SqlResult): IndexPage;
15
+ /** Shared D1 engine. Transports supply only prepared SQL execution. */
16
+ export declare class D1IndexStore implements IndexStore {
17
+ #private;
18
+ private readonly transport;
19
+ constructor(transport: D1Transport);
20
+ initialize(input: CollectionMetadata): Promise<void>;
21
+ upsert(entry: IndexEntry): Promise<void>;
22
+ query(query: IndexQuery): Promise<IndexPage>;
23
+ delete(documentKey: DocumentKey): Promise<void>;
24
+ clear(collection: string): Promise<void>;
25
+ private registration;
26
+ }
27
+ /** Validate transport rows without accepting arrays, nulls, or primitive values. */
28
+ export declare function sqlRows(value: unknown): SqlResult;
@@ -0,0 +1,301 @@
1
+ import { StorageOperationError } from '../core/errors.js';
2
+ import { indexValue, key, positive } from '../core/model.js';
3
+ import { identifier, validateMetadata } from '../core/schema.js';
4
+ const quote = (name) => `"${identifier(name)}"`;
5
+ const sqlTypes = {
6
+ string: 'TEXT',
7
+ long: 'INTEGER',
8
+ double: 'REAL',
9
+ boolean: 'INTEGER',
10
+ };
11
+ const defaults = {
12
+ string: "''",
13
+ long: '0',
14
+ double: '0.0',
15
+ boolean: '0',
16
+ };
17
+ function fieldType(metadata, name) {
18
+ const field = metadata.fields.find((f) => f.name === name);
19
+ if (!field)
20
+ throw new TypeError(`field is not indexed: ${name}`);
21
+ return field.type;
22
+ }
23
+ function compatible(type, value) {
24
+ if (!value || value.type !== type)
25
+ throw new TypeError(`index value type does not match ${type}`);
26
+ return indexValue(type, value.value);
27
+ }
28
+ function parameter(params, value) {
29
+ if (value.type === 'timestamp')
30
+ throw new TypeError('D1 does not support timestamp indexes');
31
+ params.push(value.type === 'long'
32
+ ? value.value.toString()
33
+ : value.type === 'boolean'
34
+ ? Number(value.value)
35
+ : value.value);
36
+ const placeholder = `?${params.length}`;
37
+ return value.type === 'long' ? `CAST(${placeholder} AS INTEGER)` : placeholder;
38
+ }
39
+ function sameCollection(metadata, collection) {
40
+ if (metadata.collection !== collection)
41
+ throw new TypeError('collection does not match initialized metadata');
42
+ }
43
+ export function compileUpsert(metadata, entry) {
44
+ sameCollection(metadata, entry.documentKey.collection);
45
+ key(entry.documentKey.collection, entry.documentKey.id);
46
+ for (const field of Object.keys(entry.values))
47
+ fieldType(metadata, field);
48
+ const params = [entry.documentKey.id];
49
+ const columns = ['"document_id"', ...metadata.fields.map((f) => quote(f.name))];
50
+ const placeholders = [
51
+ '?1',
52
+ ...metadata.fields.map((f) => parameter(params, compatible(f.type, entry.values[f.name]))),
53
+ ];
54
+ const conflict = metadata.fields.length
55
+ ? 'DO UPDATE SET ' +
56
+ metadata.fields.map((f) => `${quote(f.name)} = excluded.${quote(f.name)}`).join(', ')
57
+ : 'DO NOTHING';
58
+ return {
59
+ sql: `INSERT INTO ${quote(metadata.collection)} (${columns.join(', ')}) VALUES (${placeholders.join(', ')}) ON CONFLICT("document_id") ${conflict}`,
60
+ params,
61
+ };
62
+ }
63
+ export function compileQuery(metadata, query) {
64
+ sameCollection(metadata, query.collection);
65
+ positive(query.limit, 'limit');
66
+ const params = [];
67
+ const conditions = query.filters.map((filter) => {
68
+ const type = fieldType(metadata, filter.field);
69
+ if (!['=', '<>', '>', '>=', '<', '<='].includes(filter.operator))
70
+ throw new TypeError('unsupported comparison');
71
+ if (type === 'boolean' && filter.operator !== '=' && filter.operator !== '<>')
72
+ throw new TypeError('boolean supports only equality comparisons');
73
+ return `${quote(filter.field)} ${filter.operator} ${parameter(params, compatible(type, filter.value))}`;
74
+ });
75
+ const sort = query.sort;
76
+ let sortType;
77
+ if (sort) {
78
+ sortType = fieldType(metadata, sort.field);
79
+ if (sort.direction !== 'ASC' && sort.direction !== 'DESC')
80
+ throw new TypeError('invalid sort direction');
81
+ }
82
+ const direction = sort?.direction ?? 'ASC';
83
+ if (query.cursor) {
84
+ const cursor = query.cursor;
85
+ sameCollection(metadata, cursor.lastDocumentKey.collection);
86
+ key(cursor.lastDocumentKey.collection, cursor.lastDocumentKey.id);
87
+ if (!sort) {
88
+ if (cursor.sortValue !== undefined)
89
+ throw new TypeError('unsorted cursor must not include sort value');
90
+ conditions.push(`"document_id" > ${parameter(params, { type: 'string', value: cursor.lastDocumentKey.id })}`);
91
+ }
92
+ else {
93
+ const value = parameter(params, compatible(sortType, cursor.sortValue));
94
+ const id = parameter(params, { type: 'string', value: cursor.lastDocumentKey.id });
95
+ const op = direction === 'ASC' ? '>' : '<';
96
+ conditions.push(`(${quote(sort.field)} ${op} ${value} OR (${quote(sort.field)} = ${value} AND "document_id" ${op} ${id}))`);
97
+ }
98
+ }
99
+ const projection = sort
100
+ ? ', ' +
101
+ (sortType === 'long'
102
+ ? `CAST(${quote(sort.field)} AS TEXT) AS ${quote(sort.field)}`
103
+ : quote(sort.field))
104
+ : '';
105
+ const order = sort
106
+ ? `${quote(metadata.collection)}.${quote(sort.field)} ${direction}, "document_id" ${direction}`
107
+ : '"document_id" ASC';
108
+ params.push(query.limit + 1);
109
+ return {
110
+ sql: `SELECT "document_id"${projection} FROM ${quote(metadata.collection)}${conditions.length ? ' WHERE ' + conditions.join(' AND ') : ''} ORDER BY ${order} LIMIT ?${params.length}`,
111
+ params,
112
+ };
113
+ }
114
+ function readValue(type, value) {
115
+ try {
116
+ if (type === 'long') {
117
+ if (typeof value !== 'string' || !/^-?\d+$/.test(value))
118
+ throw new TypeError();
119
+ return indexValue(type, BigInt(value));
120
+ }
121
+ if (type === 'boolean') {
122
+ if (value !== 0 && value !== 1)
123
+ throw new TypeError();
124
+ return indexValue(type, value === 1);
125
+ }
126
+ return indexValue(type, value);
127
+ }
128
+ catch {
129
+ throw new StorageOperationError('D1 returned an invalid indexed value');
130
+ }
131
+ }
132
+ export function queryPage(metadata, query, result) {
133
+ const rows = result.rows.slice(0, query.limit);
134
+ const keys = rows.map((row) => {
135
+ try {
136
+ return key(metadata.collection, row['document_id']);
137
+ }
138
+ catch {
139
+ throw new StorageOperationError('D1 returned an invalid document_id');
140
+ }
141
+ });
142
+ // Validate each returned sort value, including final pages.
143
+ const sortValues = query.sort
144
+ ? rows.map((row) => readValue(fieldType(metadata, query.sort.field), row[query.sort.field]))
145
+ : [];
146
+ const hasMore = result.rows.length > query.limit && keys.length > 0;
147
+ return {
148
+ documentKeys: keys,
149
+ ...(hasMore
150
+ ? {
151
+ nextCursor: {
152
+ lastDocumentKey: keys.at(-1),
153
+ ...(query.sort ? { sortValue: sortValues.at(-1) } : {}),
154
+ },
155
+ }
156
+ : {}),
157
+ };
158
+ }
159
+ function stringCell(row, name) {
160
+ const value = row[name];
161
+ if (typeof value !== 'string')
162
+ throw new StorageOperationError(`D1 schema returned invalid ${name}`);
163
+ return value;
164
+ }
165
+ function integerCell(row, name) {
166
+ const value = row[name];
167
+ if (typeof value !== 'number' || !Number.isSafeInteger(value))
168
+ throw new StorageOperationError(`D1 schema returned invalid ${name}`);
169
+ return value;
170
+ }
171
+ async function initializeSchema(transport, metadata) {
172
+ const table = quote(metadata.collection);
173
+ const execute = (sql) => transport.execute({ sql, params: [] });
174
+ const inspect = async () => {
175
+ const result = await execute(`PRAGMA table_info(${table})`);
176
+ const columns = new Map();
177
+ for (const row of result.rows) {
178
+ const name = stringCell(row, 'name');
179
+ if (columns.has(name))
180
+ throw new StorageOperationError('D1 returned duplicate columns');
181
+ columns.set(name, row);
182
+ }
183
+ return columns;
184
+ };
185
+ const validate = (columns, requireAll) => {
186
+ const id = columns.get('document_id');
187
+ if (!id ||
188
+ stringCell(id, 'type').toUpperCase() !== 'TEXT' ||
189
+ integerCell(id, 'notnull') !== 1 ||
190
+ integerCell(id, 'pk') <= 0)
191
+ throw new StorageOperationError('D1 schema has incompatible document_id');
192
+ for (const field of metadata.fields) {
193
+ const actual = columns.get(field.name);
194
+ if (!actual && !requireAll)
195
+ continue;
196
+ if (!actual ||
197
+ stringCell(actual, 'type').toUpperCase() !== sqlTypes[field.type] ||
198
+ integerCell(actual, 'notnull') !== 1 ||
199
+ integerCell(actual, 'pk') !== 0)
200
+ throw new StorageOperationError(`D1 schema has incompatible column: ${field.name}`);
201
+ }
202
+ };
203
+ let columns = await inspect();
204
+ if (columns.size === 0) {
205
+ const definitions = [
206
+ '"document_id" TEXT PRIMARY KEY NOT NULL',
207
+ ...metadata.fields.map((f) => `${quote(f.name)} ${sqlTypes[f.type]} NOT NULL`),
208
+ ];
209
+ await execute(`CREATE TABLE IF NOT EXISTS ${table} (${definitions.join(', ')})`);
210
+ columns = await inspect();
211
+ validate(columns, true);
212
+ }
213
+ else {
214
+ validate(columns, false);
215
+ const missing = metadata.fields.filter((f) => !columns.has(f.name));
216
+ if (missing.length) {
217
+ const result = await execute(`SELECT EXISTS(SELECT 1 FROM ${table} LIMIT 1) AS "has_rows"`);
218
+ if (result.rows.length !== 1)
219
+ throw new StorageOperationError('D1 row-existence query returned invalid result');
220
+ const exists = integerCell(result.rows[0], 'has_rows');
221
+ if (exists !== 0 && exists !== 1)
222
+ throw new StorageOperationError('D1 row-existence query returned invalid value');
223
+ if (exists)
224
+ throw new StorageOperationError('D1 schema requires a rebuild before adding required indexed fields');
225
+ for (const field of missing)
226
+ await execute(`ALTER TABLE ${table} ADD COLUMN ${quote(field.name)} ${sqlTypes[field.type]} NOT NULL DEFAULT ${defaults[field.type]}`);
227
+ validate(await inspect(), true);
228
+ }
229
+ }
230
+ for (const field of metadata.fields) {
231
+ const name = `idx_${metadata.collection}_${field.name}`;
232
+ const inspectNames = async () => (await execute(`PRAGMA index_list(${table})`)).rows.map((r) => stringCell(r, 'name'));
233
+ if (!(await inspectNames()).includes(name))
234
+ await execute(`CREATE INDEX IF NOT EXISTS ${quote(name)} ON ${table} (${quote(field.name)})`);
235
+ if (!(await inspectNames()).includes(name))
236
+ throw new StorageOperationError('D1 index name is unavailable');
237
+ const info = await execute(`PRAGMA index_info(${quote(name)})`);
238
+ if (info.rows.length !== 1 || stringCell(info.rows[0], 'name') !== field.name)
239
+ throw new StorageOperationError('D1 index is incompatible with indexed field');
240
+ }
241
+ }
242
+ /** Shared D1 engine. Transports supply only prepared SQL execution. */
243
+ export class D1IndexStore {
244
+ transport;
245
+ #registrations = new Map();
246
+ constructor(transport) {
247
+ this.transport = transport;
248
+ }
249
+ async initialize(input) {
250
+ const metadata = validateMetadata(input);
251
+ const existing = this.#registrations.get(metadata.collection);
252
+ if (existing) {
253
+ if (JSON.stringify(existing.metadata) !== JSON.stringify(metadata))
254
+ throw new TypeError('collection is already initialized with different metadata');
255
+ return existing.ready;
256
+ }
257
+ const ready = Promise.resolve().then(() => initializeSchema(this.transport, metadata));
258
+ this.#registrations.set(metadata.collection, { metadata, ready });
259
+ return ready;
260
+ }
261
+ async upsert(entry) {
262
+ const registration = this.registration(entry.documentKey.collection);
263
+ const statement = compileUpsert(registration.metadata, entry);
264
+ await registration.ready;
265
+ await this.transport.execute(statement);
266
+ }
267
+ async query(query) {
268
+ const registration = this.registration(query.collection);
269
+ const statement = compileQuery(registration.metadata, query);
270
+ await registration.ready;
271
+ return queryPage(registration.metadata, query, await this.transport.execute(statement));
272
+ }
273
+ async delete(documentKey) {
274
+ key(documentKey.collection, documentKey.id);
275
+ const registration = this.registration(documentKey.collection);
276
+ await registration.ready;
277
+ await this.transport.execute({
278
+ sql: `DELETE FROM ${quote(documentKey.collection)} WHERE "document_id" = ?1`,
279
+ params: [documentKey.id],
280
+ });
281
+ }
282
+ async clear(collection) {
283
+ const registration = this.registration(collection);
284
+ await registration.ready;
285
+ await this.transport.execute({ sql: `DELETE FROM ${quote(collection)}`, params: [] });
286
+ }
287
+ registration(collection) {
288
+ const registration = this.#registrations.get(collection);
289
+ if (!registration)
290
+ throw new StorageOperationError('D1 collection is not initialized');
291
+ return registration;
292
+ }
293
+ }
294
+ /** Validate transport rows without accepting arrays, nulls, or primitive values. */
295
+ export function sqlRows(value) {
296
+ if (!Array.isArray(value) ||
297
+ value.some((row) => row === null || typeof row !== 'object' || Array.isArray(row)))
298
+ throw new StorageOperationError('D1 returned invalid rows');
299
+ return { rows: value };
300
+ }
301
+ //# sourceMappingURL=d1.js.map