@form-engine-ts/storage-azure-table 7.17.3 → 7.18.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.
package/README.md CHANGED
@@ -40,8 +40,9 @@ to fill the requested logical page after client-side filtering. `buildSubmission
40
40
  The caller owns table creation, credentials, retries, and client lifecycle.
41
41
 
42
42
  The default codec persists the canonical `FormSubmission.values` payload and never creates an `answers` property. The
43
- standard adapter rejects entities containing the legacy `answers` column; it does not decode or migrate them. Legacy
44
- codecs are available only from `@form-engine-ts/legacy`. `idempotentSubmissions: true` enables typed `created`, `duplicate`, and `conflict` results from
43
+ standard adapter rejects entities containing the legacy `answers` column; it does not decode or migrate them.
44
+ Applications must migrate legacy data to the canonical `values` contract before using this adapter.
45
+ `idempotentSubmissions: true` enables typed `created`, `duplicate`, and `conflict` results from
45
46
  `saveSubmission(submission)`. Pass `validateAgainstSchema: true` to re-validate against the stored schema. Pass
46
47
  `submissionSchema`, `submissionValidator`, or `validation` to validate every submission before it is written; the same
47
48
  source can be supplied per call as `saveSubmission(submission, { validation })`. A source can be a FormSchema, a
package/dist/index.cjs CHANGED
@@ -247,12 +247,12 @@ async function collectSubmissionPages(fetchPage, formId, options) {
247
247
  } while (cursor !== void 0);
248
248
  return submissions;
249
249
  }
250
- function parseSchemaEntity(value) {
250
+ function parseSchemaEntity(value, validation = {}) {
251
251
  if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || value.kind !== "schema" || !Number.isInteger(value.formVersion) || typeof value.payload !== "string") {
252
252
  throw new Error("Azure Table schema entity is invalid.");
253
253
  }
254
254
  const schema = parseJson(value.payload, `schema ${value.partitionKey}/${value.rowKey}`);
255
- (0, import_core.assertValidFormSchema)(schema);
255
+ (0, import_core.assertValidFormSchema)(schema, validation);
256
256
  if (schema.id !== value.partitionKey || schema.version !== value.formVersion) {
257
257
  throw new Error("Azure Table schema entity has inconsistent metadata.");
258
258
  }
@@ -403,10 +403,57 @@ function createAzureTableStorage(options = {}) {
403
403
  (left, right) => left.submittedAt.localeCompare(right.submittedAt) || left.id.localeCompare(right.id)
404
404
  );
405
405
  };
406
+ const lifecycleBackend = {
407
+ resources: ["schema", "submission"],
408
+ async list(formId) {
409
+ const records = [];
410
+ for (const kind of ["schema", "submission"]) {
411
+ const client = kind === "schema" ? await schemaClient(formId) : await submissionClient(formId);
412
+ const filter = kind === "schema" ? `PartitionKey eq '${escapeOData(formId)}' and kind eq 'schema'` : queryFilter(formId, {});
413
+ for await (const raw of client.listEntities({ queryOptions: { filter } })) {
414
+ if (kind === "schema" && raw.kind !== "schema") continue;
415
+ const value = kind === "schema" ? parseSchemaEntity(raw, options.schemaValidation) : deserializeIfMatching(raw);
416
+ if (value === void 0 || ("formId" in value ? value.formId : value.id) !== formId) continue;
417
+ if (typeof raw.partitionKey !== "string" || typeof raw.rowKey !== "string")
418
+ throw new TypeError("Missing entity keys.");
419
+ records.push({ kind, id: JSON.stringify([raw.partitionKey, raw.rowKey]), value: { raw, value, formId } });
420
+ }
421
+ }
422
+ return records;
423
+ },
424
+ async remove(resource) {
425
+ ensureWritable();
426
+ if (!isRecord(resource.value) || !isRecord(resource.value.raw) || typeof resource.value.formId !== "string")
427
+ throw new TypeError("Invalid deletion resource.");
428
+ const { raw, formId } = resource.value;
429
+ if (typeof raw.partitionKey !== "string" || typeof raw.rowKey !== "string" || typeof raw.etag !== "string")
430
+ throw new TypeError("Conditional deletion requires entity keys and an ETag.");
431
+ const client = resource.kind === "schema" ? await schemaClient(formId) : await submissionClient(formId);
432
+ try {
433
+ await client.deleteEntity(raw.partitionKey, raw.rowKey, { etag: raw.etag });
434
+ return 1;
435
+ } catch (error) {
436
+ if (isNotFound(error)) return 0;
437
+ throw error;
438
+ }
439
+ }
440
+ };
441
+ const lifecycle = (0, import_core.createFormLifecycleAdapter)(
442
+ lifecycleBackend,
443
+ options.lifecycle === void 0 ? {} : {
444
+ ...options.lifecycle,
445
+ ...options.lifecycle.scope === void 0 ? {} : {
446
+ scope: (resource) => {
447
+ if (!isRecord(resource.value)) throw new TypeError("Invalid deletion resource.");
448
+ return options.lifecycle?.scope?.({ ...resource, value: resource.value.value }) ?? {};
449
+ }
450
+ }
451
+ }
452
+ );
406
453
  const adapter = {
407
454
  async saveSchema(schema) {
408
455
  ensureWritable();
409
- (0, import_core.assertValidFormSchema)(schema);
456
+ (0, import_core.assertValidFormSchema)(schema, options.schemaValidation);
410
457
  const entity = {
411
458
  partitionKey: schema.id,
412
459
  rowKey: schemaRowKey(schema.version),
@@ -418,7 +465,10 @@ function createAzureTableStorage(options = {}) {
418
465
  },
419
466
  async getSchema(formId, formVersion) {
420
467
  try {
421
- return parseSchemaEntity(await (await schemaClient(formId)).getEntity(formId, schemaRowKey(formVersion)));
468
+ return parseSchemaEntity(
469
+ await (await schemaClient(formId)).getEntity(formId, schemaRowKey(formVersion)),
470
+ options.schemaValidation
471
+ );
422
472
  } catch (error) {
423
473
  if (isNotFound(error)) return null;
424
474
  throw error;
@@ -427,7 +477,7 @@ function createAzureTableStorage(options = {}) {
427
477
  async listSchemas() {
428
478
  const found = [];
429
479
  for await (const raw of (await schemaClient("")).listEntities({ queryOptions: { filter: "kind eq 'schema'" } })) {
430
- if (raw.kind === "schema") found.push(parseSchemaEntity(raw));
480
+ if (raw.kind === "schema") found.push(parseSchemaEntity(raw, options.schemaValidation));
431
481
  }
432
482
  return found.sort((left, right) => left.id.localeCompare(right.id) || left.version - right.version);
433
483
  },
@@ -448,7 +498,7 @@ function createAzureTableStorage(options = {}) {
448
498
  const schema = parseSchemaEntity(
449
499
  await (await schemaClient(stored.formId)).getEntity(stored.formId, schemaRowKey(stored.formVersion))
450
500
  );
451
- (0, import_core.assertValidFormSubmission)(schema, stored);
501
+ (0, import_core.assertValidFormSubmission)(schema, stored, options.schemaValidation);
452
502
  }
453
503
  const payloadHash = await (0, import_core.hashFormSubmissionPayload)(stored);
454
504
  const createdEntity = codec.createEntity(stored);
@@ -658,7 +708,7 @@ function createAzureTableStorage(options = {}) {
658
708
  }
659
709
  const schema = await adapter.getSchema(submission.formId, submission.formVersion);
660
710
  if (schema === null) throw new Error("Azure Table submission schema was not found.");
661
- (0, import_core.assertValidFormSubmission)(schema, submission);
711
+ (0, import_core.assertValidFormSubmission)(schema, submission, options.schemaValidation);
662
712
  },
663
713
  async deleteSubmission(submissionId) {
664
714
  ensureWritable();
@@ -704,6 +754,7 @@ function createAzureTableStorage(options = {}) {
704
754
  };
705
755
  return {
706
756
  ...adapter,
757
+ ...lifecycle,
707
758
  async fetchSubmissionPage(formId, options2) {
708
759
  return await adapter.listSubmissionPage(
709
760
  formId,
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { BaseSubmissionMetadata, PagedSubmissionStorageAdapter, TextAnswerPageQueryOptions, TextAnswerPage, TypedSubmissionPageQueryOptions, TypedSubmissionPage, FormSubmission, FormSchema, SubmissionPageQueryOptions, FormAnalytics, StorageSubmissionExportOptions, FormSubmissionValidationSource, FormSubmissionValidator, SaveSubmissionOptions, SubmissionSaveResult, SubmissionFilter, JsonValue, TypedTextAnswerPage } from '@form-engine-ts/core';
1
+ import { BaseSubmissionMetadata, PagedSubmissionStorageAdapter, TextAnswerPageQueryOptions, TextAnswerPage, TypedSubmissionPageQueryOptions, TypedSubmissionPage, FormSubmission, FormSchema, SubmissionPageQueryOptions, FormAnalytics, StorageSubmissionExportOptions, FormSubmissionValidationSource, ValidateFormSchemaOptions, FormLifecycleOptions, FormSubmissionValidator, SaveSubmissionOptions, SubmissionSaveResult, SubmissionFilter, JsonValue, TypedTextAnswerPage } from '@form-engine-ts/core';
2
2
 
3
3
  interface AzureTableListOptions {
4
4
  readonly queryOptions?: {
@@ -20,7 +20,9 @@ interface AzureTableClientLike {
20
20
  upsertEntity(entity: Record<string, unknown>, mode?: "Merge" | "Replace"): Promise<unknown>;
21
21
  getEntity(partitionKey: string, rowKey: string): Promise<Record<string, unknown>>;
22
22
  listEntities(options?: AzureTableListOptions): AzureTableEntityIterator;
23
- deleteEntity(partitionKey: string, rowKey: string): Promise<unknown>;
23
+ deleteEntity(partitionKey: string, rowKey: string, options?: {
24
+ readonly etag?: string;
25
+ }): Promise<unknown>;
24
26
  }
25
27
  type AzureTableSubmissionEntity = Record<string, unknown> & {
26
28
  readonly answers?: never;
@@ -48,6 +50,8 @@ interface AzureTableValueCodec {
48
50
  readonly decodeValues?: (raw: string) => Record<string, unknown>;
49
51
  }
50
52
  interface AzureTableStorageOptions<T = FormSubmission> {
53
+ readonly schemaValidation?: ValidateFormSchemaOptions;
54
+ readonly lifecycle?: FormLifecycleOptions;
51
55
  /** @deprecated Use schemasTableClient, submissionsTableClient, or clientResolver. */
52
56
  readonly client?: AzureTableClientLike;
53
57
  readonly schemasTableClient?: AzureTableClientLike;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { BaseSubmissionMetadata, PagedSubmissionStorageAdapter, TextAnswerPageQueryOptions, TextAnswerPage, TypedSubmissionPageQueryOptions, TypedSubmissionPage, FormSubmission, FormSchema, SubmissionPageQueryOptions, FormAnalytics, StorageSubmissionExportOptions, FormSubmissionValidationSource, FormSubmissionValidator, SaveSubmissionOptions, SubmissionSaveResult, SubmissionFilter, JsonValue, TypedTextAnswerPage } from '@form-engine-ts/core';
1
+ import { BaseSubmissionMetadata, PagedSubmissionStorageAdapter, TextAnswerPageQueryOptions, TextAnswerPage, TypedSubmissionPageQueryOptions, TypedSubmissionPage, FormSubmission, FormSchema, SubmissionPageQueryOptions, FormAnalytics, StorageSubmissionExportOptions, FormSubmissionValidationSource, ValidateFormSchemaOptions, FormLifecycleOptions, FormSubmissionValidator, SaveSubmissionOptions, SubmissionSaveResult, SubmissionFilter, JsonValue, TypedTextAnswerPage } from '@form-engine-ts/core';
2
2
 
3
3
  interface AzureTableListOptions {
4
4
  readonly queryOptions?: {
@@ -20,7 +20,9 @@ interface AzureTableClientLike {
20
20
  upsertEntity(entity: Record<string, unknown>, mode?: "Merge" | "Replace"): Promise<unknown>;
21
21
  getEntity(partitionKey: string, rowKey: string): Promise<Record<string, unknown>>;
22
22
  listEntities(options?: AzureTableListOptions): AzureTableEntityIterator;
23
- deleteEntity(partitionKey: string, rowKey: string): Promise<unknown>;
23
+ deleteEntity(partitionKey: string, rowKey: string, options?: {
24
+ readonly etag?: string;
25
+ }): Promise<unknown>;
24
26
  }
25
27
  type AzureTableSubmissionEntity = Record<string, unknown> & {
26
28
  readonly answers?: never;
@@ -48,6 +50,8 @@ interface AzureTableValueCodec {
48
50
  readonly decodeValues?: (raw: string) => Record<string, unknown>;
49
51
  }
50
52
  interface AzureTableStorageOptions<T = FormSubmission> {
53
+ readonly schemaValidation?: ValidateFormSchemaOptions;
54
+ readonly lifecycle?: FormLifecycleOptions;
51
55
  /** @deprecated Use schemasTableClient, submissionsTableClient, or clientResolver. */
52
56
  readonly client?: AzureTableClientLike;
53
57
  readonly schemasTableClient?: AzureTableClientLike;
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  assertValidFormSchema,
5
5
  assertValidFormSubmission,
6
6
  assertValidFormSubmissionWith,
7
+ createFormLifecycleAdapter,
7
8
  exportResponsesToCsv,
8
9
  hashFormSubmissionPayload,
9
10
  matchesSubmissionPageFilters,
@@ -228,12 +229,12 @@ async function collectSubmissionPages(fetchPage, formId, options) {
228
229
  } while (cursor !== void 0);
229
230
  return submissions;
230
231
  }
231
- function parseSchemaEntity(value) {
232
+ function parseSchemaEntity(value, validation = {}) {
232
233
  if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || value.kind !== "schema" || !Number.isInteger(value.formVersion) || typeof value.payload !== "string") {
233
234
  throw new Error("Azure Table schema entity is invalid.");
234
235
  }
235
236
  const schema = parseJson(value.payload, `schema ${value.partitionKey}/${value.rowKey}`);
236
- assertValidFormSchema(schema);
237
+ assertValidFormSchema(schema, validation);
237
238
  if (schema.id !== value.partitionKey || schema.version !== value.formVersion) {
238
239
  throw new Error("Azure Table schema entity has inconsistent metadata.");
239
240
  }
@@ -384,10 +385,57 @@ function createAzureTableStorage(options = {}) {
384
385
  (left, right) => left.submittedAt.localeCompare(right.submittedAt) || left.id.localeCompare(right.id)
385
386
  );
386
387
  };
388
+ const lifecycleBackend = {
389
+ resources: ["schema", "submission"],
390
+ async list(formId) {
391
+ const records = [];
392
+ for (const kind of ["schema", "submission"]) {
393
+ const client = kind === "schema" ? await schemaClient(formId) : await submissionClient(formId);
394
+ const filter = kind === "schema" ? `PartitionKey eq '${escapeOData(formId)}' and kind eq 'schema'` : queryFilter(formId, {});
395
+ for await (const raw of client.listEntities({ queryOptions: { filter } })) {
396
+ if (kind === "schema" && raw.kind !== "schema") continue;
397
+ const value = kind === "schema" ? parseSchemaEntity(raw, options.schemaValidation) : deserializeIfMatching(raw);
398
+ if (value === void 0 || ("formId" in value ? value.formId : value.id) !== formId) continue;
399
+ if (typeof raw.partitionKey !== "string" || typeof raw.rowKey !== "string")
400
+ throw new TypeError("Missing entity keys.");
401
+ records.push({ kind, id: JSON.stringify([raw.partitionKey, raw.rowKey]), value: { raw, value, formId } });
402
+ }
403
+ }
404
+ return records;
405
+ },
406
+ async remove(resource) {
407
+ ensureWritable();
408
+ if (!isRecord(resource.value) || !isRecord(resource.value.raw) || typeof resource.value.formId !== "string")
409
+ throw new TypeError("Invalid deletion resource.");
410
+ const { raw, formId } = resource.value;
411
+ if (typeof raw.partitionKey !== "string" || typeof raw.rowKey !== "string" || typeof raw.etag !== "string")
412
+ throw new TypeError("Conditional deletion requires entity keys and an ETag.");
413
+ const client = resource.kind === "schema" ? await schemaClient(formId) : await submissionClient(formId);
414
+ try {
415
+ await client.deleteEntity(raw.partitionKey, raw.rowKey, { etag: raw.etag });
416
+ return 1;
417
+ } catch (error) {
418
+ if (isNotFound(error)) return 0;
419
+ throw error;
420
+ }
421
+ }
422
+ };
423
+ const lifecycle = createFormLifecycleAdapter(
424
+ lifecycleBackend,
425
+ options.lifecycle === void 0 ? {} : {
426
+ ...options.lifecycle,
427
+ ...options.lifecycle.scope === void 0 ? {} : {
428
+ scope: (resource) => {
429
+ if (!isRecord(resource.value)) throw new TypeError("Invalid deletion resource.");
430
+ return options.lifecycle?.scope?.({ ...resource, value: resource.value.value }) ?? {};
431
+ }
432
+ }
433
+ }
434
+ );
387
435
  const adapter = {
388
436
  async saveSchema(schema) {
389
437
  ensureWritable();
390
- assertValidFormSchema(schema);
438
+ assertValidFormSchema(schema, options.schemaValidation);
391
439
  const entity = {
392
440
  partitionKey: schema.id,
393
441
  rowKey: schemaRowKey(schema.version),
@@ -399,7 +447,10 @@ function createAzureTableStorage(options = {}) {
399
447
  },
400
448
  async getSchema(formId, formVersion) {
401
449
  try {
402
- return parseSchemaEntity(await (await schemaClient(formId)).getEntity(formId, schemaRowKey(formVersion)));
450
+ return parseSchemaEntity(
451
+ await (await schemaClient(formId)).getEntity(formId, schemaRowKey(formVersion)),
452
+ options.schemaValidation
453
+ );
403
454
  } catch (error) {
404
455
  if (isNotFound(error)) return null;
405
456
  throw error;
@@ -408,7 +459,7 @@ function createAzureTableStorage(options = {}) {
408
459
  async listSchemas() {
409
460
  const found = [];
410
461
  for await (const raw of (await schemaClient("")).listEntities({ queryOptions: { filter: "kind eq 'schema'" } })) {
411
- if (raw.kind === "schema") found.push(parseSchemaEntity(raw));
462
+ if (raw.kind === "schema") found.push(parseSchemaEntity(raw, options.schemaValidation));
412
463
  }
413
464
  return found.sort((left, right) => left.id.localeCompare(right.id) || left.version - right.version);
414
465
  },
@@ -429,7 +480,7 @@ function createAzureTableStorage(options = {}) {
429
480
  const schema = parseSchemaEntity(
430
481
  await (await schemaClient(stored.formId)).getEntity(stored.formId, schemaRowKey(stored.formVersion))
431
482
  );
432
- assertValidFormSubmission(schema, stored);
483
+ assertValidFormSubmission(schema, stored, options.schemaValidation);
433
484
  }
434
485
  const payloadHash = await hashFormSubmissionPayload(stored);
435
486
  const createdEntity = codec.createEntity(stored);
@@ -639,7 +690,7 @@ function createAzureTableStorage(options = {}) {
639
690
  }
640
691
  const schema = await adapter.getSchema(submission.formId, submission.formVersion);
641
692
  if (schema === null) throw new Error("Azure Table submission schema was not found.");
642
- assertValidFormSubmission(schema, submission);
693
+ assertValidFormSubmission(schema, submission, options.schemaValidation);
643
694
  },
644
695
  async deleteSubmission(submissionId) {
645
696
  ensureWritable();
@@ -685,6 +736,7 @@ function createAzureTableStorage(options = {}) {
685
736
  };
686
737
  return {
687
738
  ...adapter,
739
+ ...lifecycle,
688
740
  async fetchSubmissionPage(formId, options2) {
689
741
  return await adapter.listSubmissionPage(
690
742
  formId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/storage-azure-table",
3
- "version": "7.17.3",
3
+ "version": "7.18.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -39,7 +39,7 @@
39
39
  "typescript"
40
40
  ],
41
41
  "dependencies": {
42
- "@form-engine-ts/core": "7.17.3"
42
+ "@form-engine-ts/core": "7.18.0"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "@azure/data-tables": "^13.3.2"
@@ -50,7 +50,8 @@
50
50
  }
51
51
  },
52
52
  "devDependencies": {
53
- "@azure/data-tables": "^13.3.2"
53
+ "@azure/data-tables": "^13.3.2",
54
+ "@form-engine-ts/storage": "7.18.0"
54
55
  },
55
56
  "scripts": {
56
57
  "build": "tsup src/index.ts --format esm,cjs --dts --clean --external @form-engine-ts/core",