@form-engine-ts/storage-azure-table 5.0.1 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/index.cjs +141 -25
- package/dist/index.d.cts +38 -4
- package/dist/index.d.ts +38 -4
- package/dist/index.js +140 -25
- package/package.json +2 -2
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,36 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
createAzureTableStorage: () => createAzureTableStorage,
|
|
24
|
+
createLegacyAzureTableCodec: () => createLegacyAzureTableCodec,
|
|
24
25
|
defaultAzureTableSubmissionCodec: () => defaultAzureTableSubmissionCodec,
|
|
25
26
|
metadataFiltersToOData: () => metadataFiltersToOData,
|
|
26
27
|
submissionFilterToOData: () => submissionFilterToOData
|
|
27
28
|
});
|
|
28
29
|
module.exports = __toCommonJS(index_exports);
|
|
29
30
|
var import_core = require("@form-engine-ts/core");
|
|
31
|
+
var createLegacyAzureTableCodec = (options = {}) => {
|
|
32
|
+
const createPartitionKey = options.partitionKeyGenerator ?? ((formId) => formId);
|
|
33
|
+
const createRowKey = options.rowKeyGenerator ?? ((submittedAt, submissionId) => `${submittedAt}_${submissionId}`);
|
|
34
|
+
return {
|
|
35
|
+
decode: (entity) => {
|
|
36
|
+
const values = entity.answers === void 0 ? {} : parseLegacyJsonObject(entity.answers, "answers");
|
|
37
|
+
const submittedAt = entity.answeredAt ?? entity.Timestamp;
|
|
38
|
+
if (submittedAt === void 0 || submittedAt.trim().length === 0) {
|
|
39
|
+
throw new Error("Azure Table legacy submission is missing answeredAt or Timestamp.");
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
id: entity.RowKey,
|
|
43
|
+
formId: entity.PartitionKey,
|
|
44
|
+
formVersion: entity.surveyVersion ?? 1,
|
|
45
|
+
values,
|
|
46
|
+
metadata: {},
|
|
47
|
+
submittedAt
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
createPartitionKey: (formId, submissionId) => createPartitionKey(formId, submissionId),
|
|
51
|
+
createRowKey: (submittedAt, submissionId) => createRowKey(submittedAt, submissionId)
|
|
52
|
+
};
|
|
53
|
+
};
|
|
30
54
|
var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
31
55
|
function encodeBase64(bytes) {
|
|
32
56
|
let result = "";
|
|
@@ -135,6 +159,11 @@ function parseJson(value, location) {
|
|
|
135
159
|
throw new Error(`Azure Table ${location} payload is invalid.`, { cause });
|
|
136
160
|
}
|
|
137
161
|
}
|
|
162
|
+
function parseLegacyJsonObject(value, property) {
|
|
163
|
+
const parsed = parseJson(value, `legacy ${property}`);
|
|
164
|
+
if (!isRecord(parsed)) throw new Error(`Azure Table legacy ${property} payload must be an object.`);
|
|
165
|
+
return parsed;
|
|
166
|
+
}
|
|
138
167
|
function parseSubmission(value, location) {
|
|
139
168
|
const parsed = typeof value === "string" ? parseJson(value, location) : value;
|
|
140
169
|
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)) {
|
|
@@ -142,6 +171,60 @@ function parseSubmission(value, location) {
|
|
|
142
171
|
}
|
|
143
172
|
return cloneJson(parsed);
|
|
144
173
|
}
|
|
174
|
+
function propertyName(mapping, logicalName, fallback) {
|
|
175
|
+
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;
|
|
176
|
+
}
|
|
177
|
+
function physicalKeyNames(mapping) {
|
|
178
|
+
return {
|
|
179
|
+
partitionKey: mapping?.partitionKeyProperty ?? "PartitionKey",
|
|
180
|
+
rowKey: mapping?.rowKeyProperty ?? "RowKey"
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function entityKey(entity, property, compatibilityProperty) {
|
|
184
|
+
return entity[property] ?? entity[compatibilityProperty];
|
|
185
|
+
}
|
|
186
|
+
function mappedSubmissionEntity(entity, submission, mapping, valueCodec) {
|
|
187
|
+
if (mapping === void 0 && valueCodec === void 0) return entity;
|
|
188
|
+
const keys = physicalKeyNames(mapping);
|
|
189
|
+
const values = valueCodec?.encodeValues?.({ ...submission.values }) ?? JSON.stringify(submission.values);
|
|
190
|
+
const metadata = submission.metadata === void 0 ? void 0 : JSON.stringify(submission.metadata);
|
|
191
|
+
return {
|
|
192
|
+
...entity,
|
|
193
|
+
[keys.partitionKey]: submission.formId,
|
|
194
|
+
[keys.rowKey]: entity.rowKey ?? defaultSubmissionRowKey(submission),
|
|
195
|
+
[propertyName(mapping, "formId", "formId")]: submission.formId,
|
|
196
|
+
[propertyName(mapping, "formVersion", "formVersion")]: submission.formVersion,
|
|
197
|
+
[propertyName(mapping, "submittedAt", "submittedAt")]: submission.submittedAt,
|
|
198
|
+
[propertyName(mapping, "values", "values")]: values,
|
|
199
|
+
...metadata === void 0 ? {} : { [propertyName(mapping, "metadata", "metadata")]: metadata }
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function mappedSubmissionFromEntity(entity, mapping, valueCodec) {
|
|
203
|
+
const id = typeof entity.responseId === "string" ? entity.responseId : entity.id;
|
|
204
|
+
const formId = entity[propertyName(mapping, "formId", "formId")];
|
|
205
|
+
const formVersion = entity[propertyName(mapping, "formVersion", "formVersion")];
|
|
206
|
+
const submittedAt = entity[propertyName(mapping, "submittedAt", "submittedAt")];
|
|
207
|
+
const rawValues = entity[propertyName(mapping, "values", "values")];
|
|
208
|
+
if (typeof id !== "string" || typeof formId !== "string" || typeof formVersion !== "number" || typeof submittedAt !== "string" || typeof rawValues !== "string")
|
|
209
|
+
return void 0;
|
|
210
|
+
const values = valueCodec?.decodeValues?.(rawValues) ?? parseJson(rawValues, "submission values");
|
|
211
|
+
if (!isRecord(values) || !Object.values(values).every(isFormValue))
|
|
212
|
+
throw new Error("Azure Table values are invalid.");
|
|
213
|
+
const rawMetadata = entity[propertyName(mapping, "metadata", "metadata")];
|
|
214
|
+
const metadata = rawMetadata === void 0 ? void 0 : parseJson(rawMetadata, "submission metadata");
|
|
215
|
+
return parseSubmission(
|
|
216
|
+
{
|
|
217
|
+
id,
|
|
218
|
+
formId,
|
|
219
|
+
formVersion,
|
|
220
|
+
locale: typeof entity.locale === "string" ? entity.locale : "",
|
|
221
|
+
values,
|
|
222
|
+
...metadata === void 0 || !isRecord(metadata) ? {} : { metadata },
|
|
223
|
+
submittedAt
|
|
224
|
+
},
|
|
225
|
+
"mapped submission entity"
|
|
226
|
+
);
|
|
227
|
+
}
|
|
145
228
|
function schemaRowKey(version) {
|
|
146
229
|
return `schema_${version}`;
|
|
147
230
|
}
|
|
@@ -201,12 +284,16 @@ function parseSchemaEntity(value) {
|
|
|
201
284
|
}
|
|
202
285
|
return cloneJson(schema);
|
|
203
286
|
}
|
|
204
|
-
function parseSubmissionEntity(value, codec) {
|
|
205
|
-
|
|
287
|
+
function parseSubmissionEntity(value, codec, mapping, valueCodec) {
|
|
288
|
+
const keys = physicalKeyNames(mapping);
|
|
289
|
+
const partitionKey = entityKey(value, keys.partitionKey, "partitionKey");
|
|
290
|
+
const rowKey = entityKey(value, keys.rowKey, "rowKey");
|
|
291
|
+
if (typeof partitionKey !== "string" || typeof rowKey !== "string" || !codec.matchesEntity(value)) {
|
|
206
292
|
throw new Error("Azure Table submission entity is invalid.");
|
|
207
293
|
}
|
|
208
|
-
const
|
|
209
|
-
|
|
294
|
+
const submissionValue = mappedSubmissionFromEntity(value, mapping, valueCodec) ?? codec.deserialize(value);
|
|
295
|
+
const submission = parseSubmission(submissionValue, `submission ${partitionKey}/${rowKey}`);
|
|
296
|
+
if (codec.createPartitionKey(submission) !== partitionKey || codec.createRowKey(submission) !== rowKey) {
|
|
210
297
|
throw new Error("Azure Table submission entity has inconsistent keys.");
|
|
211
298
|
}
|
|
212
299
|
return submission;
|
|
@@ -221,30 +308,36 @@ function valueToOData(value) {
|
|
|
221
308
|
if (typeof value === "boolean") return String(value);
|
|
222
309
|
throw new TypeError("Azure Table OData filters support only scalar JSON values.");
|
|
223
310
|
}
|
|
224
|
-
function metadataFiltersToOData(options) {
|
|
311
|
+
function metadataFiltersToOData(options, mapping) {
|
|
225
312
|
return Object.entries(options.metadataFilters ?? {}).map(([key, value]) => {
|
|
226
313
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new TypeError(`Invalid Azure Table property name: ${key}`);
|
|
227
|
-
|
|
314
|
+
const property = mapping?.customPropertyMappings?.[`metadata.${key}`] ?? mapping?.customPropertyMappings?.[key] ?? key;
|
|
315
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(property))
|
|
316
|
+
throw new TypeError(`Invalid Azure Table property name: ${property}`);
|
|
317
|
+
return `${property} eq ${valueToOData(value)}`;
|
|
228
318
|
}).join(" and ");
|
|
229
319
|
}
|
|
230
|
-
function odataProperty(path) {
|
|
320
|
+
function odataProperty(path, mapping) {
|
|
231
321
|
if (path === "id" || path === "responseId") return "responseId";
|
|
232
|
-
if (["formVersion", "locale", "submittedAt"].includes(path))
|
|
322
|
+
if (["formVersion", "locale", "submittedAt"].includes(path)) {
|
|
323
|
+
return propertyName(mapping, path, path);
|
|
324
|
+
}
|
|
233
325
|
if (path.startsWith("metadata.")) {
|
|
234
326
|
const property = path.slice("metadata.".length);
|
|
235
|
-
|
|
327
|
+
const mapped = mapping?.customPropertyMappings?.[path] ?? mapping?.customPropertyMappings?.[property] ?? property;
|
|
328
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(mapped) ? mapped : void 0;
|
|
236
329
|
}
|
|
237
330
|
return void 0;
|
|
238
331
|
}
|
|
239
|
-
function submissionFilterToOData(filter) {
|
|
332
|
+
function submissionFilterToOData(filter, mapping) {
|
|
240
333
|
if (filter.op === "and" || filter.op === "or") {
|
|
241
|
-
const converted = filter.filters.map(submissionFilterToOData);
|
|
334
|
+
const converted = filter.filters.map((child) => submissionFilterToOData(child, mapping));
|
|
242
335
|
if (filter.op === "or" && converted.some((value) => value === void 0)) return void 0;
|
|
243
336
|
const available = converted.filter((value) => value !== void 0 && value.length > 0);
|
|
244
337
|
if (available.length === 0) return void 0;
|
|
245
338
|
return available.map((value) => `(${value})`).join(filter.op === "and" ? " and " : " or ");
|
|
246
339
|
}
|
|
247
|
-
const property = odataProperty(filter.path);
|
|
340
|
+
const property = odataProperty(filter.path, mapping);
|
|
248
341
|
if (property === void 0) return void 0;
|
|
249
342
|
if (filter.op === "eq") return `${property} eq ${valueToOData(filter.value)}`;
|
|
250
343
|
if (filter.op === "in") {
|
|
@@ -257,15 +350,16 @@ function submissionFilterToOData(filter) {
|
|
|
257
350
|
...filter.to === void 0 ? [] : [`${property} le ${valueToOData(filter.to)}`]
|
|
258
351
|
].join(" and ");
|
|
259
352
|
}
|
|
260
|
-
function defaultSubmissionFilter(codec, formId, options, legacyExtension) {
|
|
353
|
+
function defaultSubmissionFilter(codec, formId, options, legacyExtension, mapping) {
|
|
261
354
|
const partitionKey = codec.createPartitionKeyFromQuery(formId, options);
|
|
262
|
-
const ast = options.filter === void 0 || typeof options.filter === "function" ? void 0 : submissionFilterToOData(options.filter);
|
|
355
|
+
const ast = options.filter === void 0 || typeof options.filter === "function" ? void 0 : submissionFilterToOData(options.filter, mapping);
|
|
356
|
+
const keys = physicalKeyNames(mapping);
|
|
263
357
|
return [
|
|
264
|
-
...partitionKey === void 0 ? [] : [
|
|
358
|
+
...partitionKey === void 0 ? [] : [`${keys.partitionKey} eq '${escapeOData(partitionKey)}'`],
|
|
265
359
|
...codec === defaultAzureTableSubmissionCodec ? ["kind eq 'submission'"] : [],
|
|
266
|
-
...options.version === void 0 ? [] : [
|
|
267
|
-
...options.since === void 0 ? [] : [
|
|
268
|
-
...options.until === void 0 ? [] : [
|
|
360
|
+
...options.version === void 0 ? [] : [`${propertyName(mapping, "formVersion", "formVersion")} eq ${options.version}`],
|
|
361
|
+
...options.since === void 0 ? [] : [`${propertyName(mapping, "submittedAt", "submittedAt")} ge '${escapeOData(options.since)}'`],
|
|
362
|
+
...options.until === void 0 ? [] : [`${propertyName(mapping, "submittedAt", "submittedAt")} le '${escapeOData(options.until)}'`],
|
|
269
363
|
...options.locale === void 0 ? [] : [`locale eq '${escapeOData(options.locale)}'`],
|
|
270
364
|
...ast === void 0 || ast.length === 0 ? [] : [`(${ast})`],
|
|
271
365
|
...legacyExtension.trim().length === 0 ? [] : [`(${legacyExtension})`]
|
|
@@ -284,7 +378,9 @@ function requireClient(client, name) {
|
|
|
284
378
|
function createAzureTableStorage(options = {}) {
|
|
285
379
|
const staticSchemas = options.schemasTableClient ?? options.client;
|
|
286
380
|
const staticSubmissions = options.submissionsTableClient ?? options.client;
|
|
287
|
-
const
|
|
381
|
+
const configuredCodec = options.codec;
|
|
382
|
+
const valueCodec = configuredCodec !== void 0 && "encodeValues" in configuredCodec ? configuredCodec : void 0;
|
|
383
|
+
const codec = (configuredCodec !== void 0 && "createEntity" in configuredCodec ? configuredCodec : void 0) ?? (options.submissionCodec === void 0 ? defaultAzureTableSubmissionCodec : legacyCodec(options.submissionCodec));
|
|
288
384
|
const maxScanPages = options.maxScanPages ?? 5;
|
|
289
385
|
if (!Number.isSafeInteger(maxScanPages) || maxScanPages < 1) {
|
|
290
386
|
throw new TypeError("maxScanPages must be a positive safe integer.");
|
|
@@ -301,10 +397,14 @@ function createAzureTableStorage(options = {}) {
|
|
|
301
397
|
codec,
|
|
302
398
|
formId,
|
|
303
399
|
query,
|
|
304
|
-
options.toODataFilter?.(query) ?? metadataFiltersToOData(query)
|
|
400
|
+
options.toODataFilter?.(query) ?? metadataFiltersToOData(query, options.fieldMapping),
|
|
401
|
+
options.fieldMapping
|
|
305
402
|
);
|
|
306
403
|
};
|
|
307
|
-
const deserializeIfMatching = (entity) => codec.matchesEntity(entity) ? parseSubmissionEntity(entity, codec) : void 0;
|
|
404
|
+
const deserializeIfMatching = (entity) => codec.matchesEntity(entity) ? parseSubmissionEntity(entity, codec, options.fieldMapping, valueCodec) : void 0;
|
|
405
|
+
const ensureWritable = () => {
|
|
406
|
+
if (options.readOnly === true) throw new Error("Azure Table storage is read-only.");
|
|
407
|
+
};
|
|
308
408
|
const listSubmissionCandidates = async (formId, query) => {
|
|
309
409
|
const client = await submissionClient(formId, query);
|
|
310
410
|
const found = [];
|
|
@@ -319,6 +419,7 @@ function createAzureTableStorage(options = {}) {
|
|
|
319
419
|
};
|
|
320
420
|
return {
|
|
321
421
|
async saveSchema(schema) {
|
|
422
|
+
ensureWritable();
|
|
322
423
|
(0, import_core.assertValidFormSchema)(schema);
|
|
323
424
|
const entity = {
|
|
324
425
|
partitionKey: schema.id,
|
|
@@ -345,14 +446,20 @@ function createAzureTableStorage(options = {}) {
|
|
|
345
446
|
return found.sort((left, right) => left.id.localeCompare(right.id) || left.version - right.version);
|
|
346
447
|
},
|
|
347
448
|
async deleteSchema(formId, formVersion) {
|
|
449
|
+
ensureWritable();
|
|
348
450
|
await (await schemaClient(formId)).deleteEntity(formId, schemaRowKey(formVersion));
|
|
349
451
|
},
|
|
350
452
|
async saveSubmission(submission) {
|
|
453
|
+
ensureWritable();
|
|
351
454
|
const stored = parseSubmission(submission, `input ${String(submission?.id)}`);
|
|
455
|
+
const mappedEntity = mappedSubmissionEntity(codec.createEntity(stored), stored, options.fieldMapping, valueCodec);
|
|
456
|
+
const keys = physicalKeyNames(options.fieldMapping);
|
|
352
457
|
await (await submissionClient(stored.formId)).createEntity({
|
|
353
|
-
...
|
|
458
|
+
...mappedEntity,
|
|
354
459
|
partitionKey: codec.createPartitionKey(stored),
|
|
355
|
-
rowKey: codec.createRowKey(stored)
|
|
460
|
+
rowKey: codec.createRowKey(stored),
|
|
461
|
+
[keys.partitionKey]: codec.createPartitionKey(stored),
|
|
462
|
+
[keys.rowKey]: codec.createRowKey(stored)
|
|
356
463
|
});
|
|
357
464
|
},
|
|
358
465
|
async listSubmissions(formId, formVersion, queryOptions = {}) {
|
|
@@ -433,6 +540,11 @@ function createAzureTableStorage(options = {}) {
|
|
|
433
540
|
continue;
|
|
434
541
|
}
|
|
435
542
|
const answers = submissionTextAnswers(submission, fieldIds);
|
|
543
|
+
const metadata = submission.metadata === void 0 ? void 0 : Object.fromEntries(
|
|
544
|
+
Object.entries(submission.metadata).filter(
|
|
545
|
+
(entry) => entry[1] !== void 0
|
|
546
|
+
)
|
|
547
|
+
);
|
|
436
548
|
for (let fieldIndex = entityIndex === entityStartIndex ? fieldStartIndex : 0; fieldIndex < answers.length; fieldIndex += 1) {
|
|
437
549
|
const answer = answers[fieldIndex];
|
|
438
550
|
if (answer === void 0) continue;
|
|
@@ -442,9 +554,9 @@ function createAzureTableStorage(options = {}) {
|
|
|
442
554
|
formVersion: submission.formVersion,
|
|
443
555
|
fieldId: answer.fieldId,
|
|
444
556
|
text: answer.text,
|
|
445
|
-
locale: submission.locale,
|
|
557
|
+
...submission.locale === void 0 ? {} : { locale: submission.locale },
|
|
446
558
|
submittedAt: submission.submittedAt,
|
|
447
|
-
...
|
|
559
|
+
...metadata === void 0 ? {} : { metadata }
|
|
448
560
|
});
|
|
449
561
|
if (items.length < pageSize) continue;
|
|
450
562
|
const nextFieldIndex = fieldIndex + 1;
|
|
@@ -491,6 +603,7 @@ function createAzureTableStorage(options = {}) {
|
|
|
491
603
|
};
|
|
492
604
|
},
|
|
493
605
|
async deleteSubmission(submissionId) {
|
|
606
|
+
ensureWritable();
|
|
494
607
|
const client = await submissionClient("");
|
|
495
608
|
for await (const raw of client.listEntities()) {
|
|
496
609
|
const submission = deserializeIfMatching(raw);
|
|
@@ -503,6 +616,7 @@ function createAzureTableStorage(options = {}) {
|
|
|
503
616
|
}
|
|
504
617
|
},
|
|
505
618
|
async clearResponses(formId) {
|
|
619
|
+
ensureWritable();
|
|
506
620
|
const query = {};
|
|
507
621
|
const client = await submissionClient(formId, query);
|
|
508
622
|
for await (const raw of client.listEntities({ queryOptions: { filter: queryFilter(formId, query) } })) {
|
|
@@ -514,6 +628,7 @@ function createAzureTableStorage(options = {}) {
|
|
|
514
628
|
}
|
|
515
629
|
},
|
|
516
630
|
async clear() {
|
|
631
|
+
ensureWritable();
|
|
517
632
|
const schemas = await schemaClient("");
|
|
518
633
|
for await (const raw of schemas.listEntities()) {
|
|
519
634
|
if (typeof raw.partitionKey === "string" && typeof raw.rowKey === "string") {
|
|
@@ -533,6 +648,7 @@ function createAzureTableStorage(options = {}) {
|
|
|
533
648
|
// Annotate the CommonJS export names for ESM import in node:
|
|
534
649
|
0 && (module.exports = {
|
|
535
650
|
createAzureTableStorage,
|
|
651
|
+
createLegacyAzureTableCodec,
|
|
536
652
|
defaultAzureTableSubmissionCodec,
|
|
537
653
|
metadataFiltersToOData,
|
|
538
654
|
submissionFilterToOData
|
package/dist/index.d.cts
CHANGED
|
@@ -30,6 +30,24 @@ 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
|
+
declare const createLegacyAzureTableCodec: (options?: {
|
|
48
|
+
readonly partitionKeyGenerator?: (formId: string, submissionId: string) => string;
|
|
49
|
+
readonly rowKeyGenerator?: (submittedAt: string, submissionId: string) => string;
|
|
50
|
+
}) => AzureTableLegacyCodec;
|
|
33
51
|
interface AzureTableSubmissionCodec<T = FormSubmission> {
|
|
34
52
|
readonly createEntity: (value: T) => Record<string, unknown>;
|
|
35
53
|
readonly deserialize: (entity: Record<string, unknown>) => T;
|
|
@@ -38,6 +56,20 @@ interface AzureTableSubmissionCodec<T = FormSubmission> {
|
|
|
38
56
|
readonly createPartitionKeyFromQuery: (formId: string, query: SubmissionPageQueryOptions) => string | undefined;
|
|
39
57
|
readonly createRowKey: (value: T) => string;
|
|
40
58
|
}
|
|
59
|
+
interface AzureTableFieldMapping {
|
|
60
|
+
readonly partitionKeyProperty?: string;
|
|
61
|
+
readonly rowKeyProperty?: string;
|
|
62
|
+
readonly formId?: string;
|
|
63
|
+
readonly formVersion?: string;
|
|
64
|
+
readonly submittedAt?: string;
|
|
65
|
+
readonly values?: string;
|
|
66
|
+
readonly metadata?: string;
|
|
67
|
+
readonly customPropertyMappings?: Readonly<Record<string, string>>;
|
|
68
|
+
}
|
|
69
|
+
interface AzureTableValueCodec {
|
|
70
|
+
readonly encodeValues?: (values: Record<string, unknown>) => string;
|
|
71
|
+
readonly decodeValues?: (raw: string) => Record<string, unknown>;
|
|
72
|
+
}
|
|
41
73
|
interface AzureTableStorageOptions<T = FormSubmission> {
|
|
42
74
|
/** @deprecated Use schemasTableClient, submissionsTableClient, or clientResolver. */
|
|
43
75
|
readonly client?: AzureTableClientLike;
|
|
@@ -47,13 +79,15 @@ interface AzureTableStorageOptions<T = FormSubmission> {
|
|
|
47
79
|
readonly formId: string;
|
|
48
80
|
readonly query?: SubmissionPageQueryOptions;
|
|
49
81
|
}) => AzureTableClientLike | Promise<AzureTableClientLike>;
|
|
50
|
-
readonly codec?: AzureTableSubmissionCodec<T
|
|
82
|
+
readonly codec?: AzureTableSubmissionCodec<T> | AzureTableValueCodec;
|
|
51
83
|
/** @deprecated Use codec. */
|
|
52
84
|
readonly submissionCodec?: AzureTableEntityCodec<FormSubmission>;
|
|
53
85
|
readonly buildSubmissionFilter?: (formId: string, query: SubmissionPageQueryOptions) => string;
|
|
54
86
|
/** @deprecated Use buildSubmissionFilter. */
|
|
55
87
|
readonly toODataFilter?: (options: SubmissionPageQueryOptions) => string;
|
|
56
88
|
readonly maxScanPages?: number;
|
|
89
|
+
readonly fieldMapping?: AzureTableFieldMapping;
|
|
90
|
+
readonly readOnly?: boolean;
|
|
57
91
|
}
|
|
58
92
|
interface AzureTextAnswerCursorPayload {
|
|
59
93
|
readonly formatVersion: 1;
|
|
@@ -66,8 +100,8 @@ interface AzureTextAnswerCursorPayload {
|
|
|
66
100
|
readonly fieldIndex: number;
|
|
67
101
|
}
|
|
68
102
|
declare const defaultAzureTableSubmissionCodec: AzureTableSubmissionCodec<FormSubmission>;
|
|
69
|
-
declare function metadataFiltersToOData(options: SubmissionPageQueryOptions): string;
|
|
70
|
-
declare function submissionFilterToOData(filter: SubmissionFilter): string | undefined;
|
|
103
|
+
declare function metadataFiltersToOData(options: SubmissionPageQueryOptions, mapping?: AzureTableFieldMapping): string;
|
|
104
|
+
declare function submissionFilterToOData(filter: SubmissionFilter, mapping?: AzureTableFieldMapping): string | undefined;
|
|
71
105
|
declare function createAzureTableStorage(options?: AzureTableStorageOptions): PagedSubmissionStorageAdapter;
|
|
72
106
|
|
|
73
|
-
export { type AzureTableClientLike, type AzureTableEntityCodec, type AzureTableEntityIterator, type AzureTableEntityPage, type AzureTableListOptions, type AzureTablePageSettings, type AzureTableStorageOptions, type AzureTableSubmissionCodec, type AzureTextAnswerCursorPayload, createAzureTableStorage, defaultAzureTableSubmissionCodec, metadataFiltersToOData, submissionFilterToOData };
|
|
107
|
+
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, createAzureTableStorage, createLegacyAzureTableCodec, defaultAzureTableSubmissionCodec, metadataFiltersToOData, submissionFilterToOData };
|
package/dist/index.d.ts
CHANGED
|
@@ -30,6 +30,24 @@ 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
|
+
declare const createLegacyAzureTableCodec: (options?: {
|
|
48
|
+
readonly partitionKeyGenerator?: (formId: string, submissionId: string) => string;
|
|
49
|
+
readonly rowKeyGenerator?: (submittedAt: string, submissionId: string) => string;
|
|
50
|
+
}) => AzureTableLegacyCodec;
|
|
33
51
|
interface AzureTableSubmissionCodec<T = FormSubmission> {
|
|
34
52
|
readonly createEntity: (value: T) => Record<string, unknown>;
|
|
35
53
|
readonly deserialize: (entity: Record<string, unknown>) => T;
|
|
@@ -38,6 +56,20 @@ interface AzureTableSubmissionCodec<T = FormSubmission> {
|
|
|
38
56
|
readonly createPartitionKeyFromQuery: (formId: string, query: SubmissionPageQueryOptions) => string | undefined;
|
|
39
57
|
readonly createRowKey: (value: T) => string;
|
|
40
58
|
}
|
|
59
|
+
interface AzureTableFieldMapping {
|
|
60
|
+
readonly partitionKeyProperty?: string;
|
|
61
|
+
readonly rowKeyProperty?: string;
|
|
62
|
+
readonly formId?: string;
|
|
63
|
+
readonly formVersion?: string;
|
|
64
|
+
readonly submittedAt?: string;
|
|
65
|
+
readonly values?: string;
|
|
66
|
+
readonly metadata?: string;
|
|
67
|
+
readonly customPropertyMappings?: Readonly<Record<string, string>>;
|
|
68
|
+
}
|
|
69
|
+
interface AzureTableValueCodec {
|
|
70
|
+
readonly encodeValues?: (values: Record<string, unknown>) => string;
|
|
71
|
+
readonly decodeValues?: (raw: string) => Record<string, unknown>;
|
|
72
|
+
}
|
|
41
73
|
interface AzureTableStorageOptions<T = FormSubmission> {
|
|
42
74
|
/** @deprecated Use schemasTableClient, submissionsTableClient, or clientResolver. */
|
|
43
75
|
readonly client?: AzureTableClientLike;
|
|
@@ -47,13 +79,15 @@ interface AzureTableStorageOptions<T = FormSubmission> {
|
|
|
47
79
|
readonly formId: string;
|
|
48
80
|
readonly query?: SubmissionPageQueryOptions;
|
|
49
81
|
}) => AzureTableClientLike | Promise<AzureTableClientLike>;
|
|
50
|
-
readonly codec?: AzureTableSubmissionCodec<T
|
|
82
|
+
readonly codec?: AzureTableSubmissionCodec<T> | AzureTableValueCodec;
|
|
51
83
|
/** @deprecated Use codec. */
|
|
52
84
|
readonly submissionCodec?: AzureTableEntityCodec<FormSubmission>;
|
|
53
85
|
readonly buildSubmissionFilter?: (formId: string, query: SubmissionPageQueryOptions) => string;
|
|
54
86
|
/** @deprecated Use buildSubmissionFilter. */
|
|
55
87
|
readonly toODataFilter?: (options: SubmissionPageQueryOptions) => string;
|
|
56
88
|
readonly maxScanPages?: number;
|
|
89
|
+
readonly fieldMapping?: AzureTableFieldMapping;
|
|
90
|
+
readonly readOnly?: boolean;
|
|
57
91
|
}
|
|
58
92
|
interface AzureTextAnswerCursorPayload {
|
|
59
93
|
readonly formatVersion: 1;
|
|
@@ -66,8 +100,8 @@ interface AzureTextAnswerCursorPayload {
|
|
|
66
100
|
readonly fieldIndex: number;
|
|
67
101
|
}
|
|
68
102
|
declare const defaultAzureTableSubmissionCodec: AzureTableSubmissionCodec<FormSubmission>;
|
|
69
|
-
declare function metadataFiltersToOData(options: SubmissionPageQueryOptions): string;
|
|
70
|
-
declare function submissionFilterToOData(filter: SubmissionFilter): string | undefined;
|
|
103
|
+
declare function metadataFiltersToOData(options: SubmissionPageQueryOptions, mapping?: AzureTableFieldMapping): string;
|
|
104
|
+
declare function submissionFilterToOData(filter: SubmissionFilter, mapping?: AzureTableFieldMapping): string | undefined;
|
|
71
105
|
declare function createAzureTableStorage(options?: AzureTableStorageOptions): PagedSubmissionStorageAdapter;
|
|
72
106
|
|
|
73
|
-
export { type AzureTableClientLike, type AzureTableEntityCodec, type AzureTableEntityIterator, type AzureTableEntityPage, type AzureTableListOptions, type AzureTablePageSettings, type AzureTableStorageOptions, type AzureTableSubmissionCodec, type AzureTextAnswerCursorPayload, createAzureTableStorage, defaultAzureTableSubmissionCodec, metadataFiltersToOData, submissionFilterToOData };
|
|
107
|
+
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, createAzureTableStorage, createLegacyAzureTableCodec, defaultAzureTableSubmissionCodec, metadataFiltersToOData, submissionFilterToOData };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { assertValidFormSchema, matchesSubmissionPageFilters, normalizeSubmissionPageSize } from "@form-engine-ts/core";
|
|
3
|
+
var createLegacyAzureTableCodec = (options = {}) => {
|
|
4
|
+
const createPartitionKey = options.partitionKeyGenerator ?? ((formId) => formId);
|
|
5
|
+
const createRowKey = options.rowKeyGenerator ?? ((submittedAt, submissionId) => `${submittedAt}_${submissionId}`);
|
|
6
|
+
return {
|
|
7
|
+
decode: (entity) => {
|
|
8
|
+
const values = entity.answers === void 0 ? {} : parseLegacyJsonObject(entity.answers, "answers");
|
|
9
|
+
const submittedAt = entity.answeredAt ?? entity.Timestamp;
|
|
10
|
+
if (submittedAt === void 0 || submittedAt.trim().length === 0) {
|
|
11
|
+
throw new Error("Azure Table legacy submission is missing answeredAt or Timestamp.");
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
id: entity.RowKey,
|
|
15
|
+
formId: entity.PartitionKey,
|
|
16
|
+
formVersion: entity.surveyVersion ?? 1,
|
|
17
|
+
values,
|
|
18
|
+
metadata: {},
|
|
19
|
+
submittedAt
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
createPartitionKey: (formId, submissionId) => createPartitionKey(formId, submissionId),
|
|
23
|
+
createRowKey: (submittedAt, submissionId) => createRowKey(submittedAt, submissionId)
|
|
24
|
+
};
|
|
25
|
+
};
|
|
3
26
|
var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
4
27
|
function encodeBase64(bytes) {
|
|
5
28
|
let result = "";
|
|
@@ -108,6 +131,11 @@ function parseJson(value, location) {
|
|
|
108
131
|
throw new Error(`Azure Table ${location} payload is invalid.`, { cause });
|
|
109
132
|
}
|
|
110
133
|
}
|
|
134
|
+
function parseLegacyJsonObject(value, property) {
|
|
135
|
+
const parsed = parseJson(value, `legacy ${property}`);
|
|
136
|
+
if (!isRecord(parsed)) throw new Error(`Azure Table legacy ${property} payload must be an object.`);
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
111
139
|
function parseSubmission(value, location) {
|
|
112
140
|
const parsed = typeof value === "string" ? parseJson(value, location) : value;
|
|
113
141
|
if (!isRecord(parsed) || typeof parsed.id !== "string" || typeof parsed.formId !== "string" || !Number.isInteger(parsed.formVersion) || typeof parsed.locale !== "string" || typeof parsed.submittedAt !== "string" || !isRecord(parsed.values) || !Object.values(parsed.values).every(isFormValue)) {
|
|
@@ -115,6 +143,60 @@ function parseSubmission(value, location) {
|
|
|
115
143
|
}
|
|
116
144
|
return cloneJson(parsed);
|
|
117
145
|
}
|
|
146
|
+
function propertyName(mapping, logicalName, fallback) {
|
|
147
|
+
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;
|
|
148
|
+
}
|
|
149
|
+
function physicalKeyNames(mapping) {
|
|
150
|
+
return {
|
|
151
|
+
partitionKey: mapping?.partitionKeyProperty ?? "PartitionKey",
|
|
152
|
+
rowKey: mapping?.rowKeyProperty ?? "RowKey"
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function entityKey(entity, property, compatibilityProperty) {
|
|
156
|
+
return entity[property] ?? entity[compatibilityProperty];
|
|
157
|
+
}
|
|
158
|
+
function mappedSubmissionEntity(entity, submission, mapping, valueCodec) {
|
|
159
|
+
if (mapping === void 0 && valueCodec === void 0) return entity;
|
|
160
|
+
const keys = physicalKeyNames(mapping);
|
|
161
|
+
const values = valueCodec?.encodeValues?.({ ...submission.values }) ?? JSON.stringify(submission.values);
|
|
162
|
+
const metadata = submission.metadata === void 0 ? void 0 : JSON.stringify(submission.metadata);
|
|
163
|
+
return {
|
|
164
|
+
...entity,
|
|
165
|
+
[keys.partitionKey]: submission.formId,
|
|
166
|
+
[keys.rowKey]: entity.rowKey ?? defaultSubmissionRowKey(submission),
|
|
167
|
+
[propertyName(mapping, "formId", "formId")]: submission.formId,
|
|
168
|
+
[propertyName(mapping, "formVersion", "formVersion")]: submission.formVersion,
|
|
169
|
+
[propertyName(mapping, "submittedAt", "submittedAt")]: submission.submittedAt,
|
|
170
|
+
[propertyName(mapping, "values", "values")]: values,
|
|
171
|
+
...metadata === void 0 ? {} : { [propertyName(mapping, "metadata", "metadata")]: metadata }
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function mappedSubmissionFromEntity(entity, mapping, valueCodec) {
|
|
175
|
+
const id = typeof entity.responseId === "string" ? entity.responseId : entity.id;
|
|
176
|
+
const formId = entity[propertyName(mapping, "formId", "formId")];
|
|
177
|
+
const formVersion = entity[propertyName(mapping, "formVersion", "formVersion")];
|
|
178
|
+
const submittedAt = entity[propertyName(mapping, "submittedAt", "submittedAt")];
|
|
179
|
+
const rawValues = entity[propertyName(mapping, "values", "values")];
|
|
180
|
+
if (typeof id !== "string" || typeof formId !== "string" || typeof formVersion !== "number" || typeof submittedAt !== "string" || typeof rawValues !== "string")
|
|
181
|
+
return void 0;
|
|
182
|
+
const values = valueCodec?.decodeValues?.(rawValues) ?? parseJson(rawValues, "submission values");
|
|
183
|
+
if (!isRecord(values) || !Object.values(values).every(isFormValue))
|
|
184
|
+
throw new Error("Azure Table values are invalid.");
|
|
185
|
+
const rawMetadata = entity[propertyName(mapping, "metadata", "metadata")];
|
|
186
|
+
const metadata = rawMetadata === void 0 ? void 0 : parseJson(rawMetadata, "submission metadata");
|
|
187
|
+
return parseSubmission(
|
|
188
|
+
{
|
|
189
|
+
id,
|
|
190
|
+
formId,
|
|
191
|
+
formVersion,
|
|
192
|
+
locale: typeof entity.locale === "string" ? entity.locale : "",
|
|
193
|
+
values,
|
|
194
|
+
...metadata === void 0 || !isRecord(metadata) ? {} : { metadata },
|
|
195
|
+
submittedAt
|
|
196
|
+
},
|
|
197
|
+
"mapped submission entity"
|
|
198
|
+
);
|
|
199
|
+
}
|
|
118
200
|
function schemaRowKey(version) {
|
|
119
201
|
return `schema_${version}`;
|
|
120
202
|
}
|
|
@@ -174,12 +256,16 @@ function parseSchemaEntity(value) {
|
|
|
174
256
|
}
|
|
175
257
|
return cloneJson(schema);
|
|
176
258
|
}
|
|
177
|
-
function parseSubmissionEntity(value, codec) {
|
|
178
|
-
|
|
259
|
+
function parseSubmissionEntity(value, codec, mapping, valueCodec) {
|
|
260
|
+
const keys = physicalKeyNames(mapping);
|
|
261
|
+
const partitionKey = entityKey(value, keys.partitionKey, "partitionKey");
|
|
262
|
+
const rowKey = entityKey(value, keys.rowKey, "rowKey");
|
|
263
|
+
if (typeof partitionKey !== "string" || typeof rowKey !== "string" || !codec.matchesEntity(value)) {
|
|
179
264
|
throw new Error("Azure Table submission entity is invalid.");
|
|
180
265
|
}
|
|
181
|
-
const
|
|
182
|
-
|
|
266
|
+
const submissionValue = mappedSubmissionFromEntity(value, mapping, valueCodec) ?? codec.deserialize(value);
|
|
267
|
+
const submission = parseSubmission(submissionValue, `submission ${partitionKey}/${rowKey}`);
|
|
268
|
+
if (codec.createPartitionKey(submission) !== partitionKey || codec.createRowKey(submission) !== rowKey) {
|
|
183
269
|
throw new Error("Azure Table submission entity has inconsistent keys.");
|
|
184
270
|
}
|
|
185
271
|
return submission;
|
|
@@ -194,30 +280,36 @@ function valueToOData(value) {
|
|
|
194
280
|
if (typeof value === "boolean") return String(value);
|
|
195
281
|
throw new TypeError("Azure Table OData filters support only scalar JSON values.");
|
|
196
282
|
}
|
|
197
|
-
function metadataFiltersToOData(options) {
|
|
283
|
+
function metadataFiltersToOData(options, mapping) {
|
|
198
284
|
return Object.entries(options.metadataFilters ?? {}).map(([key, value]) => {
|
|
199
285
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new TypeError(`Invalid Azure Table property name: ${key}`);
|
|
200
|
-
|
|
286
|
+
const property = mapping?.customPropertyMappings?.[`metadata.${key}`] ?? mapping?.customPropertyMappings?.[key] ?? key;
|
|
287
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(property))
|
|
288
|
+
throw new TypeError(`Invalid Azure Table property name: ${property}`);
|
|
289
|
+
return `${property} eq ${valueToOData(value)}`;
|
|
201
290
|
}).join(" and ");
|
|
202
291
|
}
|
|
203
|
-
function odataProperty(path) {
|
|
292
|
+
function odataProperty(path, mapping) {
|
|
204
293
|
if (path === "id" || path === "responseId") return "responseId";
|
|
205
|
-
if (["formVersion", "locale", "submittedAt"].includes(path))
|
|
294
|
+
if (["formVersion", "locale", "submittedAt"].includes(path)) {
|
|
295
|
+
return propertyName(mapping, path, path);
|
|
296
|
+
}
|
|
206
297
|
if (path.startsWith("metadata.")) {
|
|
207
298
|
const property = path.slice("metadata.".length);
|
|
208
|
-
|
|
299
|
+
const mapped = mapping?.customPropertyMappings?.[path] ?? mapping?.customPropertyMappings?.[property] ?? property;
|
|
300
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(mapped) ? mapped : void 0;
|
|
209
301
|
}
|
|
210
302
|
return void 0;
|
|
211
303
|
}
|
|
212
|
-
function submissionFilterToOData(filter) {
|
|
304
|
+
function submissionFilterToOData(filter, mapping) {
|
|
213
305
|
if (filter.op === "and" || filter.op === "or") {
|
|
214
|
-
const converted = filter.filters.map(submissionFilterToOData);
|
|
306
|
+
const converted = filter.filters.map((child) => submissionFilterToOData(child, mapping));
|
|
215
307
|
if (filter.op === "or" && converted.some((value) => value === void 0)) return void 0;
|
|
216
308
|
const available = converted.filter((value) => value !== void 0 && value.length > 0);
|
|
217
309
|
if (available.length === 0) return void 0;
|
|
218
310
|
return available.map((value) => `(${value})`).join(filter.op === "and" ? " and " : " or ");
|
|
219
311
|
}
|
|
220
|
-
const property = odataProperty(filter.path);
|
|
312
|
+
const property = odataProperty(filter.path, mapping);
|
|
221
313
|
if (property === void 0) return void 0;
|
|
222
314
|
if (filter.op === "eq") return `${property} eq ${valueToOData(filter.value)}`;
|
|
223
315
|
if (filter.op === "in") {
|
|
@@ -230,15 +322,16 @@ function submissionFilterToOData(filter) {
|
|
|
230
322
|
...filter.to === void 0 ? [] : [`${property} le ${valueToOData(filter.to)}`]
|
|
231
323
|
].join(" and ");
|
|
232
324
|
}
|
|
233
|
-
function defaultSubmissionFilter(codec, formId, options, legacyExtension) {
|
|
325
|
+
function defaultSubmissionFilter(codec, formId, options, legacyExtension, mapping) {
|
|
234
326
|
const partitionKey = codec.createPartitionKeyFromQuery(formId, options);
|
|
235
|
-
const ast = options.filter === void 0 || typeof options.filter === "function" ? void 0 : submissionFilterToOData(options.filter);
|
|
327
|
+
const ast = options.filter === void 0 || typeof options.filter === "function" ? void 0 : submissionFilterToOData(options.filter, mapping);
|
|
328
|
+
const keys = physicalKeyNames(mapping);
|
|
236
329
|
return [
|
|
237
|
-
...partitionKey === void 0 ? [] : [
|
|
330
|
+
...partitionKey === void 0 ? [] : [`${keys.partitionKey} eq '${escapeOData(partitionKey)}'`],
|
|
238
331
|
...codec === defaultAzureTableSubmissionCodec ? ["kind eq 'submission'"] : [],
|
|
239
|
-
...options.version === void 0 ? [] : [
|
|
240
|
-
...options.since === void 0 ? [] : [
|
|
241
|
-
...options.until === void 0 ? [] : [
|
|
332
|
+
...options.version === void 0 ? [] : [`${propertyName(mapping, "formVersion", "formVersion")} eq ${options.version}`],
|
|
333
|
+
...options.since === void 0 ? [] : [`${propertyName(mapping, "submittedAt", "submittedAt")} ge '${escapeOData(options.since)}'`],
|
|
334
|
+
...options.until === void 0 ? [] : [`${propertyName(mapping, "submittedAt", "submittedAt")} le '${escapeOData(options.until)}'`],
|
|
242
335
|
...options.locale === void 0 ? [] : [`locale eq '${escapeOData(options.locale)}'`],
|
|
243
336
|
...ast === void 0 || ast.length === 0 ? [] : [`(${ast})`],
|
|
244
337
|
...legacyExtension.trim().length === 0 ? [] : [`(${legacyExtension})`]
|
|
@@ -257,7 +350,9 @@ function requireClient(client, name) {
|
|
|
257
350
|
function createAzureTableStorage(options = {}) {
|
|
258
351
|
const staticSchemas = options.schemasTableClient ?? options.client;
|
|
259
352
|
const staticSubmissions = options.submissionsTableClient ?? options.client;
|
|
260
|
-
const
|
|
353
|
+
const configuredCodec = options.codec;
|
|
354
|
+
const valueCodec = configuredCodec !== void 0 && "encodeValues" in configuredCodec ? configuredCodec : void 0;
|
|
355
|
+
const codec = (configuredCodec !== void 0 && "createEntity" in configuredCodec ? configuredCodec : void 0) ?? (options.submissionCodec === void 0 ? defaultAzureTableSubmissionCodec : legacyCodec(options.submissionCodec));
|
|
261
356
|
const maxScanPages = options.maxScanPages ?? 5;
|
|
262
357
|
if (!Number.isSafeInteger(maxScanPages) || maxScanPages < 1) {
|
|
263
358
|
throw new TypeError("maxScanPages must be a positive safe integer.");
|
|
@@ -274,10 +369,14 @@ function createAzureTableStorage(options = {}) {
|
|
|
274
369
|
codec,
|
|
275
370
|
formId,
|
|
276
371
|
query,
|
|
277
|
-
options.toODataFilter?.(query) ?? metadataFiltersToOData(query)
|
|
372
|
+
options.toODataFilter?.(query) ?? metadataFiltersToOData(query, options.fieldMapping),
|
|
373
|
+
options.fieldMapping
|
|
278
374
|
);
|
|
279
375
|
};
|
|
280
|
-
const deserializeIfMatching = (entity) => codec.matchesEntity(entity) ? parseSubmissionEntity(entity, codec) : void 0;
|
|
376
|
+
const deserializeIfMatching = (entity) => codec.matchesEntity(entity) ? parseSubmissionEntity(entity, codec, options.fieldMapping, valueCodec) : void 0;
|
|
377
|
+
const ensureWritable = () => {
|
|
378
|
+
if (options.readOnly === true) throw new Error("Azure Table storage is read-only.");
|
|
379
|
+
};
|
|
281
380
|
const listSubmissionCandidates = async (formId, query) => {
|
|
282
381
|
const client = await submissionClient(formId, query);
|
|
283
382
|
const found = [];
|
|
@@ -292,6 +391,7 @@ function createAzureTableStorage(options = {}) {
|
|
|
292
391
|
};
|
|
293
392
|
return {
|
|
294
393
|
async saveSchema(schema) {
|
|
394
|
+
ensureWritable();
|
|
295
395
|
assertValidFormSchema(schema);
|
|
296
396
|
const entity = {
|
|
297
397
|
partitionKey: schema.id,
|
|
@@ -318,14 +418,20 @@ function createAzureTableStorage(options = {}) {
|
|
|
318
418
|
return found.sort((left, right) => left.id.localeCompare(right.id) || left.version - right.version);
|
|
319
419
|
},
|
|
320
420
|
async deleteSchema(formId, formVersion) {
|
|
421
|
+
ensureWritable();
|
|
321
422
|
await (await schemaClient(formId)).deleteEntity(formId, schemaRowKey(formVersion));
|
|
322
423
|
},
|
|
323
424
|
async saveSubmission(submission) {
|
|
425
|
+
ensureWritable();
|
|
324
426
|
const stored = parseSubmission(submission, `input ${String(submission?.id)}`);
|
|
427
|
+
const mappedEntity = mappedSubmissionEntity(codec.createEntity(stored), stored, options.fieldMapping, valueCodec);
|
|
428
|
+
const keys = physicalKeyNames(options.fieldMapping);
|
|
325
429
|
await (await submissionClient(stored.formId)).createEntity({
|
|
326
|
-
...
|
|
430
|
+
...mappedEntity,
|
|
327
431
|
partitionKey: codec.createPartitionKey(stored),
|
|
328
|
-
rowKey: codec.createRowKey(stored)
|
|
432
|
+
rowKey: codec.createRowKey(stored),
|
|
433
|
+
[keys.partitionKey]: codec.createPartitionKey(stored),
|
|
434
|
+
[keys.rowKey]: codec.createRowKey(stored)
|
|
329
435
|
});
|
|
330
436
|
},
|
|
331
437
|
async listSubmissions(formId, formVersion, queryOptions = {}) {
|
|
@@ -406,6 +512,11 @@ function createAzureTableStorage(options = {}) {
|
|
|
406
512
|
continue;
|
|
407
513
|
}
|
|
408
514
|
const answers = submissionTextAnswers(submission, fieldIds);
|
|
515
|
+
const metadata = submission.metadata === void 0 ? void 0 : Object.fromEntries(
|
|
516
|
+
Object.entries(submission.metadata).filter(
|
|
517
|
+
(entry) => entry[1] !== void 0
|
|
518
|
+
)
|
|
519
|
+
);
|
|
409
520
|
for (let fieldIndex = entityIndex === entityStartIndex ? fieldStartIndex : 0; fieldIndex < answers.length; fieldIndex += 1) {
|
|
410
521
|
const answer = answers[fieldIndex];
|
|
411
522
|
if (answer === void 0) continue;
|
|
@@ -415,9 +526,9 @@ function createAzureTableStorage(options = {}) {
|
|
|
415
526
|
formVersion: submission.formVersion,
|
|
416
527
|
fieldId: answer.fieldId,
|
|
417
528
|
text: answer.text,
|
|
418
|
-
locale: submission.locale,
|
|
529
|
+
...submission.locale === void 0 ? {} : { locale: submission.locale },
|
|
419
530
|
submittedAt: submission.submittedAt,
|
|
420
|
-
...
|
|
531
|
+
...metadata === void 0 ? {} : { metadata }
|
|
421
532
|
});
|
|
422
533
|
if (items.length < pageSize) continue;
|
|
423
534
|
const nextFieldIndex = fieldIndex + 1;
|
|
@@ -464,6 +575,7 @@ function createAzureTableStorage(options = {}) {
|
|
|
464
575
|
};
|
|
465
576
|
},
|
|
466
577
|
async deleteSubmission(submissionId) {
|
|
578
|
+
ensureWritable();
|
|
467
579
|
const client = await submissionClient("");
|
|
468
580
|
for await (const raw of client.listEntities()) {
|
|
469
581
|
const submission = deserializeIfMatching(raw);
|
|
@@ -476,6 +588,7 @@ function createAzureTableStorage(options = {}) {
|
|
|
476
588
|
}
|
|
477
589
|
},
|
|
478
590
|
async clearResponses(formId) {
|
|
591
|
+
ensureWritable();
|
|
479
592
|
const query = {};
|
|
480
593
|
const client = await submissionClient(formId, query);
|
|
481
594
|
for await (const raw of client.listEntities({ queryOptions: { filter: queryFilter(formId, query) } })) {
|
|
@@ -487,6 +600,7 @@ function createAzureTableStorage(options = {}) {
|
|
|
487
600
|
}
|
|
488
601
|
},
|
|
489
602
|
async clear() {
|
|
603
|
+
ensureWritable();
|
|
490
604
|
const schemas = await schemaClient("");
|
|
491
605
|
for await (const raw of schemas.listEntities()) {
|
|
492
606
|
if (typeof raw.partitionKey === "string" && typeof raw.rowKey === "string") {
|
|
@@ -505,6 +619,7 @@ function createAzureTableStorage(options = {}) {
|
|
|
505
619
|
}
|
|
506
620
|
export {
|
|
507
621
|
createAzureTableStorage,
|
|
622
|
+
createLegacyAzureTableCodec,
|
|
508
623
|
defaultAzureTableSubmissionCodec,
|
|
509
624
|
metadataFiltersToOData,
|
|
510
625
|
submissionFilterToOData
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@form-engine-ts/storage-azure-table",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"typescript"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@form-engine-ts/core": "
|
|
42
|
+
"@form-engine-ts/core": "6.0.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@azure/data-tables": "^13.3.2"
|