@puzzle-ui/storage-d1 0.0.0-stage → 0.5.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.
@@ -0,0 +1,30 @@
1
+ import { D1StorageAdapterOptions } from "./d1.types.cjs";
2
+ import { StorageAdapter, StorageBatchOperation, StorageBatchResult, StorageCapabilities, StorageCapabilityReport, StorageCollection, StorageCollectionName, StorageCondition, StorageCursorQuery, StoragePage, StorageQuery, StorageRecord, StorageUpdate } from "@puzzle-ui/storage";
3
+ export declare class D1StorageAdapter implements StorageAdapter {
4
+ readonly capabilities: StorageCapabilities;
5
+ private readonly database;
6
+ private readonly prefix;
7
+ constructor(options: D1StorageAdapterOptions);
8
+ getCapabilities(): StorageCapabilityReport;
9
+ private table;
10
+ private prepare;
11
+ private run;
12
+ private first;
13
+ private all;
14
+ private insertStatement;
15
+ registerCollection<T extends StorageRecord>(definition: StorageCollection<T>): Promise<void>;
16
+ findById<T extends StorageRecord>(collection: StorageCollectionName, id: string): Promise<T | null>;
17
+ findOne<T extends StorageRecord>(collection: StorageCollectionName, query: StorageQuery<T>): Promise<T | null>;
18
+ findMany<T extends StorageRecord>(collection: StorageCollectionName, query?: StorageQuery<T>): Promise<readonly T[]>;
19
+ findPage<T extends StorageRecord>(collection: StorageCollectionName, query: StorageCursorQuery<T>): Promise<StoragePage<T>>;
20
+ insert<T extends StorageRecord>(collection: StorageCollectionName, record: Parameters<StorageAdapter["insert"]>[1]): Promise<T>;
21
+ upsert<T extends StorageRecord>(collection: StorageCollectionName, query: StorageQuery<T>, record: Parameters<StorageAdapter["insert"]>[1]): Promise<T>;
22
+ private buildUpdate;
23
+ update<T extends StorageRecord>(collection: StorageCollectionName, id: string, changes: StorageUpdate<T>, condition?: StorageCondition): Promise<T | null>;
24
+ updateMany<T extends StorageRecord>(collection: StorageCollectionName, query: StorageQuery<T>, changes: StorageUpdate<T>): Promise<number>;
25
+ delete<T extends StorageRecord>(collection: StorageCollectionName, id: string, condition?: StorageCondition): Promise<boolean>;
26
+ deleteMany<T extends StorageRecord>(collection: StorageCollectionName, query: StorageQuery<T>): Promise<number>;
27
+ exists<T extends StorageRecord>(collection: StorageCollectionName, query: StorageQuery<T>): Promise<boolean>;
28
+ count<T extends StorageRecord>(collection: StorageCollectionName, query?: StorageQuery<T>): Promise<number>;
29
+ atomicBatch<T extends StorageRecord>(operations: readonly StorageBatchOperation<T>[]): Promise<StorageBatchResult<T>>;
30
+ }
@@ -0,0 +1,30 @@
1
+ import { D1StorageAdapterOptions } from "./d1.types.js";
2
+ import { StorageAdapter, StorageBatchOperation, StorageBatchResult, StorageCapabilities, StorageCapabilityReport, StorageCollection, StorageCollectionName, StorageCondition, StorageCursorQuery, StoragePage, StorageQuery, StorageRecord, StorageUpdate } from "@puzzle-ui/storage";
3
+ export declare class D1StorageAdapter implements StorageAdapter {
4
+ readonly capabilities: StorageCapabilities;
5
+ private readonly database;
6
+ private readonly prefix;
7
+ constructor(options: D1StorageAdapterOptions);
8
+ getCapabilities(): StorageCapabilityReport;
9
+ private table;
10
+ private prepare;
11
+ private run;
12
+ private first;
13
+ private all;
14
+ private insertStatement;
15
+ registerCollection<T extends StorageRecord>(definition: StorageCollection<T>): Promise<void>;
16
+ findById<T extends StorageRecord>(collection: StorageCollectionName, id: string): Promise<T | null>;
17
+ findOne<T extends StorageRecord>(collection: StorageCollectionName, query: StorageQuery<T>): Promise<T | null>;
18
+ findMany<T extends StorageRecord>(collection: StorageCollectionName, query?: StorageQuery<T>): Promise<readonly T[]>;
19
+ findPage<T extends StorageRecord>(collection: StorageCollectionName, query: StorageCursorQuery<T>): Promise<StoragePage<T>>;
20
+ insert<T extends StorageRecord>(collection: StorageCollectionName, record: Parameters<StorageAdapter["insert"]>[1]): Promise<T>;
21
+ upsert<T extends StorageRecord>(collection: StorageCollectionName, query: StorageQuery<T>, record: Parameters<StorageAdapter["insert"]>[1]): Promise<T>;
22
+ private buildUpdate;
23
+ update<T extends StorageRecord>(collection: StorageCollectionName, id: string, changes: StorageUpdate<T>, condition?: StorageCondition): Promise<T | null>;
24
+ updateMany<T extends StorageRecord>(collection: StorageCollectionName, query: StorageQuery<T>, changes: StorageUpdate<T>): Promise<number>;
25
+ delete<T extends StorageRecord>(collection: StorageCollectionName, id: string, condition?: StorageCondition): Promise<boolean>;
26
+ deleteMany<T extends StorageRecord>(collection: StorageCollectionName, query: StorageQuery<T>): Promise<number>;
27
+ exists<T extends StorageRecord>(collection: StorageCollectionName, query: StorageQuery<T>): Promise<boolean>;
28
+ count<T extends StorageRecord>(collection: StorageCollectionName, query?: StorageQuery<T>): Promise<number>;
29
+ atomicBatch<T extends StorageRecord>(operations: readonly StorageBatchOperation<T>[]): Promise<StorageBatchResult<T>>;
30
+ }
@@ -0,0 +1,531 @@
1
+ import { StorageAtomicityError, StorageConflictError, StorageDuplicateError, StorageValidationError, validateQuery } from "@puzzle-ui/storage";
2
+ // src/d1.adapter.ts
3
+ const DATE_MARKER = "__puzzle_storage_date__";
4
+ const serialize = (value) => JSON.stringify(value, (_key, current) => current instanceof Date ? { [DATE_MARKER]: current.toISOString() } : current);
5
+ const deserialize = (value) => JSON.parse(value, (_key, current) => {
6
+ if (typeof current === "object" && current !== null && DATE_MARKER in current) {
7
+ const date = current[DATE_MARKER];
8
+ if (typeof date !== "string") throw new StorageValidationError("Invalid serialized D1 date.");
9
+ return new Date(date);
10
+ }
11
+ return current;
12
+ });
13
+ const encodeCursor = (payload) => ({ value: btoa(encodeURIComponent(JSON.stringify(payload))) });
14
+ const decodeCursor = (cursor) => {
15
+ try {
16
+ return JSON.parse(decodeURIComponent(atob(cursor.value)));
17
+ } catch {
18
+ throw new StorageValidationError("Invalid D1 cursor.");
19
+ }
20
+ };
21
+ const quoteIdentifier = (value) => {
22
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(value)) throw new StorageValidationError(`Invalid SQL identifier: ${value}`);
23
+ return `"${value}"`;
24
+ };
25
+ const jsonPath = (field) => {
26
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field)) throw new StorageValidationError(`Unsupported JSON field name: ${field}`);
27
+ return `$."${field}"`;
28
+ };
29
+ const sqlField = (field) => {
30
+ if (field === "id") return "id";
31
+ if (field === "createdAt") return "created_at";
32
+ if (field === "updatedAt") return "updated_at";
33
+ if (field === "revision") return "revision";
34
+ return `json_extract(data, '${jsonPath(field)}')`;
35
+ };
36
+ const sqlValue = (value) => value instanceof Date ? value.toISOString() : value;
37
+ const buildComparison = (field, comparison) => {
38
+ const column = sqlField(field);
39
+ const value = comparison.value;
40
+ switch (comparison.operator) {
41
+ case "eq": return value === null ? {
42
+ sql: `${column} IS NULL`,
43
+ values: []
44
+ } : {
45
+ sql: `${column} = ?`,
46
+ values: [sqlValue(value)]
47
+ };
48
+ case "ne": return value === null ? {
49
+ sql: `${column} IS NOT NULL`,
50
+ values: []
51
+ } : {
52
+ sql: `${column} != ?`,
53
+ values: [sqlValue(value)]
54
+ };
55
+ case "gt": return {
56
+ sql: `${column} > ?`,
57
+ values: [sqlValue(value)]
58
+ };
59
+ case "gte": return {
60
+ sql: `${column} >= ?`,
61
+ values: [sqlValue(value)]
62
+ };
63
+ case "lt": return {
64
+ sql: `${column} < ?`,
65
+ values: [sqlValue(value)]
66
+ };
67
+ case "lte": return {
68
+ sql: `${column} <= ?`,
69
+ values: [sqlValue(value)]
70
+ };
71
+ case "exists": return {
72
+ sql: comparison.value ? `${column} IS NOT NULL` : `${column} IS NULL`,
73
+ values: []
74
+ };
75
+ case "in":
76
+ case "notIn": {
77
+ const values = value;
78
+ if (values.length === 0) return {
79
+ sql: comparison.operator === "in" ? "0 = 1" : "1 = 1",
80
+ values: []
81
+ };
82
+ const placeholders = values.map(() => "?").join(", ");
83
+ return {
84
+ sql: `${column} ${comparison.operator === "in" ? "IN" : "NOT IN"} (${placeholders})`,
85
+ values: values.map(sqlValue)
86
+ };
87
+ }
88
+ default: throw new StorageValidationError(`Unsupported query operator: ${comparison.operator}`);
89
+ }
90
+ };
91
+ const buildWhere = (where) => {
92
+ if (!where) return {
93
+ sql: "1 = 1",
94
+ values: []
95
+ };
96
+ const parts = [];
97
+ const values = [];
98
+ for (const [field, comparison] of Object.entries(where)) {
99
+ if (field === "and" || field === "or") {
100
+ const nested = comparison.map((item) => buildWhere(item));
101
+ if (nested.length === 0) continue;
102
+ parts.push(`(${nested.map((item) => item.sql).join(field === "and" ? " AND " : " OR ")})`);
103
+ for (const item of nested) values.push(...item.values);
104
+ continue;
105
+ }
106
+ const value = comparison;
107
+ if (typeof value !== "object" || value === null || !("operator" in value) || typeof value.operator !== "string" || !("value" in value)) throw new StorageValidationError(`Invalid query comparison for field: ${field}`);
108
+ const result = buildComparison(field, {
109
+ operator: value.operator,
110
+ value: value.value
111
+ });
112
+ parts.push(result.sql);
113
+ values.push(...result.values);
114
+ }
115
+ return {
116
+ sql: parts.length ? parts.join(" AND ") : "1 = 1",
117
+ values
118
+ };
119
+ };
120
+ const buildOrderBy = (orderBy) => {
121
+ const items = (orderBy ?? []).map((item) => `${sqlField(String(item.field))} ${item.direction === "desc" ? "DESC" : "ASC"}`);
122
+ if (!items.some((item) => item.startsWith("id "))) items.push("id ASC");
123
+ return items.join(", ");
124
+ };
125
+ const buildCursorWhere = (orderBy, values) => {
126
+ const fields = [...orderBy.map((item) => ({
127
+ field: String(item.field),
128
+ direction: item.direction
129
+ }))];
130
+ if (!fields.some((item) => item.field === "id")) fields.push({
131
+ field: "id",
132
+ direction: "asc"
133
+ });
134
+ const branches = [];
135
+ const parameters = [];
136
+ for (const [index, field] of fields.entries()) {
137
+ const conditions = [];
138
+ for (const [previous, prefix] of fields.slice(0, index).entries()) {
139
+ conditions.push(`${sqlField(prefix.field)} = ?`);
140
+ parameters.push(sqlValue(values[previous]));
141
+ }
142
+ const operator = field.direction === "desc" ? "<" : ">";
143
+ conditions.push(`${sqlField(field.field)} ${operator} ?`);
144
+ parameters.push(sqlValue(values[index]));
145
+ branches.push(`(${conditions.join(" AND ")})`);
146
+ }
147
+ return {
148
+ sql: `(${branches.join(" OR ")})`,
149
+ values: parameters
150
+ };
151
+ };
152
+ const cleanChanges = (changes) => {
153
+ const result = { ...changes };
154
+ delete result.id;
155
+ delete result.createdAt;
156
+ delete result.updatedAt;
157
+ delete result.revision;
158
+ return result;
159
+ };
160
+ const toRecord = (row) => {
161
+ return {
162
+ ...deserialize(row.data),
163
+ id: row.id,
164
+ createdAt: new Date(row.created_at),
165
+ updatedAt: new Date(row.updated_at),
166
+ revision: row.revision
167
+ };
168
+ };
169
+ const getChanges = (result) => Number(result.meta?.changes ?? 0);
170
+ const mapD1Error = (error) => {
171
+ const message = error instanceof Error ? error.message.toLowerCase() : "";
172
+ if (message.includes("unique constraint") || message.includes("primary key")) throw new StorageDuplicateError("d1_unique_constraint");
173
+ throw error;
174
+ };
175
+ var D1StorageAdapter = class {
176
+ capabilities = {
177
+ transactions: true,
178
+ atomicBatch: true,
179
+ conditionalWrites: true,
180
+ uniqueConstraints: true,
181
+ upsert: true,
182
+ cursorPagination: true,
183
+ migrations: true,
184
+ ttlIndexes: false
185
+ };
186
+ database;
187
+ prefix;
188
+ constructor(options) {
189
+ this.database = options.database;
190
+ this.prefix = options.tablePrefix ?? "puzzle_";
191
+ }
192
+ getCapabilities() {
193
+ return {
194
+ capabilities: this.capabilities,
195
+ limitations: ["D1 does not provide native TTL indexes. Expiration cleanup must be handled by an application or scheduled job."]
196
+ };
197
+ }
198
+ table(collection) {
199
+ return quoteIdentifier(`${this.prefix}${collection}`);
200
+ }
201
+ prepare(sql, values = []) {
202
+ return this.database.prepare(sql).bind(...values);
203
+ }
204
+ async run(sql, values = []) {
205
+ return this.prepare(sql, values).run();
206
+ }
207
+ async first(sql, values = []) {
208
+ return this.prepare(sql, values).first();
209
+ }
210
+ async all(sql, values = []) {
211
+ return (await this.prepare(sql, values).all()).results ?? [];
212
+ }
213
+ insertStatement(collection, record) {
214
+ const input = record;
215
+ const now = /* @__PURE__ */ new Date();
216
+ const createdAt = input.createdAt ?? now;
217
+ const updatedAt = input.updatedAt ?? now;
218
+ const revision = input.revision ?? 1;
219
+ const { id, createdAt: _createdAt, updatedAt: _updatedAt, revision: _revision, ...data } = input;
220
+ const result = {
221
+ id,
222
+ createdAt,
223
+ updatedAt,
224
+ revision,
225
+ ...data
226
+ };
227
+ return {
228
+ sql: `INSERT INTO ${this.table(collection)}
229
+ (id, created_at, updated_at, revision, data)
230
+ VALUES (?, ?, ?, ?, ?)`,
231
+ values: [
232
+ id,
233
+ createdAt.toISOString(),
234
+ updatedAt.toISOString(),
235
+ revision,
236
+ serialize(data)
237
+ ],
238
+ record: result
239
+ };
240
+ }
241
+ async registerCollection(definition) {
242
+ const table = this.table(definition.name);
243
+ await this.run(`
244
+ CREATE TABLE IF NOT EXISTS ${table} (
245
+ id TEXT PRIMARY KEY NOT NULL,
246
+ created_at TEXT NOT NULL,
247
+ updated_at TEXT NOT NULL,
248
+ revision INTEGER NOT NULL CHECK (revision > 0),
249
+ data TEXT NOT NULL CHECK (json_valid(data))
250
+ )
251
+ `);
252
+ for (const index of definition.indexes ?? []) {
253
+ const fields = index.fields.map((field) => {
254
+ return sqlField(String(field));
255
+ });
256
+ const unique = index.unique ? "UNIQUE " : "";
257
+ const name = quoteIdentifier(`${this.prefix}${definition.name}_${index.name}`);
258
+ await this.run(`
259
+ CREATE ${unique}INDEX IF NOT EXISTS ${name}
260
+ ON ${table} (${fields.join(", ")})
261
+ `);
262
+ }
263
+ }
264
+ async findById(collection, id) {
265
+ const row = await this.first(`SELECT * FROM ${this.table(collection)} WHERE id = ? LIMIT 1`, [id]);
266
+ return row ? toRecord(row) : null;
267
+ }
268
+ async findOne(collection, query) {
269
+ validateQuery(query);
270
+ const where = buildWhere(query.where);
271
+ const row = await this.first(`SELECT * FROM ${this.table(collection)}
272
+ WHERE ${where.sql}
273
+ ORDER BY ${buildOrderBy(query.orderBy)}
274
+ LIMIT 1`, where.values);
275
+ return row ? toRecord(row) : null;
276
+ }
277
+ async findMany(collection, query = {}) {
278
+ validateQuery(query);
279
+ const where = buildWhere(query.where);
280
+ const values = [...where.values];
281
+ let pagination = "";
282
+ if (query.limit !== void 0) {
283
+ pagination += " LIMIT ?";
284
+ values.push(query.limit);
285
+ } else if (query.offset !== void 0) pagination += " LIMIT -1";
286
+ if (query.offset !== void 0) {
287
+ pagination += " OFFSET ?";
288
+ values.push(query.offset);
289
+ }
290
+ return (await this.all(`SELECT * FROM ${this.table(collection)}
291
+ WHERE ${where.sql}
292
+ ORDER BY ${buildOrderBy(query.orderBy)}
293
+ ${pagination}`, values)).map((row) => toRecord(row));
294
+ }
295
+ async findPage(collection, query) {
296
+ if (!Number.isInteger(query.first) || query.first < 1) throw new StorageValidationError("Cursor page size must be a positive integer.");
297
+ const orderBy = query.orderBy ?? [{
298
+ field: "id",
299
+ direction: "asc"
300
+ }];
301
+ const base = buildWhere(query.where);
302
+ let whereSql = base.sql;
303
+ const values = [...base.values];
304
+ if (query.after) {
305
+ const cursor = decodeCursor(query.after);
306
+ const after = buildCursorWhere(orderBy, cursor.values);
307
+ whereSql = `(${whereSql}) AND ${after.sql}`;
308
+ values.push(...after.values);
309
+ }
310
+ values.push(query.first + 1);
311
+ const rows = await this.all(`SELECT * FROM ${this.table(collection)}
312
+ WHERE ${whereSql}
313
+ ORDER BY ${buildOrderBy(orderBy)}
314
+ LIMIT ?`, values);
315
+ const hasNextPage = rows.length > query.first;
316
+ const items = rows.slice(0, query.first);
317
+ const last = items.at(-1);
318
+ const cursorFields = [...orderBy.map((item) => String(item.field))];
319
+ if (!cursorFields.includes("id")) cursorFields.push("id");
320
+ const nextCursor = hasNextPage && last ? encodeCursor({ values: cursorFields.map((field) => {
321
+ if (field === "id") return last.id;
322
+ if (field === "createdAt") return last.created_at;
323
+ if (field === "updatedAt") return last.updated_at;
324
+ if (field === "revision") return last.revision;
325
+ return deserialize(last.data)[field];
326
+ }) }) : null;
327
+ return {
328
+ items: items.map((row) => toRecord(row)),
329
+ hasNextPage,
330
+ nextCursor
331
+ };
332
+ }
333
+ async insert(collection, record) {
334
+ const statement = this.insertStatement(collection, record);
335
+ try {
336
+ await this.run(statement.sql, statement.values);
337
+ return statement.record;
338
+ } catch (error) {
339
+ return mapD1Error(error);
340
+ }
341
+ }
342
+ async upsert(collection, query, record) {
343
+ const existing = await this.findOne(collection, query);
344
+ if (existing) {
345
+ const changes = { ...record };
346
+ const updated = await this.update(collection, existing.id, changes);
347
+ if (!updated) throw new StorageConflictError();
348
+ return updated;
349
+ }
350
+ return this.insert(collection, record);
351
+ }
352
+ buildUpdate(collection, id, changes, condition) {
353
+ const entries = Object.entries(changes);
354
+ entries.map(([field]) => `data = json_set(data, ?, json(?))`);
355
+ const assignments = [];
356
+ if (entries.length > 0) assignments.push(`data = ${entries.reduce((expression, _entry, index) => `json_set(${expression}, ?, json(?))`, "data")}`);
357
+ assignments.push("updated_at = ?");
358
+ assignments.push("revision = revision + 1");
359
+ const where = ["id = ?"];
360
+ const values = [];
361
+ for (const [field, value] of entries) values.push(jsonPath(field), serialize(value));
362
+ values.push((/* @__PURE__ */ new Date()).toISOString(), id);
363
+ if (condition?.expectedRevision !== void 0) {
364
+ where.push("revision = ?");
365
+ values.push(condition.expectedRevision);
366
+ }
367
+ if (condition?.expectedUpdatedAt !== void 0) {
368
+ where.push("updated_at = ?");
369
+ values.push(condition.expectedUpdatedAt.toISOString());
370
+ }
371
+ return {
372
+ sql: `UPDATE ${this.table(collection)}
373
+ SET ${assignments.join(", ")}
374
+ WHERE ${where.join(" AND ")}`,
375
+ values
376
+ };
377
+ }
378
+ async update(collection, id, changes, condition) {
379
+ const clean = cleanChanges(changes);
380
+ const statement = this.buildUpdate(collection, id, clean, condition);
381
+ const result = await this.run(statement.sql, statement.values);
382
+ if (getChanges(result) === 0) {
383
+ if (condition && await this.findById(collection, id)) throw new StorageConflictError();
384
+ return null;
385
+ }
386
+ return this.findById(collection, id);
387
+ }
388
+ async updateMany(collection, query, changes) {
389
+ validateQuery(query);
390
+ const clean = cleanChanges(changes);
391
+ const entries = Object.entries(clean);
392
+ const assignments = entries.length ? `data = ${entries.reduce((expression) => `json_set(${expression}, ?, json(?))`, "data")}, ` : "";
393
+ const values = [];
394
+ for (const [field, value] of entries) values.push(jsonPath(field), serialize(value));
395
+ values.push((/* @__PURE__ */ new Date()).toISOString());
396
+ const where = buildWhere(query.where);
397
+ values.push(...where.values);
398
+ const result = await this.run(`UPDATE ${this.table(collection)}
399
+ SET ${assignments}updated_at = ?, revision = revision + 1
400
+ WHERE ${where.sql}`, values);
401
+ return getChanges(result);
402
+ }
403
+ async delete(collection, id, condition) {
404
+ const where = ["id = ?"];
405
+ const values = [id];
406
+ if (condition?.expectedRevision !== void 0) {
407
+ where.push("revision = ?");
408
+ values.push(condition.expectedRevision);
409
+ }
410
+ if (condition?.expectedUpdatedAt !== void 0) {
411
+ where.push("updated_at = ?");
412
+ values.push(condition.expectedUpdatedAt.toISOString());
413
+ }
414
+ const result = await this.run(`DELETE FROM ${this.table(collection)}
415
+ WHERE ${where.join(" AND ")}`, values);
416
+ if (getChanges(result) > 0) return true;
417
+ if (condition && await this.findById(collection, id)) throw new StorageConflictError();
418
+ return false;
419
+ }
420
+ async deleteMany(collection, query) {
421
+ validateQuery(query);
422
+ const where = buildWhere(query.where);
423
+ const result = await this.run(`DELETE FROM ${this.table(collection)} WHERE ${where.sql}`, where.values);
424
+ return getChanges(result);
425
+ }
426
+ async exists(collection, query) {
427
+ validateQuery(query);
428
+ const where = buildWhere(query.where);
429
+ return await this.first(`SELECT 1 AS found FROM ${this.table(collection)}
430
+ WHERE ${where.sql} LIMIT 1`, where.values) !== null;
431
+ }
432
+ async count(collection, query = {}) {
433
+ validateQuery(query);
434
+ const where = buildWhere(query.where);
435
+ const result = await this.first(`SELECT COUNT(*) AS total FROM ${this.table(collection)}
436
+ WHERE ${where.sql}`, where.values);
437
+ return Number(result?.total ?? 0);
438
+ }
439
+ async atomicBatch(operations) {
440
+ const assertionTable = quoteIdentifier(`${this.prefix}atomic_assertions`);
441
+ await this.run(`
442
+ CREATE TABLE IF NOT EXISTS ${assertionTable} (
443
+ value INTEGER NOT NULL CHECK (value = 1)
444
+ )
445
+ `);
446
+ const statements = [];
447
+ const resultIndexes = [];
448
+ for (const operation of operations) {
449
+ const table = this.table(operation.collection);
450
+ if (operation.type !== "insert" && (operation.condition?.expectedRevision !== void 0 || operation.condition?.expectedUpdatedAt !== void 0)) {
451
+ const conditions = ["id = ?"];
452
+ const values = [operation.id];
453
+ if (operation.condition.expectedRevision !== void 0) {
454
+ conditions.push("revision = ?");
455
+ values.push(operation.condition.expectedRevision);
456
+ }
457
+ if (operation.condition.expectedUpdatedAt !== void 0) {
458
+ conditions.push("updated_at = ?");
459
+ values.push(operation.condition.expectedUpdatedAt.toISOString());
460
+ }
461
+ statements.push(this.prepare(`INSERT INTO ${assertionTable}(value)
462
+ SELECT CASE WHEN EXISTS (
463
+ SELECT 1 FROM ${table}
464
+ WHERE ${conditions.join(" AND ")}
465
+ ) THEN 1 ELSE 0 END`, values));
466
+ statements.push(this.prepare(`DELETE FROM ${assertionTable}`));
467
+ }
468
+ if (operation.type === "insert") {
469
+ const statement = this.insertStatement(operation.collection, operation.record);
470
+ statements.push(this.prepare(statement.sql, statement.values));
471
+ resultIndexes.push(statements.length - 1);
472
+ continue;
473
+ }
474
+ if (operation.type === "update") {
475
+ const statement = this.buildUpdate(operation.collection, operation.id, cleanChanges(operation.changes), operation.condition);
476
+ statements.push(this.prepare(statement.sql, statement.values));
477
+ resultIndexes.push(statements.length - 1);
478
+ continue;
479
+ }
480
+ const conditions = ["id = ?"];
481
+ const values = [operation.id];
482
+ if (operation.condition?.expectedRevision !== void 0) {
483
+ conditions.push("revision = ?");
484
+ values.push(operation.condition.expectedRevision);
485
+ }
486
+ if (operation.condition?.expectedUpdatedAt !== void 0) {
487
+ conditions.push("updated_at = ?");
488
+ values.push(operation.condition.expectedUpdatedAt.toISOString());
489
+ }
490
+ statements.push(this.prepare(`DELETE FROM ${table} WHERE ${conditions.join(" AND ")}`, values));
491
+ resultIndexes.push(statements.length - 1);
492
+ }
493
+ try {
494
+ const batchResults = await this.database.batch(statements);
495
+ const results = [];
496
+ for (const [index, operation] of operations.entries()) {
497
+ const resultIndex = resultIndexes[index];
498
+ const result = resultIndex === void 0 ? void 0 : batchResults[resultIndex];
499
+ if (!result) throw new StorageAtomicityError("D1 batch did not return a result for every operation.");
500
+ if (operation.type === "insert") {
501
+ const input = operation.record;
502
+ const inserted = await this.findById(operation.collection, input.id);
503
+ if (!inserted) throw new StorageAtomicityError("D1 batch committed but the inserted record could not be read.");
504
+ results.push({
505
+ type: "insert",
506
+ record: inserted
507
+ });
508
+ continue;
509
+ }
510
+ if (operation.type === "update") {
511
+ results.push({
512
+ type: "update",
513
+ record: await this.findById(operation.collection, operation.id)
514
+ });
515
+ continue;
516
+ }
517
+ results.push({
518
+ type: "delete",
519
+ deleted: getChanges(result) > 0
520
+ });
521
+ }
522
+ return { results };
523
+ } catch (error) {
524
+ if ((error instanceof Error ? error.message.toLowerCase() : "").includes("check constraint")) throw new StorageConflictError();
525
+ return mapD1Error(error);
526
+ }
527
+ }
528
+ };
529
+ export { D1StorageAdapter };
530
+
531
+ //# sourceMappingURL=d1.adapter.js.map