@form-engine-ts/storage-d1 7.18.1 → 8.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -240,6 +240,61 @@ function createD1Storage(options) {
240
240
  [stored.id, stored.formId, stored.formVersion, stored.locale, stored.submittedAt, JSON.stringify(stored)]
241
241
  );
242
242
  },
243
+ async saveSubmissionWithinLimit(submission, maxResponses, saveOptions = {}) {
244
+ if (!Number.isInteger(maxResponses) || maxResponses < 1)
245
+ throw new RangeError("maxResponses must be a positive integer.");
246
+ await ensureReady();
247
+ const stored = parseSubmission(submission, `input "${String(submission?.id)}"`);
248
+ const payloadHash = await (0, import_core.hashFormSubmissionPayload)(stored);
249
+ const idempotent = saveOptions.idempotent === true;
250
+ const existingRow = await boundStatement(
251
+ options.db,
252
+ `SELECT response_id, form_id, form_version, locale, submitted_at, submission_json
253
+ FROM ${responsesTable} WHERE response_id = ? LIMIT 1`,
254
+ [stored.id]
255
+ ).first();
256
+ if (existingRow !== null) {
257
+ if (saveOptions.idempotent !== true) throw new Error(`A submission with ID "${stored.id}" already exists.`);
258
+ const existing = parseSubmissionRow(existingRow, 0);
259
+ const existingPayloadHash = await (0, import_core.hashFormSubmissionPayload)(existing);
260
+ if (existingPayloadHash === payloadHash) return { status: "duplicate", submission: existing, payloadHash };
261
+ return { status: "conflict", submissionId: stored.id, payloadHash, existingPayloadHash };
262
+ }
263
+ const inserted = await boundStatement(
264
+ options.db,
265
+ `INSERT INTO ${responsesTable}
266
+ (response_id, form_id, form_version, locale, submitted_at, submission_json)
267
+ SELECT ?, ?, ?, ?, ?, ?
268
+ WHERE (SELECT COUNT(*) FROM ${responsesTable} WHERE form_id = ? AND form_version = ?) < ?
269
+ RETURNING response_id`,
270
+ [
271
+ stored.id,
272
+ stored.formId,
273
+ stored.formVersion,
274
+ stored.locale,
275
+ stored.submittedAt,
276
+ JSON.stringify(stored),
277
+ stored.formId,
278
+ stored.formVersion,
279
+ maxResponses
280
+ ]
281
+ ).first();
282
+ if (inserted === null) {
283
+ const racedRow = await boundStatement(
284
+ options.db,
285
+ `SELECT response_id, form_id, form_version, locale, submitted_at, submission_json
286
+ FROM ${responsesTable} WHERE response_id = ? LIMIT 1`,
287
+ [stored.id]
288
+ ).first();
289
+ if (racedRow === null) return { status: "limit_reached" };
290
+ if (!idempotent) throw new Error(`A submission with ID "${stored.id}" already exists.`);
291
+ const raced = parseSubmissionRow(racedRow, 0);
292
+ const existingPayloadHash = await (0, import_core.hashFormSubmissionPayload)(raced);
293
+ if (existingPayloadHash === payloadHash) return { status: "duplicate", submission: raced, payloadHash };
294
+ return { status: "conflict", submissionId: stored.id, payloadHash, existingPayloadHash };
295
+ }
296
+ if (saveOptions.idempotent === true) return { status: "created", submission: stored, payloadHash };
297
+ },
243
298
  async listSubmissions(formId, formVersion, queryOptions) {
244
299
  await ensureReady();
245
300
  const conditions = ["form_id = ?"];
@@ -263,6 +318,25 @@ function createD1Storage(options) {
263
318
  );
264
319
  return rows.map(parseSubmissionRow);
265
320
  },
321
+ async countSubmissions(formId, formVersion, queryOptions) {
322
+ await ensureReady();
323
+ const conditions = ["form_id = ?"];
324
+ const params = [formId];
325
+ if (formVersion !== void 0) {
326
+ conditions.push("form_version = ?");
327
+ params.push(formVersion);
328
+ }
329
+ if (queryOptions?.since !== void 0) {
330
+ conditions.push("submitted_at >= ?");
331
+ params.push(queryOptions.since);
332
+ }
333
+ if (queryOptions?.until !== void 0) {
334
+ conditions.push("submitted_at <= ?");
335
+ params.push(queryOptions.until);
336
+ }
337
+ const row = await options.db.prepare(`SELECT COUNT(*) AS count FROM ${responsesTable} WHERE ${conditions.join(" AND ")}`).bind(...params).first();
338
+ return Number(row?.count ?? 0);
339
+ },
266
340
  async deleteSubmission(submissionId) {
267
341
  await ensureReady();
268
342
  await run(`DELETE FROM ${responsesTable} WHERE response_id = ?`, [submissionId]);
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // src/index.ts
2
2
  import {
3
3
  assertValidFormSchema,
4
- createFormLifecycleAdapter
4
+ createFormLifecycleAdapter,
5
+ hashFormSubmissionPayload
5
6
  } from "@form-engine-ts/core";
6
7
  function isRecord(value) {
7
8
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -219,6 +220,61 @@ function createD1Storage(options) {
219
220
  [stored.id, stored.formId, stored.formVersion, stored.locale, stored.submittedAt, JSON.stringify(stored)]
220
221
  );
221
222
  },
223
+ async saveSubmissionWithinLimit(submission, maxResponses, saveOptions = {}) {
224
+ if (!Number.isInteger(maxResponses) || maxResponses < 1)
225
+ throw new RangeError("maxResponses must be a positive integer.");
226
+ await ensureReady();
227
+ const stored = parseSubmission(submission, `input "${String(submission?.id)}"`);
228
+ const payloadHash = await hashFormSubmissionPayload(stored);
229
+ const idempotent = saveOptions.idempotent === true;
230
+ const existingRow = await boundStatement(
231
+ options.db,
232
+ `SELECT response_id, form_id, form_version, locale, submitted_at, submission_json
233
+ FROM ${responsesTable} WHERE response_id = ? LIMIT 1`,
234
+ [stored.id]
235
+ ).first();
236
+ if (existingRow !== null) {
237
+ if (saveOptions.idempotent !== true) throw new Error(`A submission with ID "${stored.id}" already exists.`);
238
+ const existing = parseSubmissionRow(existingRow, 0);
239
+ const existingPayloadHash = await hashFormSubmissionPayload(existing);
240
+ if (existingPayloadHash === payloadHash) return { status: "duplicate", submission: existing, payloadHash };
241
+ return { status: "conflict", submissionId: stored.id, payloadHash, existingPayloadHash };
242
+ }
243
+ const inserted = await boundStatement(
244
+ options.db,
245
+ `INSERT INTO ${responsesTable}
246
+ (response_id, form_id, form_version, locale, submitted_at, submission_json)
247
+ SELECT ?, ?, ?, ?, ?, ?
248
+ WHERE (SELECT COUNT(*) FROM ${responsesTable} WHERE form_id = ? AND form_version = ?) < ?
249
+ RETURNING response_id`,
250
+ [
251
+ stored.id,
252
+ stored.formId,
253
+ stored.formVersion,
254
+ stored.locale,
255
+ stored.submittedAt,
256
+ JSON.stringify(stored),
257
+ stored.formId,
258
+ stored.formVersion,
259
+ maxResponses
260
+ ]
261
+ ).first();
262
+ if (inserted === null) {
263
+ const racedRow = await boundStatement(
264
+ options.db,
265
+ `SELECT response_id, form_id, form_version, locale, submitted_at, submission_json
266
+ FROM ${responsesTable} WHERE response_id = ? LIMIT 1`,
267
+ [stored.id]
268
+ ).first();
269
+ if (racedRow === null) return { status: "limit_reached" };
270
+ if (!idempotent) throw new Error(`A submission with ID "${stored.id}" already exists.`);
271
+ const raced = parseSubmissionRow(racedRow, 0);
272
+ const existingPayloadHash = await hashFormSubmissionPayload(raced);
273
+ if (existingPayloadHash === payloadHash) return { status: "duplicate", submission: raced, payloadHash };
274
+ return { status: "conflict", submissionId: stored.id, payloadHash, existingPayloadHash };
275
+ }
276
+ if (saveOptions.idempotent === true) return { status: "created", submission: stored, payloadHash };
277
+ },
222
278
  async listSubmissions(formId, formVersion, queryOptions) {
223
279
  await ensureReady();
224
280
  const conditions = ["form_id = ?"];
@@ -242,6 +298,25 @@ function createD1Storage(options) {
242
298
  );
243
299
  return rows.map(parseSubmissionRow);
244
300
  },
301
+ async countSubmissions(formId, formVersion, queryOptions) {
302
+ await ensureReady();
303
+ const conditions = ["form_id = ?"];
304
+ const params = [formId];
305
+ if (formVersion !== void 0) {
306
+ conditions.push("form_version = ?");
307
+ params.push(formVersion);
308
+ }
309
+ if (queryOptions?.since !== void 0) {
310
+ conditions.push("submitted_at >= ?");
311
+ params.push(queryOptions.since);
312
+ }
313
+ if (queryOptions?.until !== void 0) {
314
+ conditions.push("submitted_at <= ?");
315
+ params.push(queryOptions.until);
316
+ }
317
+ const row = await options.db.prepare(`SELECT COUNT(*) AS count FROM ${responsesTable} WHERE ${conditions.join(" AND ")}`).bind(...params).first();
318
+ return Number(row?.count ?? 0);
319
+ },
245
320
  async deleteSubmission(submissionId) {
246
321
  await ensureReady();
247
322
  await run(`DELETE FROM ${responsesTable} WHERE response_id = ?`, [submissionId]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/storage-d1",
3
- "version": "7.18.1",
3
+ "version": "8.0.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -41,7 +41,7 @@
41
41
  "typescript"
42
42
  ],
43
43
  "dependencies": {
44
- "@form-engine-ts/core": "7.18.1"
44
+ "@form-engine-ts/core": "8.0.1"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@cloudflare/workers-types": "^4.20240000.0"
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "devDependencies": {
55
55
  "@cloudflare/workers-types": "^4.20240000.0",
56
- "@form-engine-ts/storage": "7.18.1"
56
+ "@form-engine-ts/storage": "8.0.1"
57
57
  },
58
58
  "scripts": {
59
59
  "build": "tsup src/index.ts --format esm,cjs --dts --clean --external @form-engine-ts/core",