@form-engine-ts/storage-azure-table 4.8.0 → 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 +104 -23
- package/dist/index.d.cts +20 -4
- package/dist/index.d.ts +20 -4
- package/dist/index.js +104 -23
- package/package.json +2 -2
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
|
-
|
|
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
|
|
209
|
-
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
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 ? [] : [
|
|
329
|
+
...partitionKey === void 0 ? [] : [`${keys.partitionKey} eq '${escapeOData(partitionKey)}'`],
|
|
265
330
|
...codec === defaultAzureTableSubmissionCodec ? ["kind eq 'submission'"] : [],
|
|
266
|
-
...options.version === void 0 ? [] : [
|
|
267
|
-
...options.since === void 0 ? [] : [
|
|
268
|
-
...options.until === void 0 ? [] : [
|
|
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
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
|
182
|
-
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
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 ? [] : [
|
|
302
|
+
...partitionKey === void 0 ? [] : [`${keys.partitionKey} eq '${escapeOData(partitionKey)}'`],
|
|
238
303
|
...codec === defaultAzureTableSubmissionCodec ? ["kind eq 'submission'"] : [],
|
|
239
|
-
...options.version === void 0 ? [] : [
|
|
240
|
-
...options.since === void 0 ? [] : [
|
|
241
|
-
...options.until === void 0 ? [] : [
|
|
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
|
|
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
|
-
...
|
|
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": "
|
|
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": "
|
|
42
|
+
"@form-engine-ts/core": "5.1.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@azure/data-tables": "^13.3.2"
|