@form-engine-ts/storage-azure-table 5.1.0 → 6.1.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
@@ -42,3 +42,7 @@ and field indexes, so a page can resume inside a multi-answer entity without gap
42
42
  consume the item limit, and scanning remains bounded by `maxScanPages`. Cursor format version 1 also records the form,
43
43
  version, sorted fields, and a SHA-256 filter fingerprint. Reusing a cursor with different query context throws
44
44
  `invalid_cursor_context`.
45
+
46
+ `createLegacyAzureTableCodec()` decodes older entities using `PartitionKey`, `RowKey`, `answers`, `answeredAt`, and
47
+ `surveyVersion` into the canonical `FormSubmission` shape. Custom partition- and row-key generators can be supplied
48
+ for migration tooling.
package/dist/index.cjs CHANGED
@@ -21,12 +21,88 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  createAzureTableStorage: () => createAzureTableStorage,
24
+ createLegacyArrayAzureTableCodec: () => createLegacyArrayAzureTableCodec,
25
+ createLegacyAzureTableCodec: () => createLegacyAzureTableCodec,
24
26
  defaultAzureTableSubmissionCodec: () => defaultAzureTableSubmissionCodec,
25
27
  metadataFiltersToOData: () => metadataFiltersToOData,
26
28
  submissionFilterToOData: () => submissionFilterToOData
27
29
  });
28
30
  module.exports = __toCommonJS(index_exports);
29
31
  var import_core = require("@form-engine-ts/core");
32
+ function createLegacyArrayAzureTableCodec(options = {}) {
33
+ const defaultLocale = options.defaultLocale ?? "ja";
34
+ if (defaultLocale.trim().length === 0) throw new TypeError("defaultLocale must not be empty.");
35
+ return {
36
+ encode: (submission) => ({
37
+ PartitionKey: submission.formId,
38
+ RowKey: submission.id,
39
+ answers: JSON.stringify(Object.entries(submission.values).map(([questionId, value]) => ({ questionId, value }))),
40
+ answeredAt: submission.submittedAt,
41
+ surveyVersion: submission.formVersion,
42
+ locale: submission.locale,
43
+ ...submission.metadata
44
+ }),
45
+ decode: (entity) => {
46
+ const raw = entity;
47
+ if (typeof raw.PartitionKey !== "string" || typeof raw.RowKey !== "string") {
48
+ throw new Error("Azure Table legacy array submission is missing PartitionKey or RowKey.");
49
+ }
50
+ const submittedAt = typeof raw.answeredAt === "string" ? raw.answeredAt : entity.Timestamp;
51
+ if (typeof submittedAt !== "string" || submittedAt.trim().length === 0) {
52
+ throw new Error("Azure Table legacy array submission is missing answeredAt or Timestamp.");
53
+ }
54
+ let values = {};
55
+ if (typeof raw.answers === "string") {
56
+ const parsed = parseJson(raw.answers, "legacy array answers");
57
+ if (Array.isArray(parsed)) {
58
+ values = Object.fromEntries(
59
+ parsed.flatMap((item) => {
60
+ if (!isRecord(item) || typeof item.questionId !== "string") return [];
61
+ return [[item.questionId, item.value]];
62
+ })
63
+ );
64
+ } else if (isRecord(parsed)) {
65
+ values = parsed;
66
+ } else {
67
+ throw new Error("Azure Table legacy array answers payload must be an array or object.");
68
+ }
69
+ }
70
+ const formVersion = Number(raw.surveyVersion);
71
+ return {
72
+ id: raw.RowKey,
73
+ formId: raw.PartitionKey,
74
+ formVersion: Number.isSafeInteger(formVersion) && formVersion > 0 ? formVersion : 1,
75
+ values,
76
+ locale: typeof raw.locale === "string" && raw.locale.trim().length > 0 ? raw.locale : defaultLocale,
77
+ metadata: options.metadataExtractor?.(entity) ?? {},
78
+ submittedAt
79
+ };
80
+ }
81
+ };
82
+ }
83
+ var createLegacyAzureTableCodec = (options = {}) => {
84
+ const createPartitionKey = options.partitionKeyGenerator ?? ((formId) => formId);
85
+ const createRowKey = options.rowKeyGenerator ?? ((submittedAt, submissionId) => `${submittedAt}_${submissionId}`);
86
+ return {
87
+ decode: (entity) => {
88
+ const values = entity.answers === void 0 ? {} : parseLegacyJsonObject(entity.answers, "answers");
89
+ const submittedAt = entity.answeredAt ?? entity.Timestamp;
90
+ if (submittedAt === void 0 || submittedAt.trim().length === 0) {
91
+ throw new Error("Azure Table legacy submission is missing answeredAt or Timestamp.");
92
+ }
93
+ return {
94
+ id: entity.RowKey,
95
+ formId: entity.PartitionKey,
96
+ formVersion: entity.surveyVersion ?? 1,
97
+ values,
98
+ metadata: {},
99
+ submittedAt
100
+ };
101
+ },
102
+ createPartitionKey: (formId, submissionId) => createPartitionKey(formId, submissionId),
103
+ createRowKey: (submittedAt, submissionId) => createRowKey(submittedAt, submissionId)
104
+ };
105
+ };
30
106
  var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
31
107
  function encodeBase64(bytes) {
32
108
  let result = "";
@@ -135,6 +211,11 @@ function parseJson(value, location) {
135
211
  throw new Error(`Azure Table ${location} payload is invalid.`, { cause });
136
212
  }
137
213
  }
214
+ function parseLegacyJsonObject(value, property) {
215
+ const parsed = parseJson(value, `legacy ${property}`);
216
+ if (!isRecord(parsed)) throw new Error(`Azure Table legacy ${property} payload must be an object.`);
217
+ return parsed;
218
+ }
138
219
  function parseSubmission(value, location) {
139
220
  const parsed = typeof value === "string" ? parseJson(value, location) : value;
140
221
  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)) {
@@ -511,6 +592,11 @@ function createAzureTableStorage(options = {}) {
511
592
  continue;
512
593
  }
513
594
  const answers = submissionTextAnswers(submission, fieldIds);
595
+ const metadata = submission.metadata === void 0 ? void 0 : Object.fromEntries(
596
+ Object.entries(submission.metadata).filter(
597
+ (entry) => entry[1] !== void 0
598
+ )
599
+ );
514
600
  for (let fieldIndex = entityIndex === entityStartIndex ? fieldStartIndex : 0; fieldIndex < answers.length; fieldIndex += 1) {
515
601
  const answer = answers[fieldIndex];
516
602
  if (answer === void 0) continue;
@@ -520,9 +606,9 @@ function createAzureTableStorage(options = {}) {
520
606
  formVersion: submission.formVersion,
521
607
  fieldId: answer.fieldId,
522
608
  text: answer.text,
523
- locale: submission.locale,
609
+ ...submission.locale === void 0 ? {} : { locale: submission.locale },
524
610
  submittedAt: submission.submittedAt,
525
- ...submission.metadata === void 0 ? {} : { metadata: submission.metadata }
611
+ ...metadata === void 0 ? {} : { metadata }
526
612
  });
527
613
  if (items.length < pageSize) continue;
528
614
  const nextFieldIndex = fieldIndex + 1;
@@ -614,6 +700,8 @@ function createAzureTableStorage(options = {}) {
614
700
  // Annotate the CommonJS export names for ESM import in node:
615
701
  0 && (module.exports = {
616
702
  createAzureTableStorage,
703
+ createLegacyArrayAzureTableCodec,
704
+ createLegacyAzureTableCodec,
617
705
  defaultAzureTableSubmissionCodec,
618
706
  metadataFiltersToOData,
619
707
  submissionFilterToOData
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { FormSubmission, SubmissionPageQueryOptions, PagedSubmissionStorageAdapter, SubmissionFilter } from '@form-engine-ts/core';
1
+ import { FormSubmission, SubmissionPageQueryOptions, BaseSubmissionMetadata, StrictFormSubmission, PagedSubmissionStorageAdapter, SubmissionFilter } from '@form-engine-ts/core';
2
2
 
3
3
  interface AzureTableListOptions {
4
4
  readonly queryOptions?: {
@@ -30,6 +30,40 @@ interface AzureTableEntityCodec<T> {
30
30
  readonly serialize: (submission: T) => Record<string, unknown>;
31
31
  readonly deserialize: (entity: Record<string, unknown>) => T;
32
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;
33
67
  interface AzureTableSubmissionCodec<T = FormSubmission> {
34
68
  readonly createEntity: (value: T) => Record<string, unknown>;
35
69
  readonly deserialize: (entity: Record<string, unknown>) => T;
@@ -86,4 +120,4 @@ declare function metadataFiltersToOData(options: SubmissionPageQueryOptions, map
86
120
  declare function submissionFilterToOData(filter: SubmissionFilter, mapping?: AzureTableFieldMapping): string | undefined;
87
121
  declare function createAzureTableStorage(options?: AzureTableStorageOptions): PagedSubmissionStorageAdapter;
88
122
 
89
- export { type AzureTableClientLike, type AzureTableEntityCodec, type AzureTableEntityIterator, type AzureTableEntityPage, type AzureTableFieldMapping, type AzureTableListOptions, type AzureTablePageSettings, type AzureTableStorageOptions, type AzureTableSubmissionCodec, type AzureTableValueCodec, type AzureTextAnswerCursorPayload, createAzureTableStorage, defaultAzureTableSubmissionCodec, metadataFiltersToOData, submissionFilterToOData };
123
+ export { type AzureTableClientLike, type AzureTableEntityCodec, type AzureTableEntityIterator, type AzureTableEntityPage, type AzureTableFieldMapping, type AzureTableLegacyCodec, type AzureTableLegacyEntity, type AzureTableListOptions, type AzureTablePageSettings, type AzureTableStorageOptions, type AzureTableSubmissionCodec, type AzureTableValueCodec, type AzureTextAnswerCursorPayload, type LegacyAnswerArrayEntity, type LegacyArrayAzureTableCodec, createAzureTableStorage, createLegacyArrayAzureTableCodec, createLegacyAzureTableCodec, defaultAzureTableSubmissionCodec, metadataFiltersToOData, submissionFilterToOData };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { FormSubmission, SubmissionPageQueryOptions, PagedSubmissionStorageAdapter, SubmissionFilter } from '@form-engine-ts/core';
1
+ import { FormSubmission, SubmissionPageQueryOptions, BaseSubmissionMetadata, StrictFormSubmission, PagedSubmissionStorageAdapter, SubmissionFilter } from '@form-engine-ts/core';
2
2
 
3
3
  interface AzureTableListOptions {
4
4
  readonly queryOptions?: {
@@ -30,6 +30,40 @@ interface AzureTableEntityCodec<T> {
30
30
  readonly serialize: (submission: T) => Record<string, unknown>;
31
31
  readonly deserialize: (entity: Record<string, unknown>) => T;
32
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;
33
67
  interface AzureTableSubmissionCodec<T = FormSubmission> {
34
68
  readonly createEntity: (value: T) => Record<string, unknown>;
35
69
  readonly deserialize: (entity: Record<string, unknown>) => T;
@@ -86,4 +120,4 @@ declare function metadataFiltersToOData(options: SubmissionPageQueryOptions, map
86
120
  declare function submissionFilterToOData(filter: SubmissionFilter, mapping?: AzureTableFieldMapping): string | undefined;
87
121
  declare function createAzureTableStorage(options?: AzureTableStorageOptions): PagedSubmissionStorageAdapter;
88
122
 
89
- export { type AzureTableClientLike, type AzureTableEntityCodec, type AzureTableEntityIterator, type AzureTableEntityPage, type AzureTableFieldMapping, type AzureTableListOptions, type AzureTablePageSettings, type AzureTableStorageOptions, type AzureTableSubmissionCodec, type AzureTableValueCodec, type AzureTextAnswerCursorPayload, createAzureTableStorage, defaultAzureTableSubmissionCodec, metadataFiltersToOData, submissionFilterToOData };
123
+ export { type AzureTableClientLike, type AzureTableEntityCodec, type AzureTableEntityIterator, type AzureTableEntityPage, type AzureTableFieldMapping, type AzureTableLegacyCodec, type AzureTableLegacyEntity, type AzureTableListOptions, type AzureTablePageSettings, type AzureTableStorageOptions, type AzureTableSubmissionCodec, type AzureTableValueCodec, type AzureTextAnswerCursorPayload, type LegacyAnswerArrayEntity, type LegacyArrayAzureTableCodec, createAzureTableStorage, createLegacyArrayAzureTableCodec, createLegacyAzureTableCodec, defaultAzureTableSubmissionCodec, metadataFiltersToOData, submissionFilterToOData };
package/dist/index.js CHANGED
@@ -1,5 +1,79 @@
1
1
  // src/index.ts
2
2
  import { assertValidFormSchema, matchesSubmissionPageFilters, normalizeSubmissionPageSize } from "@form-engine-ts/core";
3
+ function createLegacyArrayAzureTableCodec(options = {}) {
4
+ const defaultLocale = options.defaultLocale ?? "ja";
5
+ if (defaultLocale.trim().length === 0) throw new TypeError("defaultLocale must not be empty.");
6
+ return {
7
+ encode: (submission) => ({
8
+ PartitionKey: submission.formId,
9
+ RowKey: submission.id,
10
+ answers: JSON.stringify(Object.entries(submission.values).map(([questionId, value]) => ({ questionId, value }))),
11
+ answeredAt: submission.submittedAt,
12
+ surveyVersion: submission.formVersion,
13
+ locale: submission.locale,
14
+ ...submission.metadata
15
+ }),
16
+ decode: (entity) => {
17
+ const raw = entity;
18
+ if (typeof raw.PartitionKey !== "string" || typeof raw.RowKey !== "string") {
19
+ throw new Error("Azure Table legacy array submission is missing PartitionKey or RowKey.");
20
+ }
21
+ const submittedAt = typeof raw.answeredAt === "string" ? raw.answeredAt : entity.Timestamp;
22
+ if (typeof submittedAt !== "string" || submittedAt.trim().length === 0) {
23
+ throw new Error("Azure Table legacy array submission is missing answeredAt or Timestamp.");
24
+ }
25
+ let values = {};
26
+ if (typeof raw.answers === "string") {
27
+ const parsed = parseJson(raw.answers, "legacy array answers");
28
+ if (Array.isArray(parsed)) {
29
+ values = Object.fromEntries(
30
+ parsed.flatMap((item) => {
31
+ if (!isRecord(item) || typeof item.questionId !== "string") return [];
32
+ return [[item.questionId, item.value]];
33
+ })
34
+ );
35
+ } else if (isRecord(parsed)) {
36
+ values = parsed;
37
+ } else {
38
+ throw new Error("Azure Table legacy array answers payload must be an array or object.");
39
+ }
40
+ }
41
+ const formVersion = Number(raw.surveyVersion);
42
+ return {
43
+ id: raw.RowKey,
44
+ formId: raw.PartitionKey,
45
+ formVersion: Number.isSafeInteger(formVersion) && formVersion > 0 ? formVersion : 1,
46
+ values,
47
+ locale: typeof raw.locale === "string" && raw.locale.trim().length > 0 ? raw.locale : defaultLocale,
48
+ metadata: options.metadataExtractor?.(entity) ?? {},
49
+ submittedAt
50
+ };
51
+ }
52
+ };
53
+ }
54
+ var createLegacyAzureTableCodec = (options = {}) => {
55
+ const createPartitionKey = options.partitionKeyGenerator ?? ((formId) => formId);
56
+ const createRowKey = options.rowKeyGenerator ?? ((submittedAt, submissionId) => `${submittedAt}_${submissionId}`);
57
+ return {
58
+ decode: (entity) => {
59
+ const values = entity.answers === void 0 ? {} : parseLegacyJsonObject(entity.answers, "answers");
60
+ const submittedAt = entity.answeredAt ?? entity.Timestamp;
61
+ if (submittedAt === void 0 || submittedAt.trim().length === 0) {
62
+ throw new Error("Azure Table legacy submission is missing answeredAt or Timestamp.");
63
+ }
64
+ return {
65
+ id: entity.RowKey,
66
+ formId: entity.PartitionKey,
67
+ formVersion: entity.surveyVersion ?? 1,
68
+ values,
69
+ metadata: {},
70
+ submittedAt
71
+ };
72
+ },
73
+ createPartitionKey: (formId, submissionId) => createPartitionKey(formId, submissionId),
74
+ createRowKey: (submittedAt, submissionId) => createRowKey(submittedAt, submissionId)
75
+ };
76
+ };
3
77
  var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
4
78
  function encodeBase64(bytes) {
5
79
  let result = "";
@@ -108,6 +182,11 @@ function parseJson(value, location) {
108
182
  throw new Error(`Azure Table ${location} payload is invalid.`, { cause });
109
183
  }
110
184
  }
185
+ function parseLegacyJsonObject(value, property) {
186
+ const parsed = parseJson(value, `legacy ${property}`);
187
+ if (!isRecord(parsed)) throw new Error(`Azure Table legacy ${property} payload must be an object.`);
188
+ return parsed;
189
+ }
111
190
  function parseSubmission(value, location) {
112
191
  const parsed = typeof value === "string" ? parseJson(value, location) : value;
113
192
  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)) {
@@ -484,6 +563,11 @@ function createAzureTableStorage(options = {}) {
484
563
  continue;
485
564
  }
486
565
  const answers = submissionTextAnswers(submission, fieldIds);
566
+ const metadata = submission.metadata === void 0 ? void 0 : Object.fromEntries(
567
+ Object.entries(submission.metadata).filter(
568
+ (entry) => entry[1] !== void 0
569
+ )
570
+ );
487
571
  for (let fieldIndex = entityIndex === entityStartIndex ? fieldStartIndex : 0; fieldIndex < answers.length; fieldIndex += 1) {
488
572
  const answer = answers[fieldIndex];
489
573
  if (answer === void 0) continue;
@@ -493,9 +577,9 @@ function createAzureTableStorage(options = {}) {
493
577
  formVersion: submission.formVersion,
494
578
  fieldId: answer.fieldId,
495
579
  text: answer.text,
496
- locale: submission.locale,
580
+ ...submission.locale === void 0 ? {} : { locale: submission.locale },
497
581
  submittedAt: submission.submittedAt,
498
- ...submission.metadata === void 0 ? {} : { metadata: submission.metadata }
582
+ ...metadata === void 0 ? {} : { metadata }
499
583
  });
500
584
  if (items.length < pageSize) continue;
501
585
  const nextFieldIndex = fieldIndex + 1;
@@ -586,6 +670,8 @@ function createAzureTableStorage(options = {}) {
586
670
  }
587
671
  export {
588
672
  createAzureTableStorage,
673
+ createLegacyArrayAzureTableCodec,
674
+ createLegacyAzureTableCodec,
589
675
  defaultAzureTableSubmissionCodec,
590
676
  metadataFiltersToOData,
591
677
  submissionFilterToOData
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/storage-azure-table",
3
- "version": "5.1.0",
3
+ "version": "6.1.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": "5.1.0"
42
+ "@form-engine-ts/core": "6.1.0"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "@azure/data-tables": "^13.3.2"