@form-engine-ts/storage-azure-table 5.0.1 → 5.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/dist/index.cjs CHANGED
@@ -142,6 +142,60 @@ function parseSubmission(value, location) {
142
142
  }
143
143
  return cloneJson(parsed);
144
144
  }
145
+ function propertyName(mapping, logicalName, fallback) {
146
+ return mapping?.customPropertyMappings?.[logicalName] ?? (logicalName === "formId" ? mapping?.formId : void 0) ?? (logicalName === "formVersion" ? mapping?.formVersion : void 0) ?? (logicalName === "submittedAt" ? mapping?.submittedAt : void 0) ?? (logicalName === "values" ? mapping?.values : void 0) ?? (logicalName === "metadata" ? mapping?.metadata : void 0) ?? fallback;
147
+ }
148
+ function physicalKeyNames(mapping) {
149
+ return {
150
+ partitionKey: mapping?.partitionKeyProperty ?? "PartitionKey",
151
+ rowKey: mapping?.rowKeyProperty ?? "RowKey"
152
+ };
153
+ }
154
+ function entityKey(entity, property, compatibilityProperty) {
155
+ return entity[property] ?? entity[compatibilityProperty];
156
+ }
157
+ function mappedSubmissionEntity(entity, submission, mapping, valueCodec) {
158
+ if (mapping === void 0 && valueCodec === void 0) return entity;
159
+ const keys = physicalKeyNames(mapping);
160
+ const values = valueCodec?.encodeValues?.({ ...submission.values }) ?? JSON.stringify(submission.values);
161
+ const metadata = submission.metadata === void 0 ? void 0 : JSON.stringify(submission.metadata);
162
+ return {
163
+ ...entity,
164
+ [keys.partitionKey]: submission.formId,
165
+ [keys.rowKey]: entity.rowKey ?? defaultSubmissionRowKey(submission),
166
+ [propertyName(mapping, "formId", "formId")]: submission.formId,
167
+ [propertyName(mapping, "formVersion", "formVersion")]: submission.formVersion,
168
+ [propertyName(mapping, "submittedAt", "submittedAt")]: submission.submittedAt,
169
+ [propertyName(mapping, "values", "values")]: values,
170
+ ...metadata === void 0 ? {} : { [propertyName(mapping, "metadata", "metadata")]: metadata }
171
+ };
172
+ }
173
+ function mappedSubmissionFromEntity(entity, mapping, valueCodec) {
174
+ const id = typeof entity.responseId === "string" ? entity.responseId : entity.id;
175
+ const formId = entity[propertyName(mapping, "formId", "formId")];
176
+ const formVersion = entity[propertyName(mapping, "formVersion", "formVersion")];
177
+ const submittedAt = entity[propertyName(mapping, "submittedAt", "submittedAt")];
178
+ const rawValues = entity[propertyName(mapping, "values", "values")];
179
+ if (typeof id !== "string" || typeof formId !== "string" || typeof formVersion !== "number" || typeof submittedAt !== "string" || typeof rawValues !== "string")
180
+ return void 0;
181
+ const values = valueCodec?.decodeValues?.(rawValues) ?? parseJson(rawValues, "submission values");
182
+ if (!isRecord(values) || !Object.values(values).every(isFormValue))
183
+ throw new Error("Azure Table values are invalid.");
184
+ const rawMetadata = entity[propertyName(mapping, "metadata", "metadata")];
185
+ const metadata = rawMetadata === void 0 ? void 0 : parseJson(rawMetadata, "submission metadata");
186
+ return parseSubmission(
187
+ {
188
+ id,
189
+ formId,
190
+ formVersion,
191
+ locale: typeof entity.locale === "string" ? entity.locale : "",
192
+ values,
193
+ ...metadata === void 0 || !isRecord(metadata) ? {} : { metadata },
194
+ submittedAt
195
+ },
196
+ "mapped submission entity"
197
+ );
198
+ }
145
199
  function schemaRowKey(version) {
146
200
  return `schema_${version}`;
147
201
  }
@@ -201,12 +255,16 @@ function parseSchemaEntity(value) {
201
255
  }
202
256
  return cloneJson(schema);
203
257
  }
204
- function parseSubmissionEntity(value, codec) {
205
- if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || !codec.matchesEntity(value)) {
258
+ function parseSubmissionEntity(value, codec, mapping, valueCodec) {
259
+ const keys = physicalKeyNames(mapping);
260
+ const partitionKey = entityKey(value, keys.partitionKey, "partitionKey");
261
+ const rowKey = entityKey(value, keys.rowKey, "rowKey");
262
+ if (typeof partitionKey !== "string" || typeof rowKey !== "string" || !codec.matchesEntity(value)) {
206
263
  throw new Error("Azure Table submission entity is invalid.");
207
264
  }
208
- const submission = parseSubmission(codec.deserialize(value), `submission ${value.partitionKey}/${value.rowKey}`);
209
- if (codec.createPartitionKey(submission) !== value.partitionKey || codec.createRowKey(submission) !== value.rowKey) {
265
+ const submissionValue = mappedSubmissionFromEntity(value, mapping, valueCodec) ?? codec.deserialize(value);
266
+ const submission = parseSubmission(submissionValue, `submission ${partitionKey}/${rowKey}`);
267
+ if (codec.createPartitionKey(submission) !== partitionKey || codec.createRowKey(submission) !== rowKey) {
210
268
  throw new Error("Azure Table submission entity has inconsistent keys.");
211
269
  }
212
270
  return submission;
@@ -221,30 +279,36 @@ function valueToOData(value) {
221
279
  if (typeof value === "boolean") return String(value);
222
280
  throw new TypeError("Azure Table OData filters support only scalar JSON values.");
223
281
  }
224
- function metadataFiltersToOData(options) {
282
+ function metadataFiltersToOData(options, mapping) {
225
283
  return Object.entries(options.metadataFilters ?? {}).map(([key, value]) => {
226
284
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new TypeError(`Invalid Azure Table property name: ${key}`);
227
- return `${key} eq ${valueToOData(value)}`;
285
+ const property = mapping?.customPropertyMappings?.[`metadata.${key}`] ?? mapping?.customPropertyMappings?.[key] ?? key;
286
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(property))
287
+ throw new TypeError(`Invalid Azure Table property name: ${property}`);
288
+ return `${property} eq ${valueToOData(value)}`;
228
289
  }).join(" and ");
229
290
  }
230
- function odataProperty(path) {
291
+ function odataProperty(path, mapping) {
231
292
  if (path === "id" || path === "responseId") return "responseId";
232
- if (["formVersion", "locale", "submittedAt"].includes(path)) return path;
293
+ if (["formVersion", "locale", "submittedAt"].includes(path)) {
294
+ return propertyName(mapping, path, path);
295
+ }
233
296
  if (path.startsWith("metadata.")) {
234
297
  const property = path.slice("metadata.".length);
235
- return /^[A-Za-z_][A-Za-z0-9_]*$/.test(property) ? property : void 0;
298
+ const mapped = mapping?.customPropertyMappings?.[path] ?? mapping?.customPropertyMappings?.[property] ?? property;
299
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(mapped) ? mapped : void 0;
236
300
  }
237
301
  return void 0;
238
302
  }
239
- function submissionFilterToOData(filter) {
303
+ function submissionFilterToOData(filter, mapping) {
240
304
  if (filter.op === "and" || filter.op === "or") {
241
- const converted = filter.filters.map(submissionFilterToOData);
305
+ const converted = filter.filters.map((child) => submissionFilterToOData(child, mapping));
242
306
  if (filter.op === "or" && converted.some((value) => value === void 0)) return void 0;
243
307
  const available = converted.filter((value) => value !== void 0 && value.length > 0);
244
308
  if (available.length === 0) return void 0;
245
309
  return available.map((value) => `(${value})`).join(filter.op === "and" ? " and " : " or ");
246
310
  }
247
- const property = odataProperty(filter.path);
311
+ const property = odataProperty(filter.path, mapping);
248
312
  if (property === void 0) return void 0;
249
313
  if (filter.op === "eq") return `${property} eq ${valueToOData(filter.value)}`;
250
314
  if (filter.op === "in") {
@@ -257,15 +321,16 @@ function submissionFilterToOData(filter) {
257
321
  ...filter.to === void 0 ? [] : [`${property} le ${valueToOData(filter.to)}`]
258
322
  ].join(" and ");
259
323
  }
260
- function defaultSubmissionFilter(codec, formId, options, legacyExtension) {
324
+ function defaultSubmissionFilter(codec, formId, options, legacyExtension, mapping) {
261
325
  const partitionKey = codec.createPartitionKeyFromQuery(formId, options);
262
- const ast = options.filter === void 0 || typeof options.filter === "function" ? void 0 : submissionFilterToOData(options.filter);
326
+ const ast = options.filter === void 0 || typeof options.filter === "function" ? void 0 : submissionFilterToOData(options.filter, mapping);
327
+ const keys = physicalKeyNames(mapping);
263
328
  return [
264
- ...partitionKey === void 0 ? [] : [`PartitionKey eq '${escapeOData(partitionKey)}'`],
329
+ ...partitionKey === void 0 ? [] : [`${keys.partitionKey} eq '${escapeOData(partitionKey)}'`],
265
330
  ...codec === defaultAzureTableSubmissionCodec ? ["kind eq 'submission'"] : [],
266
- ...options.version === void 0 ? [] : [`formVersion eq ${options.version}`],
267
- ...options.since === void 0 ? [] : [`submittedAt ge '${escapeOData(options.since)}'`],
268
- ...options.until === void 0 ? [] : [`submittedAt le '${escapeOData(options.until)}'`],
331
+ ...options.version === void 0 ? [] : [`${propertyName(mapping, "formVersion", "formVersion")} eq ${options.version}`],
332
+ ...options.since === void 0 ? [] : [`${propertyName(mapping, "submittedAt", "submittedAt")} ge '${escapeOData(options.since)}'`],
333
+ ...options.until === void 0 ? [] : [`${propertyName(mapping, "submittedAt", "submittedAt")} le '${escapeOData(options.until)}'`],
269
334
  ...options.locale === void 0 ? [] : [`locale eq '${escapeOData(options.locale)}'`],
270
335
  ...ast === void 0 || ast.length === 0 ? [] : [`(${ast})`],
271
336
  ...legacyExtension.trim().length === 0 ? [] : [`(${legacyExtension})`]
@@ -284,7 +349,9 @@ function requireClient(client, name) {
284
349
  function createAzureTableStorage(options = {}) {
285
350
  const staticSchemas = options.schemasTableClient ?? options.client;
286
351
  const staticSubmissions = options.submissionsTableClient ?? options.client;
287
- const codec = options.codec ?? (options.submissionCodec === void 0 ? defaultAzureTableSubmissionCodec : legacyCodec(options.submissionCodec));
352
+ const configuredCodec = options.codec;
353
+ const valueCodec = configuredCodec !== void 0 && "encodeValues" in configuredCodec ? configuredCodec : void 0;
354
+ const codec = (configuredCodec !== void 0 && "createEntity" in configuredCodec ? configuredCodec : void 0) ?? (options.submissionCodec === void 0 ? defaultAzureTableSubmissionCodec : legacyCodec(options.submissionCodec));
288
355
  const maxScanPages = options.maxScanPages ?? 5;
289
356
  if (!Number.isSafeInteger(maxScanPages) || maxScanPages < 1) {
290
357
  throw new TypeError("maxScanPages must be a positive safe integer.");
@@ -301,10 +368,14 @@ function createAzureTableStorage(options = {}) {
301
368
  codec,
302
369
  formId,
303
370
  query,
304
- options.toODataFilter?.(query) ?? metadataFiltersToOData(query)
371
+ options.toODataFilter?.(query) ?? metadataFiltersToOData(query, options.fieldMapping),
372
+ options.fieldMapping
305
373
  );
306
374
  };
307
- const deserializeIfMatching = (entity) => codec.matchesEntity(entity) ? parseSubmissionEntity(entity, codec) : void 0;
375
+ const deserializeIfMatching = (entity) => codec.matchesEntity(entity) ? parseSubmissionEntity(entity, codec, options.fieldMapping, valueCodec) : void 0;
376
+ const ensureWritable = () => {
377
+ if (options.readOnly === true) throw new Error("Azure Table storage is read-only.");
378
+ };
308
379
  const listSubmissionCandidates = async (formId, query) => {
309
380
  const client = await submissionClient(formId, query);
310
381
  const found = [];
@@ -319,6 +390,7 @@ function createAzureTableStorage(options = {}) {
319
390
  };
320
391
  return {
321
392
  async saveSchema(schema) {
393
+ ensureWritable();
322
394
  (0, import_core.assertValidFormSchema)(schema);
323
395
  const entity = {
324
396
  partitionKey: schema.id,
@@ -345,14 +417,20 @@ function createAzureTableStorage(options = {}) {
345
417
  return found.sort((left, right) => left.id.localeCompare(right.id) || left.version - right.version);
346
418
  },
347
419
  async deleteSchema(formId, formVersion) {
420
+ ensureWritable();
348
421
  await (await schemaClient(formId)).deleteEntity(formId, schemaRowKey(formVersion));
349
422
  },
350
423
  async saveSubmission(submission) {
424
+ ensureWritable();
351
425
  const stored = parseSubmission(submission, `input ${String(submission?.id)}`);
426
+ const mappedEntity = mappedSubmissionEntity(codec.createEntity(stored), stored, options.fieldMapping, valueCodec);
427
+ const keys = physicalKeyNames(options.fieldMapping);
352
428
  await (await submissionClient(stored.formId)).createEntity({
353
- ...codec.createEntity(stored),
429
+ ...mappedEntity,
354
430
  partitionKey: codec.createPartitionKey(stored),
355
- rowKey: codec.createRowKey(stored)
431
+ rowKey: codec.createRowKey(stored),
432
+ [keys.partitionKey]: codec.createPartitionKey(stored),
433
+ [keys.rowKey]: codec.createRowKey(stored)
356
434
  });
357
435
  },
358
436
  async listSubmissions(formId, formVersion, queryOptions = {}) {
@@ -491,6 +569,7 @@ function createAzureTableStorage(options = {}) {
491
569
  };
492
570
  },
493
571
  async deleteSubmission(submissionId) {
572
+ ensureWritable();
494
573
  const client = await submissionClient("");
495
574
  for await (const raw of client.listEntities()) {
496
575
  const submission = deserializeIfMatching(raw);
@@ -503,6 +582,7 @@ function createAzureTableStorage(options = {}) {
503
582
  }
504
583
  },
505
584
  async clearResponses(formId) {
585
+ ensureWritable();
506
586
  const query = {};
507
587
  const client = await submissionClient(formId, query);
508
588
  for await (const raw of client.listEntities({ queryOptions: { filter: queryFilter(formId, query) } })) {
@@ -514,6 +594,7 @@ function createAzureTableStorage(options = {}) {
514
594
  }
515
595
  },
516
596
  async clear() {
597
+ ensureWritable();
517
598
  const schemas = await schemaClient("");
518
599
  for await (const raw of schemas.listEntities()) {
519
600
  if (typeof raw.partitionKey === "string" && typeof raw.rowKey === "string") {
package/dist/index.d.cts CHANGED
@@ -38,6 +38,20 @@ interface AzureTableSubmissionCodec<T = FormSubmission> {
38
38
  readonly createPartitionKeyFromQuery: (formId: string, query: SubmissionPageQueryOptions) => string | undefined;
39
39
  readonly createRowKey: (value: T) => string;
40
40
  }
41
+ interface AzureTableFieldMapping {
42
+ readonly partitionKeyProperty?: string;
43
+ readonly rowKeyProperty?: string;
44
+ readonly formId?: string;
45
+ readonly formVersion?: string;
46
+ readonly submittedAt?: string;
47
+ readonly values?: string;
48
+ readonly metadata?: string;
49
+ readonly customPropertyMappings?: Readonly<Record<string, string>>;
50
+ }
51
+ interface AzureTableValueCodec {
52
+ readonly encodeValues?: (values: Record<string, unknown>) => string;
53
+ readonly decodeValues?: (raw: string) => Record<string, unknown>;
54
+ }
41
55
  interface AzureTableStorageOptions<T = FormSubmission> {
42
56
  /** @deprecated Use schemasTableClient, submissionsTableClient, or clientResolver. */
43
57
  readonly client?: AzureTableClientLike;
@@ -47,13 +61,15 @@ interface AzureTableStorageOptions<T = FormSubmission> {
47
61
  readonly formId: string;
48
62
  readonly query?: SubmissionPageQueryOptions;
49
63
  }) => AzureTableClientLike | Promise<AzureTableClientLike>;
50
- readonly codec?: AzureTableSubmissionCodec<T>;
64
+ readonly codec?: AzureTableSubmissionCodec<T> | AzureTableValueCodec;
51
65
  /** @deprecated Use codec. */
52
66
  readonly submissionCodec?: AzureTableEntityCodec<FormSubmission>;
53
67
  readonly buildSubmissionFilter?: (formId: string, query: SubmissionPageQueryOptions) => string;
54
68
  /** @deprecated Use buildSubmissionFilter. */
55
69
  readonly toODataFilter?: (options: SubmissionPageQueryOptions) => string;
56
70
  readonly maxScanPages?: number;
71
+ readonly fieldMapping?: AzureTableFieldMapping;
72
+ readonly readOnly?: boolean;
57
73
  }
58
74
  interface AzureTextAnswerCursorPayload {
59
75
  readonly formatVersion: 1;
@@ -66,8 +82,8 @@ interface AzureTextAnswerCursorPayload {
66
82
  readonly fieldIndex: number;
67
83
  }
68
84
  declare const defaultAzureTableSubmissionCodec: AzureTableSubmissionCodec<FormSubmission>;
69
- declare function metadataFiltersToOData(options: SubmissionPageQueryOptions): string;
70
- declare function submissionFilterToOData(filter: SubmissionFilter): string | undefined;
85
+ declare function metadataFiltersToOData(options: SubmissionPageQueryOptions, mapping?: AzureTableFieldMapping): string;
86
+ declare function submissionFilterToOData(filter: SubmissionFilter, mapping?: AzureTableFieldMapping): string | undefined;
71
87
  declare function createAzureTableStorage(options?: AzureTableStorageOptions): PagedSubmissionStorageAdapter;
72
88
 
73
- export { type AzureTableClientLike, type AzureTableEntityCodec, type AzureTableEntityIterator, type AzureTableEntityPage, type AzureTableListOptions, type AzureTablePageSettings, type AzureTableStorageOptions, type AzureTableSubmissionCodec, type AzureTextAnswerCursorPayload, createAzureTableStorage, defaultAzureTableSubmissionCodec, metadataFiltersToOData, submissionFilterToOData };
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 };
package/dist/index.d.ts CHANGED
@@ -38,6 +38,20 @@ interface AzureTableSubmissionCodec<T = FormSubmission> {
38
38
  readonly createPartitionKeyFromQuery: (formId: string, query: SubmissionPageQueryOptions) => string | undefined;
39
39
  readonly createRowKey: (value: T) => string;
40
40
  }
41
+ interface AzureTableFieldMapping {
42
+ readonly partitionKeyProperty?: string;
43
+ readonly rowKeyProperty?: string;
44
+ readonly formId?: string;
45
+ readonly formVersion?: string;
46
+ readonly submittedAt?: string;
47
+ readonly values?: string;
48
+ readonly metadata?: string;
49
+ readonly customPropertyMappings?: Readonly<Record<string, string>>;
50
+ }
51
+ interface AzureTableValueCodec {
52
+ readonly encodeValues?: (values: Record<string, unknown>) => string;
53
+ readonly decodeValues?: (raw: string) => Record<string, unknown>;
54
+ }
41
55
  interface AzureTableStorageOptions<T = FormSubmission> {
42
56
  /** @deprecated Use schemasTableClient, submissionsTableClient, or clientResolver. */
43
57
  readonly client?: AzureTableClientLike;
@@ -47,13 +61,15 @@ interface AzureTableStorageOptions<T = FormSubmission> {
47
61
  readonly formId: string;
48
62
  readonly query?: SubmissionPageQueryOptions;
49
63
  }) => AzureTableClientLike | Promise<AzureTableClientLike>;
50
- readonly codec?: AzureTableSubmissionCodec<T>;
64
+ readonly codec?: AzureTableSubmissionCodec<T> | AzureTableValueCodec;
51
65
  /** @deprecated Use codec. */
52
66
  readonly submissionCodec?: AzureTableEntityCodec<FormSubmission>;
53
67
  readonly buildSubmissionFilter?: (formId: string, query: SubmissionPageQueryOptions) => string;
54
68
  /** @deprecated Use buildSubmissionFilter. */
55
69
  readonly toODataFilter?: (options: SubmissionPageQueryOptions) => string;
56
70
  readonly maxScanPages?: number;
71
+ readonly fieldMapping?: AzureTableFieldMapping;
72
+ readonly readOnly?: boolean;
57
73
  }
58
74
  interface AzureTextAnswerCursorPayload {
59
75
  readonly formatVersion: 1;
@@ -66,8 +82,8 @@ interface AzureTextAnswerCursorPayload {
66
82
  readonly fieldIndex: number;
67
83
  }
68
84
  declare const defaultAzureTableSubmissionCodec: AzureTableSubmissionCodec<FormSubmission>;
69
- declare function metadataFiltersToOData(options: SubmissionPageQueryOptions): string;
70
- declare function submissionFilterToOData(filter: SubmissionFilter): string | undefined;
85
+ declare function metadataFiltersToOData(options: SubmissionPageQueryOptions, mapping?: AzureTableFieldMapping): string;
86
+ declare function submissionFilterToOData(filter: SubmissionFilter, mapping?: AzureTableFieldMapping): string | undefined;
71
87
  declare function createAzureTableStorage(options?: AzureTableStorageOptions): PagedSubmissionStorageAdapter;
72
88
 
73
- export { type AzureTableClientLike, type AzureTableEntityCodec, type AzureTableEntityIterator, type AzureTableEntityPage, type AzureTableListOptions, type AzureTablePageSettings, type AzureTableStorageOptions, type AzureTableSubmissionCodec, type AzureTextAnswerCursorPayload, createAzureTableStorage, defaultAzureTableSubmissionCodec, metadataFiltersToOData, submissionFilterToOData };
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 };
package/dist/index.js CHANGED
@@ -115,6 +115,60 @@ function parseSubmission(value, location) {
115
115
  }
116
116
  return cloneJson(parsed);
117
117
  }
118
+ function propertyName(mapping, logicalName, fallback) {
119
+ return mapping?.customPropertyMappings?.[logicalName] ?? (logicalName === "formId" ? mapping?.formId : void 0) ?? (logicalName === "formVersion" ? mapping?.formVersion : void 0) ?? (logicalName === "submittedAt" ? mapping?.submittedAt : void 0) ?? (logicalName === "values" ? mapping?.values : void 0) ?? (logicalName === "metadata" ? mapping?.metadata : void 0) ?? fallback;
120
+ }
121
+ function physicalKeyNames(mapping) {
122
+ return {
123
+ partitionKey: mapping?.partitionKeyProperty ?? "PartitionKey",
124
+ rowKey: mapping?.rowKeyProperty ?? "RowKey"
125
+ };
126
+ }
127
+ function entityKey(entity, property, compatibilityProperty) {
128
+ return entity[property] ?? entity[compatibilityProperty];
129
+ }
130
+ function mappedSubmissionEntity(entity, submission, mapping, valueCodec) {
131
+ if (mapping === void 0 && valueCodec === void 0) return entity;
132
+ const keys = physicalKeyNames(mapping);
133
+ const values = valueCodec?.encodeValues?.({ ...submission.values }) ?? JSON.stringify(submission.values);
134
+ const metadata = submission.metadata === void 0 ? void 0 : JSON.stringify(submission.metadata);
135
+ return {
136
+ ...entity,
137
+ [keys.partitionKey]: submission.formId,
138
+ [keys.rowKey]: entity.rowKey ?? defaultSubmissionRowKey(submission),
139
+ [propertyName(mapping, "formId", "formId")]: submission.formId,
140
+ [propertyName(mapping, "formVersion", "formVersion")]: submission.formVersion,
141
+ [propertyName(mapping, "submittedAt", "submittedAt")]: submission.submittedAt,
142
+ [propertyName(mapping, "values", "values")]: values,
143
+ ...metadata === void 0 ? {} : { [propertyName(mapping, "metadata", "metadata")]: metadata }
144
+ };
145
+ }
146
+ function mappedSubmissionFromEntity(entity, mapping, valueCodec) {
147
+ const id = typeof entity.responseId === "string" ? entity.responseId : entity.id;
148
+ const formId = entity[propertyName(mapping, "formId", "formId")];
149
+ const formVersion = entity[propertyName(mapping, "formVersion", "formVersion")];
150
+ const submittedAt = entity[propertyName(mapping, "submittedAt", "submittedAt")];
151
+ const rawValues = entity[propertyName(mapping, "values", "values")];
152
+ if (typeof id !== "string" || typeof formId !== "string" || typeof formVersion !== "number" || typeof submittedAt !== "string" || typeof rawValues !== "string")
153
+ return void 0;
154
+ const values = valueCodec?.decodeValues?.(rawValues) ?? parseJson(rawValues, "submission values");
155
+ if (!isRecord(values) || !Object.values(values).every(isFormValue))
156
+ throw new Error("Azure Table values are invalid.");
157
+ const rawMetadata = entity[propertyName(mapping, "metadata", "metadata")];
158
+ const metadata = rawMetadata === void 0 ? void 0 : parseJson(rawMetadata, "submission metadata");
159
+ return parseSubmission(
160
+ {
161
+ id,
162
+ formId,
163
+ formVersion,
164
+ locale: typeof entity.locale === "string" ? entity.locale : "",
165
+ values,
166
+ ...metadata === void 0 || !isRecord(metadata) ? {} : { metadata },
167
+ submittedAt
168
+ },
169
+ "mapped submission entity"
170
+ );
171
+ }
118
172
  function schemaRowKey(version) {
119
173
  return `schema_${version}`;
120
174
  }
@@ -174,12 +228,16 @@ function parseSchemaEntity(value) {
174
228
  }
175
229
  return cloneJson(schema);
176
230
  }
177
- function parseSubmissionEntity(value, codec) {
178
- if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || !codec.matchesEntity(value)) {
231
+ function parseSubmissionEntity(value, codec, mapping, valueCodec) {
232
+ const keys = physicalKeyNames(mapping);
233
+ const partitionKey = entityKey(value, keys.partitionKey, "partitionKey");
234
+ const rowKey = entityKey(value, keys.rowKey, "rowKey");
235
+ if (typeof partitionKey !== "string" || typeof rowKey !== "string" || !codec.matchesEntity(value)) {
179
236
  throw new Error("Azure Table submission entity is invalid.");
180
237
  }
181
- const submission = parseSubmission(codec.deserialize(value), `submission ${value.partitionKey}/${value.rowKey}`);
182
- if (codec.createPartitionKey(submission) !== value.partitionKey || codec.createRowKey(submission) !== value.rowKey) {
238
+ const submissionValue = mappedSubmissionFromEntity(value, mapping, valueCodec) ?? codec.deserialize(value);
239
+ const submission = parseSubmission(submissionValue, `submission ${partitionKey}/${rowKey}`);
240
+ if (codec.createPartitionKey(submission) !== partitionKey || codec.createRowKey(submission) !== rowKey) {
183
241
  throw new Error("Azure Table submission entity has inconsistent keys.");
184
242
  }
185
243
  return submission;
@@ -194,30 +252,36 @@ function valueToOData(value) {
194
252
  if (typeof value === "boolean") return String(value);
195
253
  throw new TypeError("Azure Table OData filters support only scalar JSON values.");
196
254
  }
197
- function metadataFiltersToOData(options) {
255
+ function metadataFiltersToOData(options, mapping) {
198
256
  return Object.entries(options.metadataFilters ?? {}).map(([key, value]) => {
199
257
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new TypeError(`Invalid Azure Table property name: ${key}`);
200
- return `${key} eq ${valueToOData(value)}`;
258
+ const property = mapping?.customPropertyMappings?.[`metadata.${key}`] ?? mapping?.customPropertyMappings?.[key] ?? key;
259
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(property))
260
+ throw new TypeError(`Invalid Azure Table property name: ${property}`);
261
+ return `${property} eq ${valueToOData(value)}`;
201
262
  }).join(" and ");
202
263
  }
203
- function odataProperty(path) {
264
+ function odataProperty(path, mapping) {
204
265
  if (path === "id" || path === "responseId") return "responseId";
205
- if (["formVersion", "locale", "submittedAt"].includes(path)) return path;
266
+ if (["formVersion", "locale", "submittedAt"].includes(path)) {
267
+ return propertyName(mapping, path, path);
268
+ }
206
269
  if (path.startsWith("metadata.")) {
207
270
  const property = path.slice("metadata.".length);
208
- return /^[A-Za-z_][A-Za-z0-9_]*$/.test(property) ? property : void 0;
271
+ const mapped = mapping?.customPropertyMappings?.[path] ?? mapping?.customPropertyMappings?.[property] ?? property;
272
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(mapped) ? mapped : void 0;
209
273
  }
210
274
  return void 0;
211
275
  }
212
- function submissionFilterToOData(filter) {
276
+ function submissionFilterToOData(filter, mapping) {
213
277
  if (filter.op === "and" || filter.op === "or") {
214
- const converted = filter.filters.map(submissionFilterToOData);
278
+ const converted = filter.filters.map((child) => submissionFilterToOData(child, mapping));
215
279
  if (filter.op === "or" && converted.some((value) => value === void 0)) return void 0;
216
280
  const available = converted.filter((value) => value !== void 0 && value.length > 0);
217
281
  if (available.length === 0) return void 0;
218
282
  return available.map((value) => `(${value})`).join(filter.op === "and" ? " and " : " or ");
219
283
  }
220
- const property = odataProperty(filter.path);
284
+ const property = odataProperty(filter.path, mapping);
221
285
  if (property === void 0) return void 0;
222
286
  if (filter.op === "eq") return `${property} eq ${valueToOData(filter.value)}`;
223
287
  if (filter.op === "in") {
@@ -230,15 +294,16 @@ function submissionFilterToOData(filter) {
230
294
  ...filter.to === void 0 ? [] : [`${property} le ${valueToOData(filter.to)}`]
231
295
  ].join(" and ");
232
296
  }
233
- function defaultSubmissionFilter(codec, formId, options, legacyExtension) {
297
+ function defaultSubmissionFilter(codec, formId, options, legacyExtension, mapping) {
234
298
  const partitionKey = codec.createPartitionKeyFromQuery(formId, options);
235
- const ast = options.filter === void 0 || typeof options.filter === "function" ? void 0 : submissionFilterToOData(options.filter);
299
+ const ast = options.filter === void 0 || typeof options.filter === "function" ? void 0 : submissionFilterToOData(options.filter, mapping);
300
+ const keys = physicalKeyNames(mapping);
236
301
  return [
237
- ...partitionKey === void 0 ? [] : [`PartitionKey eq '${escapeOData(partitionKey)}'`],
302
+ ...partitionKey === void 0 ? [] : [`${keys.partitionKey} eq '${escapeOData(partitionKey)}'`],
238
303
  ...codec === defaultAzureTableSubmissionCodec ? ["kind eq 'submission'"] : [],
239
- ...options.version === void 0 ? [] : [`formVersion eq ${options.version}`],
240
- ...options.since === void 0 ? [] : [`submittedAt ge '${escapeOData(options.since)}'`],
241
- ...options.until === void 0 ? [] : [`submittedAt le '${escapeOData(options.until)}'`],
304
+ ...options.version === void 0 ? [] : [`${propertyName(mapping, "formVersion", "formVersion")} eq ${options.version}`],
305
+ ...options.since === void 0 ? [] : [`${propertyName(mapping, "submittedAt", "submittedAt")} ge '${escapeOData(options.since)}'`],
306
+ ...options.until === void 0 ? [] : [`${propertyName(mapping, "submittedAt", "submittedAt")} le '${escapeOData(options.until)}'`],
242
307
  ...options.locale === void 0 ? [] : [`locale eq '${escapeOData(options.locale)}'`],
243
308
  ...ast === void 0 || ast.length === 0 ? [] : [`(${ast})`],
244
309
  ...legacyExtension.trim().length === 0 ? [] : [`(${legacyExtension})`]
@@ -257,7 +322,9 @@ function requireClient(client, name) {
257
322
  function createAzureTableStorage(options = {}) {
258
323
  const staticSchemas = options.schemasTableClient ?? options.client;
259
324
  const staticSubmissions = options.submissionsTableClient ?? options.client;
260
- const codec = options.codec ?? (options.submissionCodec === void 0 ? defaultAzureTableSubmissionCodec : legacyCodec(options.submissionCodec));
325
+ const configuredCodec = options.codec;
326
+ const valueCodec = configuredCodec !== void 0 && "encodeValues" in configuredCodec ? configuredCodec : void 0;
327
+ const codec = (configuredCodec !== void 0 && "createEntity" in configuredCodec ? configuredCodec : void 0) ?? (options.submissionCodec === void 0 ? defaultAzureTableSubmissionCodec : legacyCodec(options.submissionCodec));
261
328
  const maxScanPages = options.maxScanPages ?? 5;
262
329
  if (!Number.isSafeInteger(maxScanPages) || maxScanPages < 1) {
263
330
  throw new TypeError("maxScanPages must be a positive safe integer.");
@@ -274,10 +341,14 @@ function createAzureTableStorage(options = {}) {
274
341
  codec,
275
342
  formId,
276
343
  query,
277
- options.toODataFilter?.(query) ?? metadataFiltersToOData(query)
344
+ options.toODataFilter?.(query) ?? metadataFiltersToOData(query, options.fieldMapping),
345
+ options.fieldMapping
278
346
  );
279
347
  };
280
- const deserializeIfMatching = (entity) => codec.matchesEntity(entity) ? parseSubmissionEntity(entity, codec) : void 0;
348
+ const deserializeIfMatching = (entity) => codec.matchesEntity(entity) ? parseSubmissionEntity(entity, codec, options.fieldMapping, valueCodec) : void 0;
349
+ const ensureWritable = () => {
350
+ if (options.readOnly === true) throw new Error("Azure Table storage is read-only.");
351
+ };
281
352
  const listSubmissionCandidates = async (formId, query) => {
282
353
  const client = await submissionClient(formId, query);
283
354
  const found = [];
@@ -292,6 +363,7 @@ function createAzureTableStorage(options = {}) {
292
363
  };
293
364
  return {
294
365
  async saveSchema(schema) {
366
+ ensureWritable();
295
367
  assertValidFormSchema(schema);
296
368
  const entity = {
297
369
  partitionKey: schema.id,
@@ -318,14 +390,20 @@ function createAzureTableStorage(options = {}) {
318
390
  return found.sort((left, right) => left.id.localeCompare(right.id) || left.version - right.version);
319
391
  },
320
392
  async deleteSchema(formId, formVersion) {
393
+ ensureWritable();
321
394
  await (await schemaClient(formId)).deleteEntity(formId, schemaRowKey(formVersion));
322
395
  },
323
396
  async saveSubmission(submission) {
397
+ ensureWritable();
324
398
  const stored = parseSubmission(submission, `input ${String(submission?.id)}`);
399
+ const mappedEntity = mappedSubmissionEntity(codec.createEntity(stored), stored, options.fieldMapping, valueCodec);
400
+ const keys = physicalKeyNames(options.fieldMapping);
325
401
  await (await submissionClient(stored.formId)).createEntity({
326
- ...codec.createEntity(stored),
402
+ ...mappedEntity,
327
403
  partitionKey: codec.createPartitionKey(stored),
328
- rowKey: codec.createRowKey(stored)
404
+ rowKey: codec.createRowKey(stored),
405
+ [keys.partitionKey]: codec.createPartitionKey(stored),
406
+ [keys.rowKey]: codec.createRowKey(stored)
329
407
  });
330
408
  },
331
409
  async listSubmissions(formId, formVersion, queryOptions = {}) {
@@ -464,6 +542,7 @@ function createAzureTableStorage(options = {}) {
464
542
  };
465
543
  },
466
544
  async deleteSubmission(submissionId) {
545
+ ensureWritable();
467
546
  const client = await submissionClient("");
468
547
  for await (const raw of client.listEntities()) {
469
548
  const submission = deserializeIfMatching(raw);
@@ -476,6 +555,7 @@ function createAzureTableStorage(options = {}) {
476
555
  }
477
556
  },
478
557
  async clearResponses(formId) {
558
+ ensureWritable();
479
559
  const query = {};
480
560
  const client = await submissionClient(formId, query);
481
561
  for await (const raw of client.listEntities({ queryOptions: { filter: queryFilter(formId, query) } })) {
@@ -487,6 +567,7 @@ function createAzureTableStorage(options = {}) {
487
567
  }
488
568
  },
489
569
  async clear() {
570
+ ensureWritable();
490
571
  const schemas = await schemaClient("");
491
572
  for await (const raw of schemas.listEntities()) {
492
573
  if (typeof raw.partitionKey === "string" && typeof raw.rowKey === "string") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/storage-azure-table",
3
- "version": "5.0.1",
3
+ "version": "5.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.0.1"
42
+ "@form-engine-ts/core": "5.1.0"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "@azure/data-tables": "^13.3.2"