@form-engine-ts/storage-azure-table 6.10.0 → 7.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.
package/README.md CHANGED
@@ -37,12 +37,11 @@ unsupported expressions remain client-filtered with identical semantics.
37
37
 
38
38
  `listSubmissionPage` follows opaque Azure continuation tokens and scans at most `maxScanPages` native pages (default 5)
39
39
  to fill the requested logical page after client-side filtering. `buildSubmissionFilter` can replace filter generation.
40
- The deprecated `client`, `submissionCodec`, and `toODataFilter` options remain available for compatibility. The caller
41
- owns table creation, credentials, retries, and client lifecycle.
40
+ The caller owns table creation, credentials, retries, and client lifecycle.
42
41
 
43
- The default codec persists the canonical `FormSubmission.values` payload and never creates an `answers` property. Set
44
- `rejectLegacyAnswers: true` to fail explicitly when a legacy entity is encountered; no legacy decoding or migration is
45
- performed. `idempotentSubmissions: true` enables typed `created`, `duplicate`, and `conflict` results from
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
46
45
  `saveSubmission(submission)`. Pass `validateAgainstSchema: true` to re-validate against the stored schema. Pass
47
46
  `submissionSchema`, `submissionValidator`, or `validation` to validate every submission before it is written; the same
48
47
  source can be supplied per call as `saveSubmission(submission, { validation })`. A source can be a FormSchema, a
@@ -55,5 +54,7 @@ consume the item limit, and scanning remains bounded by `maxScanPages`. Cursor f
55
54
  version, sorted fields, and a SHA-256 filter fingerprint. Reusing a cursor with different query context throws
56
55
  `invalid_cursor_context`.
57
56
 
58
- The legacy codec APIs are retained only as deprecated source symbols for existing applications; the standard factory does
59
- not use them.
57
+ The Azure adapter exposes the same required typed submission surface as MongoDB: `listSubmissionPage`,
58
+ `listTextAnswerPage`, `aggregateResponses`, `exportResponsesToCsv`, `validateSubmission`, and idempotent
59
+ `saveSubmission`. Aggregation and CSV options accept the same submission filter query, so applications do not need an
60
+ Azure-specific reporting branch.
package/dist/index.cjs CHANGED
@@ -22,88 +22,12 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  createAzureTableStorage: () => createAzureTableStorage,
24
24
  createAzureTableSubmissionCodec: () => createAzureTableSubmissionCodec,
25
- createLegacyArrayAzureTableCodec: () => createLegacyArrayAzureTableCodec,
26
- createLegacyAzureTableCodec: () => createLegacyAzureTableCodec,
27
25
  defaultAzureTableSubmissionCodec: () => defaultAzureTableSubmissionCodec,
28
26
  metadataFiltersToOData: () => metadataFiltersToOData,
29
27
  submissionFilterToOData: () => submissionFilterToOData
30
28
  });
31
29
  module.exports = __toCommonJS(index_exports);
32
30
  var import_core = require("@form-engine-ts/core");
33
- function createLegacyArrayAzureTableCodec(options = {}) {
34
- const defaultLocale = options.defaultLocale ?? "ja";
35
- if (defaultLocale.trim().length === 0) throw new TypeError("defaultLocale must not be empty.");
36
- return {
37
- encode: (submission) => ({
38
- PartitionKey: submission.formId,
39
- RowKey: submission.id,
40
- answers: JSON.stringify(Object.entries(submission.values).map(([questionId, value]) => ({ questionId, value }))),
41
- answeredAt: submission.submittedAt,
42
- surveyVersion: submission.formVersion,
43
- locale: submission.locale,
44
- ...submission.metadata
45
- }),
46
- decode: (entity) => {
47
- const raw = entity;
48
- if (typeof raw.PartitionKey !== "string" || typeof raw.RowKey !== "string") {
49
- throw new Error("Azure Table legacy array submission is missing PartitionKey or RowKey.");
50
- }
51
- const submittedAt = typeof raw.answeredAt === "string" ? raw.answeredAt : entity.Timestamp;
52
- if (typeof submittedAt !== "string" || submittedAt.trim().length === 0) {
53
- throw new Error("Azure Table legacy array submission is missing answeredAt or Timestamp.");
54
- }
55
- let values = {};
56
- if (typeof raw.answers === "string") {
57
- const parsed = parseJson(raw.answers, "legacy array answers");
58
- if (Array.isArray(parsed)) {
59
- values = Object.fromEntries(
60
- parsed.flatMap((item) => {
61
- if (!isRecord(item) || typeof item.questionId !== "string") return [];
62
- return [[item.questionId, item.value]];
63
- })
64
- );
65
- } else if (isRecord(parsed)) {
66
- values = parsed;
67
- } else {
68
- throw new Error("Azure Table legacy array answers payload must be an array or object.");
69
- }
70
- }
71
- const formVersion = Number(raw.surveyVersion);
72
- return {
73
- id: raw.RowKey,
74
- formId: raw.PartitionKey,
75
- formVersion: Number.isSafeInteger(formVersion) && formVersion > 0 ? formVersion : 1,
76
- values,
77
- locale: typeof raw.locale === "string" && raw.locale.trim().length > 0 ? raw.locale : defaultLocale,
78
- metadata: options.metadataExtractor?.(entity) ?? {},
79
- submittedAt
80
- };
81
- }
82
- };
83
- }
84
- var createLegacyAzureTableCodec = (options = {}) => {
85
- const createPartitionKey = options.partitionKeyGenerator ?? ((formId) => formId);
86
- const createRowKey = options.rowKeyGenerator ?? ((submittedAt, submissionId) => `${submittedAt}_${submissionId}`);
87
- return {
88
- decode: (entity) => {
89
- const values = entity.answers === void 0 ? {} : parseLegacyJsonObject(entity.answers, "answers");
90
- const submittedAt = entity.answeredAt ?? entity.Timestamp;
91
- if (submittedAt === void 0 || submittedAt.trim().length === 0) {
92
- throw new Error("Azure Table legacy submission is missing answeredAt or Timestamp.");
93
- }
94
- return {
95
- id: entity.RowKey,
96
- formId: entity.PartitionKey,
97
- formVersion: entity.surveyVersion ?? 1,
98
- values,
99
- metadata: {},
100
- submittedAt
101
- };
102
- },
103
- createPartitionKey: (formId, submissionId) => createPartitionKey(formId, submissionId),
104
- createRowKey: (submittedAt, submissionId) => createRowKey(submittedAt, submissionId)
105
- };
106
- };
107
31
  var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
108
32
  function encodeBase64(bytes) {
109
33
  let result = "";
@@ -212,11 +136,6 @@ function parseJson(value, location) {
212
136
  throw new Error(`Azure Table ${location} payload is invalid.`, { cause });
213
137
  }
214
138
  }
215
- function parseLegacyJsonObject(value, property) {
216
- const parsed = parseJson(value, `legacy ${property}`);
217
- if (!isRecord(parsed)) throw new Error(`Azure Table legacy ${property} payload must be an object.`);
218
- return parsed;
219
- }
220
139
  function parseSubmission(value, location) {
221
140
  const parsed = typeof value === "string" ? parseJson(value, location) : value;
222
141
  if (!isRecord(parsed) || typeof parsed.id !== "string" || typeof parsed.formId !== "string" || !Number.isInteger(parsed.formVersion) || typeof parsed.locale !== "string" || typeof parsed.submittedAt !== "string" || !isRecord(parsed.values) || !Object.values(parsed.values).every(isFormValue)) {
@@ -312,22 +231,21 @@ function createAzureTableSubmissionCodec() {
312
231
  };
313
232
  }
314
233
  var defaultAzureTableSubmissionCodec = createAzureTableSubmissionCodec();
315
- function legacyCodec(codec) {
316
- return {
317
- createEntity: (value) => ({
318
- ...codec.serialize(value),
319
- kind: "submission",
320
- formVersion: value.formVersion,
321
- locale: value.locale,
322
- submittedAt: value.submittedAt,
323
- responseId: value.id
324
- }),
325
- deserialize: codec.deserialize,
326
- matchesEntity: (entity) => entity.kind === "submission",
327
- createPartitionKey: codec.createPartitionKey,
328
- createPartitionKeyFromQuery: (formId) => codec.createPartitionKeyFromFormId?.(formId) ?? formId,
329
- createRowKey: codec.createRowKey
330
- };
234
+ async function collectSubmissionPages(fetchPage, formId, options) {
235
+ const submissions = [];
236
+ const seen = /* @__PURE__ */ new Set();
237
+ let cursor = options.cursor;
238
+ do {
239
+ if (cursor !== void 0) {
240
+ if (seen.has(cursor)) throw new TypeError("Submission pagination cursor cycle detected.");
241
+ seen.add(cursor);
242
+ }
243
+ const page = await fetchPage(formId, { ...options, ...cursor === void 0 ? {} : { cursor } });
244
+ submissions.push(...page.items);
245
+ if (!page.hasMore || page.nextCursor === void 0 || page.nextCursor.length === 0) break;
246
+ cursor = page.nextCursor;
247
+ } while (cursor !== void 0);
248
+ return submissions;
331
249
  }
332
250
  function parseSchemaEntity(value) {
333
251
  if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || value.kind !== "schema" || !Number.isInteger(value.formVersion) || typeof value.payload !== "string") {
@@ -354,6 +272,11 @@ function parseSubmissionEntity(value, codec, mapping, valueCodec) {
354
272
  }
355
273
  return submission;
356
274
  }
275
+ function assertCanonicalSubmissionEntity(value) {
276
+ if (Object.hasOwn(value, "answers")) {
277
+ throw new Error("Azure Table legacy answers column is not supported by the standard submission storage.");
278
+ }
279
+ }
357
280
  function escapeOData(value) {
358
281
  return value.replaceAll("'", "''");
359
282
  }
@@ -406,19 +329,19 @@ function submissionFilterToOData(filter, mapping) {
406
329
  ...filter.to === void 0 ? [] : [`${property} le ${valueToOData(filter.to)}`]
407
330
  ].join(" and ");
408
331
  }
409
- function defaultSubmissionFilter(codec, formId, options, legacyExtension, mapping, rejectLegacyAnswers = false) {
332
+ function defaultSubmissionFilter(codec, formId, options, customExtension, mapping) {
410
333
  const partitionKey = codec.createPartitionKeyFromQuery(formId, options);
411
334
  const ast = options.filter === void 0 || typeof options.filter === "function" ? void 0 : submissionFilterToOData(options.filter, mapping);
412
335
  const keys = physicalKeyNames(mapping);
413
336
  return [
414
337
  ...partitionKey === void 0 ? [] : [`${keys.partitionKey} eq '${escapeOData(partitionKey)}'`],
415
- ...codec === defaultAzureTableSubmissionCodec && !rejectLegacyAnswers ? ["kind eq 'submission'"] : [],
338
+ ...codec === defaultAzureTableSubmissionCodec ? ["kind eq 'submission'"] : [],
416
339
  ...options.version === void 0 ? [] : [`${propertyName(mapping, "formVersion", "formVersion")} eq ${options.version}`],
417
340
  ...options.since === void 0 ? [] : [`${propertyName(mapping, "submittedAt", "submittedAt")} ge '${escapeOData(options.since)}'`],
418
341
  ...options.until === void 0 ? [] : [`${propertyName(mapping, "submittedAt", "submittedAt")} le '${escapeOData(options.until)}'`],
419
342
  ...options.locale === void 0 ? [] : [`locale eq '${escapeOData(options.locale)}'`],
420
343
  ...ast === void 0 || ast.length === 0 ? [] : [`(${ast})`],
421
- ...legacyExtension.trim().length === 0 ? [] : [`(${legacyExtension})`]
344
+ ...customExtension.trim().length === 0 ? [] : [`(${customExtension})`]
422
345
  ].join(" and ");
423
346
  }
424
347
  function isNotFound(error) {
@@ -440,7 +363,7 @@ function createAzureTableStorage(options = {}) {
440
363
  const staticSubmissions = options.submissionsTableClient ?? options.client;
441
364
  const configuredCodec = options.codec;
442
365
  const valueCodec = configuredCodec !== void 0 && "encodeValues" in configuredCodec ? configuredCodec : void 0;
443
- const codec = (configuredCodec !== void 0 && "createEntity" in configuredCodec ? configuredCodec : void 0) ?? (options.submissionCodec === void 0 ? defaultAzureTableSubmissionCodec : legacyCodec(options.submissionCodec));
366
+ const codec = (configuredCodec !== void 0 && "createEntity" in configuredCodec ? configuredCodec : void 0) ?? defaultAzureTableSubmissionCodec;
444
367
  const maxScanPages = options.maxScanPages ?? 5;
445
368
  if (!Number.isSafeInteger(maxScanPages) || maxScanPages < 1) {
446
369
  throw new TypeError("maxScanPages must be a positive safe integer.");
@@ -457,21 +380,15 @@ function createAzureTableStorage(options = {}) {
457
380
  codec,
458
381
  formId,
459
382
  query,
460
- options.toODataFilter?.(query) ?? metadataFiltersToOData(query, options.fieldMapping),
461
- options.fieldMapping,
462
- options.rejectLegacyAnswers
383
+ metadataFiltersToOData(query, options.fieldMapping),
384
+ options.fieldMapping
463
385
  );
464
386
  };
465
387
  const ensureWritable = () => {
466
388
  if (options.readOnly === true) throw new Error("Azure Table storage is read-only.");
467
389
  };
468
- const rejectLegacyEntity = (entity) => {
469
- if (options.rejectLegacyAnswers === true && Object.hasOwn(entity, "answers")) {
470
- throw new Error("Azure Table legacy answers column is not supported by the standard submission storage.");
471
- }
472
- };
473
390
  const deserializeIfMatching = (entity) => {
474
- rejectLegacyEntity(entity);
391
+ assertCanonicalSubmissionEntity(entity);
475
392
  return codec.matchesEntity(entity) ? parseSubmissionEntity(entity, codec, options.fieldMapping, valueCodec) : void 0;
476
393
  };
477
394
  const listSubmissionCandidates = async (formId, query) => {
@@ -534,7 +451,9 @@ function createAzureTableStorage(options = {}) {
534
451
  (0, import_core.assertValidFormSubmission)(schema, stored);
535
452
  }
536
453
  const payloadHash = await (0, import_core.hashFormSubmissionPayload)(stored);
537
- const mappedEntity = mappedSubmissionEntity(codec.createEntity(stored), stored, options.fieldMapping, valueCodec);
454
+ const createdEntity = codec.createEntity(stored);
455
+ assertCanonicalSubmissionEntity(createdEntity);
456
+ const mappedEntity = mappedSubmissionEntity(createdEntity, stored, options.fieldMapping, valueCodec);
538
457
  const keys = physicalKeyNames(options.fieldMapping);
539
458
  const partitionKey = codec.createPartitionKey(stored);
540
459
  const rowKey = codec.createRowKey(stored);
@@ -703,6 +622,44 @@ function createAzureTableStorage(options = {}) {
703
622
  })
704
623
  };
705
624
  },
625
+ async aggregateResponses(schema, options2 = {}) {
626
+ const query = { ...options2, version: options2.version ?? schema.version };
627
+ const items = await collectSubmissionPages(
628
+ (formId, pageOptions) => adapter.listSubmissionPage(formId, pageOptions),
629
+ schema.id,
630
+ query
631
+ );
632
+ return (0, import_core.aggregateResponses)(schema, items);
633
+ },
634
+ async exportResponsesToCsv(schema, options2 = {}) {
635
+ const { query, ...csvOptions } = options2;
636
+ const items = await collectSubmissionPages(
637
+ (formId, pageOptions) => adapter.listSubmissionPage(formId, pageOptions),
638
+ schema.id,
639
+ { ...query ?? {}, version: query?.version ?? schema.version }
640
+ );
641
+ const exportOptions = {
642
+ ...csvOptions.withBom === void 0 ? {} : { withBom: csvOptions.withBom },
643
+ ...csvOptions.useBom === void 0 ? {} : { useBom: csvOptions.useBom },
644
+ ...csvOptions.neutralizeFormulas === void 0 ? {} : { neutralizeFormulas: csvOptions.neutralizeFormulas },
645
+ ...csvOptions.preventFormulaInjection === void 0 ? {} : { preventFormulaInjection: csvOptions.preventFormulaInjection },
646
+ ...csvOptions.includeLocale === void 0 ? {} : { includeLocale: csvOptions.includeLocale },
647
+ ...csvOptions.includePiiStatus === void 0 ? {} : { includePiiStatus: csvOptions.includePiiStatus },
648
+ ...csvOptions.customColumns === void 0 ? {} : { customColumns: csvOptions.customColumns },
649
+ ...csvOptions.includeMetadataFields === void 0 ? {} : { includeMetadataFields: csvOptions.includeMetadataFields }
650
+ };
651
+ const exportItems = items.map((item) => ({ ...item, metadata: item.metadata ?? {} }));
652
+ return (0, import_core.exportResponsesToCsv)(schema, exportItems, exportOptions);
653
+ },
654
+ async validateSubmission(submission, source) {
655
+ if (source !== void 0) {
656
+ await (0, import_core.assertValidFormSubmissionWith)(source, submission);
657
+ return;
658
+ }
659
+ const schema = await adapter.getSchema(submission.formId, submission.formVersion);
660
+ if (schema === null) throw new Error("Azure Table submission schema was not found.");
661
+ (0, import_core.assertValidFormSubmission)(schema, submission);
662
+ },
706
663
  async deleteSubmission(submissionId) {
707
664
  ensureWritable();
708
665
  const client = await submissionClient("");
@@ -773,8 +730,6 @@ function createAzureTableStorage(options = {}) {
773
730
  0 && (module.exports = {
774
731
  createAzureTableStorage,
775
732
  createAzureTableSubmissionCodec,
776
- createLegacyArrayAzureTableCodec,
777
- createLegacyAzureTableCodec,
778
733
  defaultAzureTableSubmissionCodec,
779
734
  metadataFiltersToOData,
780
735
  submissionFilterToOData
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { FormSubmission, BaseSubmissionMetadata, PagedSubmissionStorageAdapter, TypedSubmissionPageQueryOptions, TypedSubmissionPage, SubmissionPageQueryOptions, FormSubmissionValidationSource, FormSubmissionValidator, StrictFormSubmission, SaveSubmissionOptions, SubmissionSaveResult, SubmissionFilter, JsonValue, TextAnswerPageQueryOptions, TypedTextAnswerPage } from '@form-engine-ts/core';
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';
2
2
 
3
3
  interface AzureTableListOptions {
4
4
  readonly queryOptions?: {
@@ -22,52 +22,13 @@ interface AzureTableClientLike {
22
22
  listEntities(options?: AzureTableListOptions): AzureTableEntityIterator;
23
23
  deleteEntity(partitionKey: string, rowKey: string): Promise<unknown>;
24
24
  }
25
- /** @deprecated Use AzureTableSubmissionCodec. */
26
- interface AzureTableEntityCodec<T> {
27
- readonly createPartitionKey: (submission: T) => string;
28
- readonly createPartitionKeyFromFormId?: (formId: string) => string;
29
- readonly createRowKey: (submission: T) => string;
30
- readonly serialize: (submission: T) => Record<string, unknown>;
31
- readonly deserialize: (entity: Record<string, unknown>) => T;
32
- }
33
- interface AzureTableLegacyEntity {
34
- readonly PartitionKey: string;
35
- readonly RowKey: string;
36
- readonly answers?: string;
37
- readonly answeredAt?: string;
38
- readonly surveyVersion?: number;
39
- readonly Timestamp?: string;
40
- readonly [key: string]: unknown;
41
- }
42
- interface AzureTableLegacyCodec {
43
- readonly decode: (entity: AzureTableLegacyEntity) => FormSubmission;
44
- readonly createPartitionKey: (formId: string, submissionId: string) => string;
45
- readonly createRowKey: (submittedAt: string, submissionId: string) => string;
46
- }
47
- interface LegacyAnswerArrayEntity extends Record<string, unknown> {
48
- readonly PartitionKey: string;
49
- readonly RowKey: string;
50
- readonly answers: string;
51
- readonly answeredAt: string;
52
- readonly surveyVersion: number;
53
- readonly locale?: string;
54
- }
55
- interface LegacyArrayAzureTableCodec<TMeta extends BaseSubmissionMetadata = BaseSubmissionMetadata> {
56
- readonly encode: (submission: StrictFormSubmission<TMeta>) => Record<string, unknown>;
57
- readonly decode: (entity: Record<string, unknown>) => StrictFormSubmission<TMeta>;
58
- }
59
- declare function createLegacyArrayAzureTableCodec<TMeta extends BaseSubmissionMetadata = BaseSubmissionMetadata>(options?: {
60
- readonly defaultLocale?: string;
61
- readonly metadataExtractor?: (entity: Record<string, unknown>) => TMeta;
62
- }): LegacyArrayAzureTableCodec<TMeta>;
63
- declare const createLegacyAzureTableCodec: (options?: {
64
- readonly partitionKeyGenerator?: (formId: string, submissionId: string) => string;
65
- readonly rowKeyGenerator?: (submittedAt: string, submissionId: string) => string;
66
- }) => AzureTableLegacyCodec;
25
+ type AzureTableSubmissionEntity = Record<string, unknown> & {
26
+ readonly answers?: never;
27
+ };
67
28
  interface AzureTableSubmissionCodec<T = FormSubmission> {
68
- readonly createEntity: (value: T) => Record<string, unknown>;
69
- readonly deserialize: (entity: Record<string, unknown>) => T;
70
- readonly matchesEntity: (entity: Record<string, unknown>) => boolean;
29
+ readonly createEntity: (value: T) => AzureTableSubmissionEntity;
30
+ readonly deserialize: (entity: AzureTableSubmissionEntity) => T;
31
+ readonly matchesEntity: (entity: AzureTableSubmissionEntity) => boolean;
71
32
  readonly createPartitionKey: (value: T) => string;
72
33
  readonly createPartitionKeyFromQuery: (formId: string, query: SubmissionPageQueryOptions) => string | undefined;
73
34
  readonly createRowKey: (value: T) => string;
@@ -96,16 +57,10 @@ interface AzureTableStorageOptions<T = FormSubmission> {
96
57
  readonly query?: SubmissionPageQueryOptions;
97
58
  }) => AzureTableClientLike | Promise<AzureTableClientLike>;
98
59
  readonly codec?: AzureTableSubmissionCodec<T> | AzureTableValueCodec;
99
- /** @deprecated Use codec. */
100
- readonly submissionCodec?: AzureTableEntityCodec<FormSubmission>;
101
60
  readonly buildSubmissionFilter?: (formId: string, query: SubmissionPageQueryOptions) => string;
102
- /** @deprecated Use buildSubmissionFilter. */
103
- readonly toODataFilter?: (options: SubmissionPageQueryOptions) => string;
104
61
  readonly maxScanPages?: number;
105
62
  readonly fieldMapping?: AzureTableFieldMapping;
106
63
  readonly readOnly?: boolean;
107
- /** Detect legacy entities that expose an `answers` column and fail explicitly. */
108
- readonly rejectLegacyAnswers?: boolean;
109
64
  /** Enable typed duplicate/conflict results for submission IDs. */
110
65
  readonly idempotentSubmissions?: boolean;
111
66
  /** Alias for idempotentSubmissions. */
@@ -121,6 +76,7 @@ interface AzureTableStorageOptions<T = FormSubmission> {
121
76
  readonly schema?: FormSubmissionValidationSource;
122
77
  }
123
78
  interface AzureTableStorageAdapter<TMeta extends BaseSubmissionMetadata = BaseSubmissionMetadata> extends PagedSubmissionStorageAdapter {
79
+ readonly listTextAnswerPage: (formId: string, fieldIdOrOptions?: string | TextAnswerPageQueryOptions, options?: TextAnswerPageQueryOptions) => Promise<TextAnswerPage>;
124
80
  readonly fetchSubmissionPage?: (formId: string, options?: TypedSubmissionPageQueryOptions<TMeta>) => Promise<TypedSubmissionPage<TMeta>>;
125
81
  readonly fetchPage: (formId: string, options?: {
126
82
  readonly pageSize?: number;
@@ -133,6 +89,9 @@ interface AzureTableStorageAdapter<TMeta extends BaseSubmissionMetadata = BaseSu
133
89
  readonly items: readonly FormSubmission<TMeta>[];
134
90
  readonly nextCursor?: string;
135
91
  }>;
92
+ readonly aggregateResponses: (schema: FormSchema, options?: SubmissionPageQueryOptions) => Promise<FormAnalytics>;
93
+ readonly exportResponsesToCsv: (schema: FormSchema, options?: StorageSubmissionExportOptions) => Promise<string>;
94
+ readonly validateSubmission: (submission: FormSubmission, source?: FormSubmissionValidationSource) => Promise<void>;
136
95
  }
137
96
  type TypedAzureTableStorageAdapter<TMeta extends BaseSubmissionMetadata | undefined = undefined> = Omit<PagedSubmissionStorageAdapter, "saveSubmission" | "listSubmissionPage" | "listTextAnswerPage"> & {
138
97
  readonly saveSubmission: (submission: FormSubmission<TMeta>, options?: SaveSubmissionOptions) => Promise<undefined | SubmissionSaveResult<TMeta>>;
@@ -157,6 +116,9 @@ type TypedAzureTableStorageAdapter<TMeta extends BaseSubmissionMetadata | undefi
157
116
  }>;
158
117
  readonly fetchSubmissionPage?: (formId: string, options?: TypedSubmissionPageQueryOptions<TMeta>) => Promise<TypedSubmissionPage<TMeta>>;
159
118
  readonly listTextAnswerPage: (formId: string, fieldIdOrOptions?: string | TextAnswerPageQueryOptions, options?: TextAnswerPageQueryOptions) => Promise<TypedTextAnswerPage<TMeta>>;
119
+ readonly aggregateResponses: (schema: FormSchema, options?: TypedSubmissionPageQueryOptions<TMeta>) => Promise<FormAnalytics>;
120
+ readonly exportResponsesToCsv: (schema: FormSchema, options?: StorageSubmissionExportOptions<TMeta>) => Promise<string>;
121
+ readonly validateSubmission: (submission: FormSubmission<TMeta>, source?: FormSubmissionValidationSource<TMeta>) => Promise<void>;
160
122
  };
161
123
  interface AzureTextAnswerCursorPayload {
162
124
  readonly formatVersion: 1;
@@ -172,7 +134,7 @@ declare function createAzureTableSubmissionCodec<TMeta extends BaseSubmissionMet
172
134
  declare const defaultAzureTableSubmissionCodec: AzureTableSubmissionCodec<FormSubmission>;
173
135
  declare function metadataFiltersToOData(options: SubmissionPageQueryOptions, mapping?: AzureTableFieldMapping): string;
174
136
  declare function submissionFilterToOData(filter: SubmissionFilter, mapping?: AzureTableFieldMapping): string | undefined;
175
- declare function createAzureTableStorage(options?: AzureTableStorageOptions): PagedSubmissionStorageAdapter;
137
+ declare function createAzureTableStorage(options?: AzureTableStorageOptions): AzureTableStorageAdapter;
176
138
  declare function createAzureTableStorage<TMeta extends BaseSubmissionMetadata | undefined = undefined>(options?: AzureTableStorageOptions): TypedAzureTableStorageAdapter<TMeta>;
177
139
 
178
- export { type AzureTableClientLike, type AzureTableEntityCodec, type AzureTableEntityIterator, type AzureTableEntityPage, type AzureTableFieldMapping, type AzureTableLegacyCodec, type AzureTableLegacyEntity, type AzureTableListOptions, type AzureTablePageSettings, type AzureTableStorageAdapter, type AzureTableStorageOptions, type AzureTableSubmissionCodec, type AzureTableValueCodec, type AzureTextAnswerCursorPayload, type LegacyAnswerArrayEntity, type LegacyArrayAzureTableCodec, type TypedAzureTableStorageAdapter, createAzureTableStorage, createAzureTableSubmissionCodec, createLegacyArrayAzureTableCodec, createLegacyAzureTableCodec, defaultAzureTableSubmissionCodec, metadataFiltersToOData, submissionFilterToOData };
140
+ export { type AzureTableClientLike, type AzureTableEntityIterator, type AzureTableEntityPage, type AzureTableFieldMapping, type AzureTableListOptions, type AzureTablePageSettings, type AzureTableStorageAdapter, type AzureTableStorageOptions, type AzureTableSubmissionCodec, type AzureTableSubmissionEntity, type AzureTableValueCodec, type AzureTextAnswerCursorPayload, type TypedAzureTableStorageAdapter, createAzureTableStorage, createAzureTableSubmissionCodec, defaultAzureTableSubmissionCodec, metadataFiltersToOData, submissionFilterToOData };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { FormSubmission, BaseSubmissionMetadata, PagedSubmissionStorageAdapter, TypedSubmissionPageQueryOptions, TypedSubmissionPage, SubmissionPageQueryOptions, FormSubmissionValidationSource, FormSubmissionValidator, StrictFormSubmission, SaveSubmissionOptions, SubmissionSaveResult, SubmissionFilter, JsonValue, TextAnswerPageQueryOptions, TypedTextAnswerPage } from '@form-engine-ts/core';
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';
2
2
 
3
3
  interface AzureTableListOptions {
4
4
  readonly queryOptions?: {
@@ -22,52 +22,13 @@ interface AzureTableClientLike {
22
22
  listEntities(options?: AzureTableListOptions): AzureTableEntityIterator;
23
23
  deleteEntity(partitionKey: string, rowKey: string): Promise<unknown>;
24
24
  }
25
- /** @deprecated Use AzureTableSubmissionCodec. */
26
- interface AzureTableEntityCodec<T> {
27
- readonly createPartitionKey: (submission: T) => string;
28
- readonly createPartitionKeyFromFormId?: (formId: string) => string;
29
- readonly createRowKey: (submission: T) => string;
30
- readonly serialize: (submission: T) => Record<string, unknown>;
31
- readonly deserialize: (entity: Record<string, unknown>) => T;
32
- }
33
- interface AzureTableLegacyEntity {
34
- readonly PartitionKey: string;
35
- readonly RowKey: string;
36
- readonly answers?: string;
37
- readonly answeredAt?: string;
38
- readonly surveyVersion?: number;
39
- readonly Timestamp?: string;
40
- readonly [key: string]: unknown;
41
- }
42
- interface AzureTableLegacyCodec {
43
- readonly decode: (entity: AzureTableLegacyEntity) => FormSubmission;
44
- readonly createPartitionKey: (formId: string, submissionId: string) => string;
45
- readonly createRowKey: (submittedAt: string, submissionId: string) => string;
46
- }
47
- interface LegacyAnswerArrayEntity extends Record<string, unknown> {
48
- readonly PartitionKey: string;
49
- readonly RowKey: string;
50
- readonly answers: string;
51
- readonly answeredAt: string;
52
- readonly surveyVersion: number;
53
- readonly locale?: string;
54
- }
55
- interface LegacyArrayAzureTableCodec<TMeta extends BaseSubmissionMetadata = BaseSubmissionMetadata> {
56
- readonly encode: (submission: StrictFormSubmission<TMeta>) => Record<string, unknown>;
57
- readonly decode: (entity: Record<string, unknown>) => StrictFormSubmission<TMeta>;
58
- }
59
- declare function createLegacyArrayAzureTableCodec<TMeta extends BaseSubmissionMetadata = BaseSubmissionMetadata>(options?: {
60
- readonly defaultLocale?: string;
61
- readonly metadataExtractor?: (entity: Record<string, unknown>) => TMeta;
62
- }): LegacyArrayAzureTableCodec<TMeta>;
63
- declare const createLegacyAzureTableCodec: (options?: {
64
- readonly partitionKeyGenerator?: (formId: string, submissionId: string) => string;
65
- readonly rowKeyGenerator?: (submittedAt: string, submissionId: string) => string;
66
- }) => AzureTableLegacyCodec;
25
+ type AzureTableSubmissionEntity = Record<string, unknown> & {
26
+ readonly answers?: never;
27
+ };
67
28
  interface AzureTableSubmissionCodec<T = FormSubmission> {
68
- readonly createEntity: (value: T) => Record<string, unknown>;
69
- readonly deserialize: (entity: Record<string, unknown>) => T;
70
- readonly matchesEntity: (entity: Record<string, unknown>) => boolean;
29
+ readonly createEntity: (value: T) => AzureTableSubmissionEntity;
30
+ readonly deserialize: (entity: AzureTableSubmissionEntity) => T;
31
+ readonly matchesEntity: (entity: AzureTableSubmissionEntity) => boolean;
71
32
  readonly createPartitionKey: (value: T) => string;
72
33
  readonly createPartitionKeyFromQuery: (formId: string, query: SubmissionPageQueryOptions) => string | undefined;
73
34
  readonly createRowKey: (value: T) => string;
@@ -96,16 +57,10 @@ interface AzureTableStorageOptions<T = FormSubmission> {
96
57
  readonly query?: SubmissionPageQueryOptions;
97
58
  }) => AzureTableClientLike | Promise<AzureTableClientLike>;
98
59
  readonly codec?: AzureTableSubmissionCodec<T> | AzureTableValueCodec;
99
- /** @deprecated Use codec. */
100
- readonly submissionCodec?: AzureTableEntityCodec<FormSubmission>;
101
60
  readonly buildSubmissionFilter?: (formId: string, query: SubmissionPageQueryOptions) => string;
102
- /** @deprecated Use buildSubmissionFilter. */
103
- readonly toODataFilter?: (options: SubmissionPageQueryOptions) => string;
104
61
  readonly maxScanPages?: number;
105
62
  readonly fieldMapping?: AzureTableFieldMapping;
106
63
  readonly readOnly?: boolean;
107
- /** Detect legacy entities that expose an `answers` column and fail explicitly. */
108
- readonly rejectLegacyAnswers?: boolean;
109
64
  /** Enable typed duplicate/conflict results for submission IDs. */
110
65
  readonly idempotentSubmissions?: boolean;
111
66
  /** Alias for idempotentSubmissions. */
@@ -121,6 +76,7 @@ interface AzureTableStorageOptions<T = FormSubmission> {
121
76
  readonly schema?: FormSubmissionValidationSource;
122
77
  }
123
78
  interface AzureTableStorageAdapter<TMeta extends BaseSubmissionMetadata = BaseSubmissionMetadata> extends PagedSubmissionStorageAdapter {
79
+ readonly listTextAnswerPage: (formId: string, fieldIdOrOptions?: string | TextAnswerPageQueryOptions, options?: TextAnswerPageQueryOptions) => Promise<TextAnswerPage>;
124
80
  readonly fetchSubmissionPage?: (formId: string, options?: TypedSubmissionPageQueryOptions<TMeta>) => Promise<TypedSubmissionPage<TMeta>>;
125
81
  readonly fetchPage: (formId: string, options?: {
126
82
  readonly pageSize?: number;
@@ -133,6 +89,9 @@ interface AzureTableStorageAdapter<TMeta extends BaseSubmissionMetadata = BaseSu
133
89
  readonly items: readonly FormSubmission<TMeta>[];
134
90
  readonly nextCursor?: string;
135
91
  }>;
92
+ readonly aggregateResponses: (schema: FormSchema, options?: SubmissionPageQueryOptions) => Promise<FormAnalytics>;
93
+ readonly exportResponsesToCsv: (schema: FormSchema, options?: StorageSubmissionExportOptions) => Promise<string>;
94
+ readonly validateSubmission: (submission: FormSubmission, source?: FormSubmissionValidationSource) => Promise<void>;
136
95
  }
137
96
  type TypedAzureTableStorageAdapter<TMeta extends BaseSubmissionMetadata | undefined = undefined> = Omit<PagedSubmissionStorageAdapter, "saveSubmission" | "listSubmissionPage" | "listTextAnswerPage"> & {
138
97
  readonly saveSubmission: (submission: FormSubmission<TMeta>, options?: SaveSubmissionOptions) => Promise<undefined | SubmissionSaveResult<TMeta>>;
@@ -157,6 +116,9 @@ type TypedAzureTableStorageAdapter<TMeta extends BaseSubmissionMetadata | undefi
157
116
  }>;
158
117
  readonly fetchSubmissionPage?: (formId: string, options?: TypedSubmissionPageQueryOptions<TMeta>) => Promise<TypedSubmissionPage<TMeta>>;
159
118
  readonly listTextAnswerPage: (formId: string, fieldIdOrOptions?: string | TextAnswerPageQueryOptions, options?: TextAnswerPageQueryOptions) => Promise<TypedTextAnswerPage<TMeta>>;
119
+ readonly aggregateResponses: (schema: FormSchema, options?: TypedSubmissionPageQueryOptions<TMeta>) => Promise<FormAnalytics>;
120
+ readonly exportResponsesToCsv: (schema: FormSchema, options?: StorageSubmissionExportOptions<TMeta>) => Promise<string>;
121
+ readonly validateSubmission: (submission: FormSubmission<TMeta>, source?: FormSubmissionValidationSource<TMeta>) => Promise<void>;
160
122
  };
161
123
  interface AzureTextAnswerCursorPayload {
162
124
  readonly formatVersion: 1;
@@ -172,7 +134,7 @@ declare function createAzureTableSubmissionCodec<TMeta extends BaseSubmissionMet
172
134
  declare const defaultAzureTableSubmissionCodec: AzureTableSubmissionCodec<FormSubmission>;
173
135
  declare function metadataFiltersToOData(options: SubmissionPageQueryOptions, mapping?: AzureTableFieldMapping): string;
174
136
  declare function submissionFilterToOData(filter: SubmissionFilter, mapping?: AzureTableFieldMapping): string | undefined;
175
- declare function createAzureTableStorage(options?: AzureTableStorageOptions): PagedSubmissionStorageAdapter;
137
+ declare function createAzureTableStorage(options?: AzureTableStorageOptions): AzureTableStorageAdapter;
176
138
  declare function createAzureTableStorage<TMeta extends BaseSubmissionMetadata | undefined = undefined>(options?: AzureTableStorageOptions): TypedAzureTableStorageAdapter<TMeta>;
177
139
 
178
- export { type AzureTableClientLike, type AzureTableEntityCodec, type AzureTableEntityIterator, type AzureTableEntityPage, type AzureTableFieldMapping, type AzureTableLegacyCodec, type AzureTableLegacyEntity, type AzureTableListOptions, type AzureTablePageSettings, type AzureTableStorageAdapter, type AzureTableStorageOptions, type AzureTableSubmissionCodec, type AzureTableValueCodec, type AzureTextAnswerCursorPayload, type LegacyAnswerArrayEntity, type LegacyArrayAzureTableCodec, type TypedAzureTableStorageAdapter, createAzureTableStorage, createAzureTableSubmissionCodec, createLegacyArrayAzureTableCodec, createLegacyAzureTableCodec, defaultAzureTableSubmissionCodec, metadataFiltersToOData, submissionFilterToOData };
140
+ export { type AzureTableClientLike, type AzureTableEntityIterator, type AzureTableEntityPage, type AzureTableFieldMapping, type AzureTableListOptions, type AzureTablePageSettings, type AzureTableStorageAdapter, type AzureTableStorageOptions, type AzureTableSubmissionCodec, type AzureTableSubmissionEntity, type AzureTableValueCodec, type AzureTextAnswerCursorPayload, type TypedAzureTableStorageAdapter, createAzureTableStorage, createAzureTableSubmissionCodec, defaultAzureTableSubmissionCodec, metadataFiltersToOData, submissionFilterToOData };
package/dist/index.js CHANGED
@@ -1,86 +1,14 @@
1
1
  // src/index.ts
2
2
  import {
3
+ aggregateResponses,
3
4
  assertValidFormSchema,
4
5
  assertValidFormSubmission,
5
6
  assertValidFormSubmissionWith,
7
+ exportResponsesToCsv,
6
8
  hashFormSubmissionPayload,
7
9
  matchesSubmissionPageFilters,
8
10
  normalizeSubmissionPageSize
9
11
  } from "@form-engine-ts/core";
10
- function createLegacyArrayAzureTableCodec(options = {}) {
11
- const defaultLocale = options.defaultLocale ?? "ja";
12
- if (defaultLocale.trim().length === 0) throw new TypeError("defaultLocale must not be empty.");
13
- return {
14
- encode: (submission) => ({
15
- PartitionKey: submission.formId,
16
- RowKey: submission.id,
17
- answers: JSON.stringify(Object.entries(submission.values).map(([questionId, value]) => ({ questionId, value }))),
18
- answeredAt: submission.submittedAt,
19
- surveyVersion: submission.formVersion,
20
- locale: submission.locale,
21
- ...submission.metadata
22
- }),
23
- decode: (entity) => {
24
- const raw = entity;
25
- if (typeof raw.PartitionKey !== "string" || typeof raw.RowKey !== "string") {
26
- throw new Error("Azure Table legacy array submission is missing PartitionKey or RowKey.");
27
- }
28
- const submittedAt = typeof raw.answeredAt === "string" ? raw.answeredAt : entity.Timestamp;
29
- if (typeof submittedAt !== "string" || submittedAt.trim().length === 0) {
30
- throw new Error("Azure Table legacy array submission is missing answeredAt or Timestamp.");
31
- }
32
- let values = {};
33
- if (typeof raw.answers === "string") {
34
- const parsed = parseJson(raw.answers, "legacy array answers");
35
- if (Array.isArray(parsed)) {
36
- values = Object.fromEntries(
37
- parsed.flatMap((item) => {
38
- if (!isRecord(item) || typeof item.questionId !== "string") return [];
39
- return [[item.questionId, item.value]];
40
- })
41
- );
42
- } else if (isRecord(parsed)) {
43
- values = parsed;
44
- } else {
45
- throw new Error("Azure Table legacy array answers payload must be an array or object.");
46
- }
47
- }
48
- const formVersion = Number(raw.surveyVersion);
49
- return {
50
- id: raw.RowKey,
51
- formId: raw.PartitionKey,
52
- formVersion: Number.isSafeInteger(formVersion) && formVersion > 0 ? formVersion : 1,
53
- values,
54
- locale: typeof raw.locale === "string" && raw.locale.trim().length > 0 ? raw.locale : defaultLocale,
55
- metadata: options.metadataExtractor?.(entity) ?? {},
56
- submittedAt
57
- };
58
- }
59
- };
60
- }
61
- var createLegacyAzureTableCodec = (options = {}) => {
62
- const createPartitionKey = options.partitionKeyGenerator ?? ((formId) => formId);
63
- const createRowKey = options.rowKeyGenerator ?? ((submittedAt, submissionId) => `${submittedAt}_${submissionId}`);
64
- return {
65
- decode: (entity) => {
66
- const values = entity.answers === void 0 ? {} : parseLegacyJsonObject(entity.answers, "answers");
67
- const submittedAt = entity.answeredAt ?? entity.Timestamp;
68
- if (submittedAt === void 0 || submittedAt.trim().length === 0) {
69
- throw new Error("Azure Table legacy submission is missing answeredAt or Timestamp.");
70
- }
71
- return {
72
- id: entity.RowKey,
73
- formId: entity.PartitionKey,
74
- formVersion: entity.surveyVersion ?? 1,
75
- values,
76
- metadata: {},
77
- submittedAt
78
- };
79
- },
80
- createPartitionKey: (formId, submissionId) => createPartitionKey(formId, submissionId),
81
- createRowKey: (submittedAt, submissionId) => createRowKey(submittedAt, submissionId)
82
- };
83
- };
84
12
  var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
85
13
  function encodeBase64(bytes) {
86
14
  let result = "";
@@ -189,11 +117,6 @@ function parseJson(value, location) {
189
117
  throw new Error(`Azure Table ${location} payload is invalid.`, { cause });
190
118
  }
191
119
  }
192
- function parseLegacyJsonObject(value, property) {
193
- const parsed = parseJson(value, `legacy ${property}`);
194
- if (!isRecord(parsed)) throw new Error(`Azure Table legacy ${property} payload must be an object.`);
195
- return parsed;
196
- }
197
120
  function parseSubmission(value, location) {
198
121
  const parsed = typeof value === "string" ? parseJson(value, location) : value;
199
122
  if (!isRecord(parsed) || typeof parsed.id !== "string" || typeof parsed.formId !== "string" || !Number.isInteger(parsed.formVersion) || typeof parsed.locale !== "string" || typeof parsed.submittedAt !== "string" || !isRecord(parsed.values) || !Object.values(parsed.values).every(isFormValue)) {
@@ -289,22 +212,21 @@ function createAzureTableSubmissionCodec() {
289
212
  };
290
213
  }
291
214
  var defaultAzureTableSubmissionCodec = createAzureTableSubmissionCodec();
292
- function legacyCodec(codec) {
293
- return {
294
- createEntity: (value) => ({
295
- ...codec.serialize(value),
296
- kind: "submission",
297
- formVersion: value.formVersion,
298
- locale: value.locale,
299
- submittedAt: value.submittedAt,
300
- responseId: value.id
301
- }),
302
- deserialize: codec.deserialize,
303
- matchesEntity: (entity) => entity.kind === "submission",
304
- createPartitionKey: codec.createPartitionKey,
305
- createPartitionKeyFromQuery: (formId) => codec.createPartitionKeyFromFormId?.(formId) ?? formId,
306
- createRowKey: codec.createRowKey
307
- };
215
+ async function collectSubmissionPages(fetchPage, formId, options) {
216
+ const submissions = [];
217
+ const seen = /* @__PURE__ */ new Set();
218
+ let cursor = options.cursor;
219
+ do {
220
+ if (cursor !== void 0) {
221
+ if (seen.has(cursor)) throw new TypeError("Submission pagination cursor cycle detected.");
222
+ seen.add(cursor);
223
+ }
224
+ const page = await fetchPage(formId, { ...options, ...cursor === void 0 ? {} : { cursor } });
225
+ submissions.push(...page.items);
226
+ if (!page.hasMore || page.nextCursor === void 0 || page.nextCursor.length === 0) break;
227
+ cursor = page.nextCursor;
228
+ } while (cursor !== void 0);
229
+ return submissions;
308
230
  }
309
231
  function parseSchemaEntity(value) {
310
232
  if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || value.kind !== "schema" || !Number.isInteger(value.formVersion) || typeof value.payload !== "string") {
@@ -331,6 +253,11 @@ function parseSubmissionEntity(value, codec, mapping, valueCodec) {
331
253
  }
332
254
  return submission;
333
255
  }
256
+ function assertCanonicalSubmissionEntity(value) {
257
+ if (Object.hasOwn(value, "answers")) {
258
+ throw new Error("Azure Table legacy answers column is not supported by the standard submission storage.");
259
+ }
260
+ }
334
261
  function escapeOData(value) {
335
262
  return value.replaceAll("'", "''");
336
263
  }
@@ -383,19 +310,19 @@ function submissionFilterToOData(filter, mapping) {
383
310
  ...filter.to === void 0 ? [] : [`${property} le ${valueToOData(filter.to)}`]
384
311
  ].join(" and ");
385
312
  }
386
- function defaultSubmissionFilter(codec, formId, options, legacyExtension, mapping, rejectLegacyAnswers = false) {
313
+ function defaultSubmissionFilter(codec, formId, options, customExtension, mapping) {
387
314
  const partitionKey = codec.createPartitionKeyFromQuery(formId, options);
388
315
  const ast = options.filter === void 0 || typeof options.filter === "function" ? void 0 : submissionFilterToOData(options.filter, mapping);
389
316
  const keys = physicalKeyNames(mapping);
390
317
  return [
391
318
  ...partitionKey === void 0 ? [] : [`${keys.partitionKey} eq '${escapeOData(partitionKey)}'`],
392
- ...codec === defaultAzureTableSubmissionCodec && !rejectLegacyAnswers ? ["kind eq 'submission'"] : [],
319
+ ...codec === defaultAzureTableSubmissionCodec ? ["kind eq 'submission'"] : [],
393
320
  ...options.version === void 0 ? [] : [`${propertyName(mapping, "formVersion", "formVersion")} eq ${options.version}`],
394
321
  ...options.since === void 0 ? [] : [`${propertyName(mapping, "submittedAt", "submittedAt")} ge '${escapeOData(options.since)}'`],
395
322
  ...options.until === void 0 ? [] : [`${propertyName(mapping, "submittedAt", "submittedAt")} le '${escapeOData(options.until)}'`],
396
323
  ...options.locale === void 0 ? [] : [`locale eq '${escapeOData(options.locale)}'`],
397
324
  ...ast === void 0 || ast.length === 0 ? [] : [`(${ast})`],
398
- ...legacyExtension.trim().length === 0 ? [] : [`(${legacyExtension})`]
325
+ ...customExtension.trim().length === 0 ? [] : [`(${customExtension})`]
399
326
  ].join(" and ");
400
327
  }
401
328
  function isNotFound(error) {
@@ -417,7 +344,7 @@ function createAzureTableStorage(options = {}) {
417
344
  const staticSubmissions = options.submissionsTableClient ?? options.client;
418
345
  const configuredCodec = options.codec;
419
346
  const valueCodec = configuredCodec !== void 0 && "encodeValues" in configuredCodec ? configuredCodec : void 0;
420
- const codec = (configuredCodec !== void 0 && "createEntity" in configuredCodec ? configuredCodec : void 0) ?? (options.submissionCodec === void 0 ? defaultAzureTableSubmissionCodec : legacyCodec(options.submissionCodec));
347
+ const codec = (configuredCodec !== void 0 && "createEntity" in configuredCodec ? configuredCodec : void 0) ?? defaultAzureTableSubmissionCodec;
421
348
  const maxScanPages = options.maxScanPages ?? 5;
422
349
  if (!Number.isSafeInteger(maxScanPages) || maxScanPages < 1) {
423
350
  throw new TypeError("maxScanPages must be a positive safe integer.");
@@ -434,21 +361,15 @@ function createAzureTableStorage(options = {}) {
434
361
  codec,
435
362
  formId,
436
363
  query,
437
- options.toODataFilter?.(query) ?? metadataFiltersToOData(query, options.fieldMapping),
438
- options.fieldMapping,
439
- options.rejectLegacyAnswers
364
+ metadataFiltersToOData(query, options.fieldMapping),
365
+ options.fieldMapping
440
366
  );
441
367
  };
442
368
  const ensureWritable = () => {
443
369
  if (options.readOnly === true) throw new Error("Azure Table storage is read-only.");
444
370
  };
445
- const rejectLegacyEntity = (entity) => {
446
- if (options.rejectLegacyAnswers === true && Object.hasOwn(entity, "answers")) {
447
- throw new Error("Azure Table legacy answers column is not supported by the standard submission storage.");
448
- }
449
- };
450
371
  const deserializeIfMatching = (entity) => {
451
- rejectLegacyEntity(entity);
372
+ assertCanonicalSubmissionEntity(entity);
452
373
  return codec.matchesEntity(entity) ? parseSubmissionEntity(entity, codec, options.fieldMapping, valueCodec) : void 0;
453
374
  };
454
375
  const listSubmissionCandidates = async (formId, query) => {
@@ -511,7 +432,9 @@ function createAzureTableStorage(options = {}) {
511
432
  assertValidFormSubmission(schema, stored);
512
433
  }
513
434
  const payloadHash = await hashFormSubmissionPayload(stored);
514
- const mappedEntity = mappedSubmissionEntity(codec.createEntity(stored), stored, options.fieldMapping, valueCodec);
435
+ const createdEntity = codec.createEntity(stored);
436
+ assertCanonicalSubmissionEntity(createdEntity);
437
+ const mappedEntity = mappedSubmissionEntity(createdEntity, stored, options.fieldMapping, valueCodec);
515
438
  const keys = physicalKeyNames(options.fieldMapping);
516
439
  const partitionKey = codec.createPartitionKey(stored);
517
440
  const rowKey = codec.createRowKey(stored);
@@ -680,6 +603,44 @@ function createAzureTableStorage(options = {}) {
680
603
  })
681
604
  };
682
605
  },
606
+ async aggregateResponses(schema, options2 = {}) {
607
+ const query = { ...options2, version: options2.version ?? schema.version };
608
+ const items = await collectSubmissionPages(
609
+ (formId, pageOptions) => adapter.listSubmissionPage(formId, pageOptions),
610
+ schema.id,
611
+ query
612
+ );
613
+ return aggregateResponses(schema, items);
614
+ },
615
+ async exportResponsesToCsv(schema, options2 = {}) {
616
+ const { query, ...csvOptions } = options2;
617
+ const items = await collectSubmissionPages(
618
+ (formId, pageOptions) => adapter.listSubmissionPage(formId, pageOptions),
619
+ schema.id,
620
+ { ...query ?? {}, version: query?.version ?? schema.version }
621
+ );
622
+ const exportOptions = {
623
+ ...csvOptions.withBom === void 0 ? {} : { withBom: csvOptions.withBom },
624
+ ...csvOptions.useBom === void 0 ? {} : { useBom: csvOptions.useBom },
625
+ ...csvOptions.neutralizeFormulas === void 0 ? {} : { neutralizeFormulas: csvOptions.neutralizeFormulas },
626
+ ...csvOptions.preventFormulaInjection === void 0 ? {} : { preventFormulaInjection: csvOptions.preventFormulaInjection },
627
+ ...csvOptions.includeLocale === void 0 ? {} : { includeLocale: csvOptions.includeLocale },
628
+ ...csvOptions.includePiiStatus === void 0 ? {} : { includePiiStatus: csvOptions.includePiiStatus },
629
+ ...csvOptions.customColumns === void 0 ? {} : { customColumns: csvOptions.customColumns },
630
+ ...csvOptions.includeMetadataFields === void 0 ? {} : { includeMetadataFields: csvOptions.includeMetadataFields }
631
+ };
632
+ const exportItems = items.map((item) => ({ ...item, metadata: item.metadata ?? {} }));
633
+ return exportResponsesToCsv(schema, exportItems, exportOptions);
634
+ },
635
+ async validateSubmission(submission, source) {
636
+ if (source !== void 0) {
637
+ await assertValidFormSubmissionWith(source, submission);
638
+ return;
639
+ }
640
+ const schema = await adapter.getSchema(submission.formId, submission.formVersion);
641
+ if (schema === null) throw new Error("Azure Table submission schema was not found.");
642
+ assertValidFormSubmission(schema, submission);
643
+ },
683
644
  async deleteSubmission(submissionId) {
684
645
  ensureWritable();
685
646
  const client = await submissionClient("");
@@ -749,8 +710,6 @@ function createAzureTableStorage(options = {}) {
749
710
  export {
750
711
  createAzureTableStorage,
751
712
  createAzureTableSubmissionCodec,
752
- createLegacyArrayAzureTableCodec,
753
- createLegacyAzureTableCodec,
754
713
  defaultAzureTableSubmissionCodec,
755
714
  metadataFiltersToOData,
756
715
  submissionFilterToOData
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/storage-azure-table",
3
- "version": "6.10.0",
3
+ "version": "7.0.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": "6.10.0"
42
+ "@form-engine-ts/core": "7.0.0"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "@azure/data-tables": "^13.3.2"