@form-engine-ts/storage-azure-table 6.9.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 +8 -7
- package/dist/index.cjs +74 -113
- package/dist/index.d.cts +18 -54
- package/dist/index.d.ts +18 -54
- package/dist/index.js +76 -111
- package/package.json +2 -2
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
|
|
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.
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
59
|
-
|
|
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
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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,
|
|
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
|
|
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
|
-
...
|
|
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) ??
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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("");
|
|
@@ -747,6 +704,12 @@ function createAzureTableStorage(options = {}) {
|
|
|
747
704
|
};
|
|
748
705
|
return {
|
|
749
706
|
...adapter,
|
|
707
|
+
async fetchSubmissionPage(formId, options2) {
|
|
708
|
+
return await adapter.listSubmissionPage(
|
|
709
|
+
formId,
|
|
710
|
+
options2
|
|
711
|
+
);
|
|
712
|
+
},
|
|
750
713
|
async fetchPage(formId, options2) {
|
|
751
714
|
const page = await adapter.listSubmissionPage(formId, {
|
|
752
715
|
...options2?.pageSize === void 0 ? {} : { pageSize: options2.pageSize },
|
|
@@ -767,8 +730,6 @@ function createAzureTableStorage(options = {}) {
|
|
|
767
730
|
0 && (module.exports = {
|
|
768
731
|
createAzureTableStorage,
|
|
769
732
|
createAzureTableSubmissionCodec,
|
|
770
|
-
createLegacyArrayAzureTableCodec,
|
|
771
|
-
createLegacyAzureTableCodec,
|
|
772
733
|
defaultAzureTableSubmissionCodec,
|
|
773
734
|
metadataFiltersToOData,
|
|
774
735
|
submissionFilterToOData
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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) =>
|
|
69
|
-
readonly deserialize: (entity:
|
|
70
|
-
readonly matchesEntity: (entity:
|
|
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,8 @@ 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>;
|
|
80
|
+
readonly fetchSubmissionPage?: (formId: string, options?: TypedSubmissionPageQueryOptions<TMeta>) => Promise<TypedSubmissionPage<TMeta>>;
|
|
124
81
|
readonly fetchPage: (formId: string, options?: {
|
|
125
82
|
readonly pageSize?: number;
|
|
126
83
|
readonly fromSubmittedAt?: string;
|
|
@@ -132,6 +89,9 @@ interface AzureTableStorageAdapter<TMeta extends BaseSubmissionMetadata = BaseSu
|
|
|
132
89
|
readonly items: readonly FormSubmission<TMeta>[];
|
|
133
90
|
readonly nextCursor?: string;
|
|
134
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>;
|
|
135
95
|
}
|
|
136
96
|
type TypedAzureTableStorageAdapter<TMeta extends BaseSubmissionMetadata | undefined = undefined> = Omit<PagedSubmissionStorageAdapter, "saveSubmission" | "listSubmissionPage" | "listTextAnswerPage"> & {
|
|
137
97
|
readonly saveSubmission: (submission: FormSubmission<TMeta>, options?: SaveSubmissionOptions) => Promise<undefined | SubmissionSaveResult<TMeta>>;
|
|
@@ -154,7 +114,11 @@ type TypedAzureTableStorageAdapter<TMeta extends BaseSubmissionMetadata | undefi
|
|
|
154
114
|
readonly items: readonly FormSubmission<TMeta>[];
|
|
155
115
|
readonly nextCursor?: string;
|
|
156
116
|
}>;
|
|
117
|
+
readonly fetchSubmissionPage?: (formId: string, options?: TypedSubmissionPageQueryOptions<TMeta>) => Promise<TypedSubmissionPage<TMeta>>;
|
|
157
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>;
|
|
158
122
|
};
|
|
159
123
|
interface AzureTextAnswerCursorPayload {
|
|
160
124
|
readonly formatVersion: 1;
|
|
@@ -170,7 +134,7 @@ declare function createAzureTableSubmissionCodec<TMeta extends BaseSubmissionMet
|
|
|
170
134
|
declare const defaultAzureTableSubmissionCodec: AzureTableSubmissionCodec<FormSubmission>;
|
|
171
135
|
declare function metadataFiltersToOData(options: SubmissionPageQueryOptions, mapping?: AzureTableFieldMapping): string;
|
|
172
136
|
declare function submissionFilterToOData(filter: SubmissionFilter, mapping?: AzureTableFieldMapping): string | undefined;
|
|
173
|
-
declare function createAzureTableStorage(options?: AzureTableStorageOptions):
|
|
137
|
+
declare function createAzureTableStorage(options?: AzureTableStorageOptions): AzureTableStorageAdapter;
|
|
174
138
|
declare function createAzureTableStorage<TMeta extends BaseSubmissionMetadata | undefined = undefined>(options?: AzureTableStorageOptions): TypedAzureTableStorageAdapter<TMeta>;
|
|
175
139
|
|
|
176
|
-
export { type AzureTableClientLike, type
|
|
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 {
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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) =>
|
|
69
|
-
readonly deserialize: (entity:
|
|
70
|
-
readonly matchesEntity: (entity:
|
|
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,8 @@ 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>;
|
|
80
|
+
readonly fetchSubmissionPage?: (formId: string, options?: TypedSubmissionPageQueryOptions<TMeta>) => Promise<TypedSubmissionPage<TMeta>>;
|
|
124
81
|
readonly fetchPage: (formId: string, options?: {
|
|
125
82
|
readonly pageSize?: number;
|
|
126
83
|
readonly fromSubmittedAt?: string;
|
|
@@ -132,6 +89,9 @@ interface AzureTableStorageAdapter<TMeta extends BaseSubmissionMetadata = BaseSu
|
|
|
132
89
|
readonly items: readonly FormSubmission<TMeta>[];
|
|
133
90
|
readonly nextCursor?: string;
|
|
134
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>;
|
|
135
95
|
}
|
|
136
96
|
type TypedAzureTableStorageAdapter<TMeta extends BaseSubmissionMetadata | undefined = undefined> = Omit<PagedSubmissionStorageAdapter, "saveSubmission" | "listSubmissionPage" | "listTextAnswerPage"> & {
|
|
137
97
|
readonly saveSubmission: (submission: FormSubmission<TMeta>, options?: SaveSubmissionOptions) => Promise<undefined | SubmissionSaveResult<TMeta>>;
|
|
@@ -154,7 +114,11 @@ type TypedAzureTableStorageAdapter<TMeta extends BaseSubmissionMetadata | undefi
|
|
|
154
114
|
readonly items: readonly FormSubmission<TMeta>[];
|
|
155
115
|
readonly nextCursor?: string;
|
|
156
116
|
}>;
|
|
117
|
+
readonly fetchSubmissionPage?: (formId: string, options?: TypedSubmissionPageQueryOptions<TMeta>) => Promise<TypedSubmissionPage<TMeta>>;
|
|
157
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>;
|
|
158
122
|
};
|
|
159
123
|
interface AzureTextAnswerCursorPayload {
|
|
160
124
|
readonly formatVersion: 1;
|
|
@@ -170,7 +134,7 @@ declare function createAzureTableSubmissionCodec<TMeta extends BaseSubmissionMet
|
|
|
170
134
|
declare const defaultAzureTableSubmissionCodec: AzureTableSubmissionCodec<FormSubmission>;
|
|
171
135
|
declare function metadataFiltersToOData(options: SubmissionPageQueryOptions, mapping?: AzureTableFieldMapping): string;
|
|
172
136
|
declare function submissionFilterToOData(filter: SubmissionFilter, mapping?: AzureTableFieldMapping): string | undefined;
|
|
173
|
-
declare function createAzureTableStorage(options?: AzureTableStorageOptions):
|
|
137
|
+
declare function createAzureTableStorage(options?: AzureTableStorageOptions): AzureTableStorageAdapter;
|
|
174
138
|
declare function createAzureTableStorage<TMeta extends BaseSubmissionMetadata | undefined = undefined>(options?: AzureTableStorageOptions): TypedAzureTableStorageAdapter<TMeta>;
|
|
175
139
|
|
|
176
|
-
export { type AzureTableClientLike, type
|
|
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
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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,
|
|
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
|
|
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
|
-
...
|
|
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) ??
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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("");
|
|
@@ -724,6 +685,12 @@ function createAzureTableStorage(options = {}) {
|
|
|
724
685
|
};
|
|
725
686
|
return {
|
|
726
687
|
...adapter,
|
|
688
|
+
async fetchSubmissionPage(formId, options2) {
|
|
689
|
+
return await adapter.listSubmissionPage(
|
|
690
|
+
formId,
|
|
691
|
+
options2
|
|
692
|
+
);
|
|
693
|
+
},
|
|
727
694
|
async fetchPage(formId, options2) {
|
|
728
695
|
const page = await adapter.listSubmissionPage(formId, {
|
|
729
696
|
...options2?.pageSize === void 0 ? {} : { pageSize: options2.pageSize },
|
|
@@ -743,8 +710,6 @@ function createAzureTableStorage(options = {}) {
|
|
|
743
710
|
export {
|
|
744
711
|
createAzureTableStorage,
|
|
745
712
|
createAzureTableSubmissionCodec,
|
|
746
|
-
createLegacyArrayAzureTableCodec,
|
|
747
|
-
createLegacyAzureTableCodec,
|
|
748
713
|
defaultAzureTableSubmissionCodec,
|
|
749
714
|
metadataFiltersToOData,
|
|
750
715
|
submissionFilterToOData
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@form-engine-ts/storage-azure-table",
|
|
3
|
-
"version": "
|
|
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": "
|
|
42
|
+
"@form-engine-ts/core": "7.0.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@azure/data-tables": "^13.3.2"
|