@form-engine-ts/storage-azure-table 8.1.7 → 8.2.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 +2 -1
- package/dist/index.cjs +43 -12
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +44 -12
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -49,7 +49,8 @@ source can be supplied per call as `saveSubmission(submission, { validation })`.
|
|
|
49
49
|
Zod-compatible object with `safeParse`, or an async validator callback.
|
|
50
50
|
|
|
51
51
|
`saveSubmissionWithinLimit(submission, maxResponses, options?)` uses an ETag-protected capacity entity and an Azure
|
|
52
|
-
Table transaction to update capacity and create the response together.
|
|
52
|
+
Table transaction to update capacity and create the response together. Set `responseLimitScope: "form"` to share its
|
|
53
|
+
capacity row across all versions. The submission codec must place a form's
|
|
53
54
|
responses in one partition (the default codec does), and the injected client must expose `submitTransaction`.
|
|
54
55
|
|
|
55
56
|
`listTextAnswerPage` accepts either the legacy single field ID or `TextAnswerPageQueryOptions.fieldIds`. Page size counts
|
package/dist/index.cjs
CHANGED
|
@@ -404,16 +404,18 @@ function createAzureTableStorage(options = {}) {
|
|
|
404
404
|
);
|
|
405
405
|
};
|
|
406
406
|
const lifecycleBackend = {
|
|
407
|
-
resources: ["schema", "submission"],
|
|
407
|
+
resources: ["schema", "submission", "runtimeSettings"],
|
|
408
408
|
async list(formId) {
|
|
409
409
|
const records = [];
|
|
410
|
-
for (const kind of ["schema", "submission"]) {
|
|
411
|
-
const
|
|
412
|
-
const
|
|
410
|
+
for (const kind of ["schema", "submission", "runtimeSettings"]) {
|
|
411
|
+
const isSchema = kind === "schema" || kind === "runtimeSettings";
|
|
412
|
+
const client = isSchema ? await schemaClient(formId) : await submissionClient(formId);
|
|
413
|
+
const filter = isSchema ? `PartitionKey eq '${escapeOData(formId)}' and kind eq '${kind === "schema" ? "schema" : "runtimeSettings"}'` : queryFilter(formId, {});
|
|
413
414
|
for await (const raw of client.listEntities({ queryOptions: { filter } })) {
|
|
414
|
-
if (
|
|
415
|
-
const value = kind === "schema" ? parseSchemaEntity(raw, options.schemaValidation) : deserializeIfMatching(raw);
|
|
416
|
-
|
|
415
|
+
if (isSchema && raw.kind !== kind) continue;
|
|
416
|
+
const value = kind === "schema" ? parseSchemaEntity(raw, options.schemaValidation) : kind === "runtimeSettings" ? parseJson(raw.payload, "runtime settings") : deserializeIfMatching(raw);
|
|
417
|
+
const resourceFormId = isRecord(value) ? typeof value.formId === "string" ? value.formId : typeof value.id === "string" ? value.id : void 0 : void 0;
|
|
418
|
+
if (resourceFormId !== formId) continue;
|
|
417
419
|
if (typeof raw.partitionKey !== "string" || typeof raw.rowKey !== "string")
|
|
418
420
|
throw new TypeError("Missing entity keys.");
|
|
419
421
|
records.push({ kind, id: JSON.stringify([raw.partitionKey, raw.rowKey]), value: { raw, value, formId } });
|
|
@@ -428,7 +430,7 @@ function createAzureTableStorage(options = {}) {
|
|
|
428
430
|
const { raw, formId } = resource.value;
|
|
429
431
|
if (typeof raw.partitionKey !== "string" || typeof raw.rowKey !== "string" || typeof raw.etag !== "string")
|
|
430
432
|
throw new TypeError("Conditional deletion requires entity keys and an ETag.");
|
|
431
|
-
const client = resource.kind === "schema" ? await schemaClient(formId) : await submissionClient(formId);
|
|
433
|
+
const client = resource.kind === "schema" || resource.kind === "runtimeSettings" ? await schemaClient(formId) : await submissionClient(formId);
|
|
432
434
|
try {
|
|
433
435
|
await client.deleteEntity(raw.partitionKey, raw.rowKey, { etag: raw.etag });
|
|
434
436
|
return 1;
|
|
@@ -474,6 +476,31 @@ function createAzureTableStorage(options = {}) {
|
|
|
474
476
|
throw error;
|
|
475
477
|
}
|
|
476
478
|
},
|
|
479
|
+
async getRuntimeSettings(formId) {
|
|
480
|
+
try {
|
|
481
|
+
const entity = await (await schemaClient(formId)).getEntity(formId, "runtime-settings");
|
|
482
|
+
const settings = parseJson(entity.payload, "runtime settings");
|
|
483
|
+
(0, import_core.assertValidFormRuntimeSettings)(settings);
|
|
484
|
+
if (settings.formId !== formId) throw new Error("Azure Table runtime settings have inconsistent metadata.");
|
|
485
|
+
return cloneJson(settings);
|
|
486
|
+
} catch (error) {
|
|
487
|
+
if (isNotFound(error)) return null;
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
async saveRuntimeSettings(settings) {
|
|
492
|
+
ensureWritable();
|
|
493
|
+
(0, import_core.assertValidFormRuntimeSettings)(settings);
|
|
494
|
+
await (await schemaClient(settings.formId)).upsertEntity(
|
|
495
|
+
{
|
|
496
|
+
partitionKey: settings.formId,
|
|
497
|
+
rowKey: "runtime-settings",
|
|
498
|
+
kind: "runtimeSettings",
|
|
499
|
+
payload: JSON.stringify(settings)
|
|
500
|
+
},
|
|
501
|
+
"Replace"
|
|
502
|
+
);
|
|
503
|
+
},
|
|
477
504
|
async listSchemas() {
|
|
478
505
|
const found = [];
|
|
479
506
|
for await (const raw of (await schemaClient("")).listEntities({ queryOptions: { filter: "kind eq 'schema'" } })) {
|
|
@@ -555,7 +582,11 @@ function createAzureTableStorage(options = {}) {
|
|
|
555
582
|
code: "transaction_unsupported"
|
|
556
583
|
});
|
|
557
584
|
const partitionKey = codec.createPartitionKey(stored);
|
|
558
|
-
|
|
585
|
+
const formScope = saveOptions.responseLimitScope === "form";
|
|
586
|
+
const capacityVersion = formScope ? void 0 : stored.formVersion;
|
|
587
|
+
if (codec.createPartitionKeyFromQuery(stored.formId, {
|
|
588
|
+
...capacityVersion === void 0 ? {} : { version: capacityVersion }
|
|
589
|
+
}) !== partitionKey)
|
|
559
590
|
throw new Error("Atomic response limits require one Azure Table partition per form.");
|
|
560
591
|
const rowKey = codec.createRowKey(stored);
|
|
561
592
|
const payloadHash = await (0, import_core.hashFormSubmissionPayload)(stored);
|
|
@@ -581,14 +612,14 @@ function createAzureTableStorage(options = {}) {
|
|
|
581
612
|
} catch (error) {
|
|
582
613
|
if (!isNotFound(error)) throw error;
|
|
583
614
|
}
|
|
584
|
-
const capacityRowKey = `capacity_${stored.formVersion}`;
|
|
615
|
+
const capacityRowKey = formScope ? "capacity_form" : `capacity_${stored.formVersion}`;
|
|
585
616
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
586
617
|
let capacity;
|
|
587
618
|
try {
|
|
588
619
|
capacity = await client.getEntity(partitionKey, capacityRowKey);
|
|
589
620
|
} catch (error) {
|
|
590
621
|
if (!isNotFound(error)) throw error;
|
|
591
|
-
const count2 = await adapter.countSubmissions(stored.formId,
|
|
622
|
+
const count2 = await adapter.countSubmissions(stored.formId, capacityVersion);
|
|
592
623
|
try {
|
|
593
624
|
await client.createEntity({ partitionKey, rowKey: capacityRowKey, kind: "capacity", count: count2 });
|
|
594
625
|
} catch (createError) {
|
|
@@ -596,7 +627,7 @@ function createAzureTableStorage(options = {}) {
|
|
|
596
627
|
}
|
|
597
628
|
continue;
|
|
598
629
|
}
|
|
599
|
-
const count = await adapter.countSubmissions(stored.formId,
|
|
630
|
+
const count = await adapter.countSubmissions(stored.formId, capacityVersion);
|
|
600
631
|
if (count >= maxResponses) return { status: "limit_reached" };
|
|
601
632
|
if (typeof capacity.etag !== "string") throw new Error("Azure Table capacity entity is missing an ETag.");
|
|
602
633
|
const actions = [
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TransactionAction } from '@azure/data-tables';
|
|
2
|
-
import { BaseSubmissionMetadata, PagedSubmissionStorageAdapter, TextAnswerPageQueryOptions, TextAnswerPage, TypedSubmissionPageQueryOptions, TypedSubmissionPage, FormSubmission, FormSchema, SubmissionPageQueryOptions, FormAnalytics, StorageSubmissionExportOptions, FormSubmissionValidationSource, ValidateFormSchemaOptions, FormLifecycleOptions, FormSubmissionValidator, SaveSubmissionOptions, SubmissionSaveResult, SubmissionFilter, JsonValue, TypedTextAnswerPage } from '@form-engine-ts/core';
|
|
2
|
+
import { BaseSubmissionMetadata, PagedSubmissionStorageAdapter, FormRuntimeSettings, TextAnswerPageQueryOptions, TextAnswerPage, TypedSubmissionPageQueryOptions, TypedSubmissionPage, FormSubmission, FormSchema, SubmissionPageQueryOptions, FormAnalytics, StorageSubmissionExportOptions, FormSubmissionValidationSource, ValidateFormSchemaOptions, FormLifecycleOptions, FormSubmissionValidator, SaveSubmissionOptions, SubmissionSaveResult, SubmissionFilter, JsonValue, TypedTextAnswerPage } from '@form-engine-ts/core';
|
|
3
3
|
|
|
4
4
|
interface AzureTableListOptions {
|
|
5
5
|
readonly queryOptions?: {
|
|
@@ -82,6 +82,8 @@ interface AzureTableStorageOptions<T = FormSubmission> {
|
|
|
82
82
|
readonly schema?: FormSubmissionValidationSource;
|
|
83
83
|
}
|
|
84
84
|
interface AzureTableStorageAdapter<TMeta extends BaseSubmissionMetadata = BaseSubmissionMetadata> extends PagedSubmissionStorageAdapter {
|
|
85
|
+
getRuntimeSettings?(formId: string): Promise<FormRuntimeSettings | null>;
|
|
86
|
+
saveRuntimeSettings?(settings: FormRuntimeSettings): Promise<void>;
|
|
85
87
|
readonly listTextAnswerPage: (formId: string, fieldIdOrOptions?: string | TextAnswerPageQueryOptions, options?: TextAnswerPageQueryOptions) => Promise<TextAnswerPage>;
|
|
86
88
|
readonly fetchSubmissionPage?: (formId: string, options?: TypedSubmissionPageQueryOptions<TMeta>) => Promise<TypedSubmissionPage<TMeta>>;
|
|
87
89
|
readonly fetchPage: (formId: string, options?: {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TransactionAction } from '@azure/data-tables';
|
|
2
|
-
import { BaseSubmissionMetadata, PagedSubmissionStorageAdapter, TextAnswerPageQueryOptions, TextAnswerPage, TypedSubmissionPageQueryOptions, TypedSubmissionPage, FormSubmission, FormSchema, SubmissionPageQueryOptions, FormAnalytics, StorageSubmissionExportOptions, FormSubmissionValidationSource, ValidateFormSchemaOptions, FormLifecycleOptions, FormSubmissionValidator, SaveSubmissionOptions, SubmissionSaveResult, SubmissionFilter, JsonValue, TypedTextAnswerPage } from '@form-engine-ts/core';
|
|
2
|
+
import { BaseSubmissionMetadata, PagedSubmissionStorageAdapter, FormRuntimeSettings, TextAnswerPageQueryOptions, TextAnswerPage, TypedSubmissionPageQueryOptions, TypedSubmissionPage, FormSubmission, FormSchema, SubmissionPageQueryOptions, FormAnalytics, StorageSubmissionExportOptions, FormSubmissionValidationSource, ValidateFormSchemaOptions, FormLifecycleOptions, FormSubmissionValidator, SaveSubmissionOptions, SubmissionSaveResult, SubmissionFilter, JsonValue, TypedTextAnswerPage } from '@form-engine-ts/core';
|
|
3
3
|
|
|
4
4
|
interface AzureTableListOptions {
|
|
5
5
|
readonly queryOptions?: {
|
|
@@ -82,6 +82,8 @@ interface AzureTableStorageOptions<T = FormSubmission> {
|
|
|
82
82
|
readonly schema?: FormSubmissionValidationSource;
|
|
83
83
|
}
|
|
84
84
|
interface AzureTableStorageAdapter<TMeta extends BaseSubmissionMetadata = BaseSubmissionMetadata> extends PagedSubmissionStorageAdapter {
|
|
85
|
+
getRuntimeSettings?(formId: string): Promise<FormRuntimeSettings | null>;
|
|
86
|
+
saveRuntimeSettings?(settings: FormRuntimeSettings): Promise<void>;
|
|
85
87
|
readonly listTextAnswerPage: (formId: string, fieldIdOrOptions?: string | TextAnswerPageQueryOptions, options?: TextAnswerPageQueryOptions) => Promise<TextAnswerPage>;
|
|
86
88
|
readonly fetchSubmissionPage?: (formId: string, options?: TypedSubmissionPageQueryOptions<TMeta>) => Promise<TypedSubmissionPage<TMeta>>;
|
|
87
89
|
readonly fetchPage: (formId: string, options?: {
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import {
|
|
3
3
|
aggregateResponses,
|
|
4
|
+
assertValidFormRuntimeSettings,
|
|
4
5
|
assertValidFormSchema,
|
|
5
6
|
assertValidFormSubmission,
|
|
6
7
|
assertValidFormSubmissionWith,
|
|
@@ -388,16 +389,18 @@ function createAzureTableStorage(options = {}) {
|
|
|
388
389
|
);
|
|
389
390
|
};
|
|
390
391
|
const lifecycleBackend = {
|
|
391
|
-
resources: ["schema", "submission"],
|
|
392
|
+
resources: ["schema", "submission", "runtimeSettings"],
|
|
392
393
|
async list(formId) {
|
|
393
394
|
const records = [];
|
|
394
|
-
for (const kind of ["schema", "submission"]) {
|
|
395
|
-
const
|
|
396
|
-
const
|
|
395
|
+
for (const kind of ["schema", "submission", "runtimeSettings"]) {
|
|
396
|
+
const isSchema = kind === "schema" || kind === "runtimeSettings";
|
|
397
|
+
const client = isSchema ? await schemaClient(formId) : await submissionClient(formId);
|
|
398
|
+
const filter = isSchema ? `PartitionKey eq '${escapeOData(formId)}' and kind eq '${kind === "schema" ? "schema" : "runtimeSettings"}'` : queryFilter(formId, {});
|
|
397
399
|
for await (const raw of client.listEntities({ queryOptions: { filter } })) {
|
|
398
|
-
if (
|
|
399
|
-
const value = kind === "schema" ? parseSchemaEntity(raw, options.schemaValidation) : deserializeIfMatching(raw);
|
|
400
|
-
|
|
400
|
+
if (isSchema && raw.kind !== kind) continue;
|
|
401
|
+
const value = kind === "schema" ? parseSchemaEntity(raw, options.schemaValidation) : kind === "runtimeSettings" ? parseJson(raw.payload, "runtime settings") : deserializeIfMatching(raw);
|
|
402
|
+
const resourceFormId = isRecord(value) ? typeof value.formId === "string" ? value.formId : typeof value.id === "string" ? value.id : void 0 : void 0;
|
|
403
|
+
if (resourceFormId !== formId) continue;
|
|
401
404
|
if (typeof raw.partitionKey !== "string" || typeof raw.rowKey !== "string")
|
|
402
405
|
throw new TypeError("Missing entity keys.");
|
|
403
406
|
records.push({ kind, id: JSON.stringify([raw.partitionKey, raw.rowKey]), value: { raw, value, formId } });
|
|
@@ -412,7 +415,7 @@ function createAzureTableStorage(options = {}) {
|
|
|
412
415
|
const { raw, formId } = resource.value;
|
|
413
416
|
if (typeof raw.partitionKey !== "string" || typeof raw.rowKey !== "string" || typeof raw.etag !== "string")
|
|
414
417
|
throw new TypeError("Conditional deletion requires entity keys and an ETag.");
|
|
415
|
-
const client = resource.kind === "schema" ? await schemaClient(formId) : await submissionClient(formId);
|
|
418
|
+
const client = resource.kind === "schema" || resource.kind === "runtimeSettings" ? await schemaClient(formId) : await submissionClient(formId);
|
|
416
419
|
try {
|
|
417
420
|
await client.deleteEntity(raw.partitionKey, raw.rowKey, { etag: raw.etag });
|
|
418
421
|
return 1;
|
|
@@ -458,6 +461,31 @@ function createAzureTableStorage(options = {}) {
|
|
|
458
461
|
throw error;
|
|
459
462
|
}
|
|
460
463
|
},
|
|
464
|
+
async getRuntimeSettings(formId) {
|
|
465
|
+
try {
|
|
466
|
+
const entity = await (await schemaClient(formId)).getEntity(formId, "runtime-settings");
|
|
467
|
+
const settings = parseJson(entity.payload, "runtime settings");
|
|
468
|
+
assertValidFormRuntimeSettings(settings);
|
|
469
|
+
if (settings.formId !== formId) throw new Error("Azure Table runtime settings have inconsistent metadata.");
|
|
470
|
+
return cloneJson(settings);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
if (isNotFound(error)) return null;
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
async saveRuntimeSettings(settings) {
|
|
477
|
+
ensureWritable();
|
|
478
|
+
assertValidFormRuntimeSettings(settings);
|
|
479
|
+
await (await schemaClient(settings.formId)).upsertEntity(
|
|
480
|
+
{
|
|
481
|
+
partitionKey: settings.formId,
|
|
482
|
+
rowKey: "runtime-settings",
|
|
483
|
+
kind: "runtimeSettings",
|
|
484
|
+
payload: JSON.stringify(settings)
|
|
485
|
+
},
|
|
486
|
+
"Replace"
|
|
487
|
+
);
|
|
488
|
+
},
|
|
461
489
|
async listSchemas() {
|
|
462
490
|
const found = [];
|
|
463
491
|
for await (const raw of (await schemaClient("")).listEntities({ queryOptions: { filter: "kind eq 'schema'" } })) {
|
|
@@ -539,7 +567,11 @@ function createAzureTableStorage(options = {}) {
|
|
|
539
567
|
code: "transaction_unsupported"
|
|
540
568
|
});
|
|
541
569
|
const partitionKey = codec.createPartitionKey(stored);
|
|
542
|
-
|
|
570
|
+
const formScope = saveOptions.responseLimitScope === "form";
|
|
571
|
+
const capacityVersion = formScope ? void 0 : stored.formVersion;
|
|
572
|
+
if (codec.createPartitionKeyFromQuery(stored.formId, {
|
|
573
|
+
...capacityVersion === void 0 ? {} : { version: capacityVersion }
|
|
574
|
+
}) !== partitionKey)
|
|
543
575
|
throw new Error("Atomic response limits require one Azure Table partition per form.");
|
|
544
576
|
const rowKey = codec.createRowKey(stored);
|
|
545
577
|
const payloadHash = await hashFormSubmissionPayload(stored);
|
|
@@ -565,14 +597,14 @@ function createAzureTableStorage(options = {}) {
|
|
|
565
597
|
} catch (error) {
|
|
566
598
|
if (!isNotFound(error)) throw error;
|
|
567
599
|
}
|
|
568
|
-
const capacityRowKey = `capacity_${stored.formVersion}`;
|
|
600
|
+
const capacityRowKey = formScope ? "capacity_form" : `capacity_${stored.formVersion}`;
|
|
569
601
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
570
602
|
let capacity;
|
|
571
603
|
try {
|
|
572
604
|
capacity = await client.getEntity(partitionKey, capacityRowKey);
|
|
573
605
|
} catch (error) {
|
|
574
606
|
if (!isNotFound(error)) throw error;
|
|
575
|
-
const count2 = await adapter.countSubmissions(stored.formId,
|
|
607
|
+
const count2 = await adapter.countSubmissions(stored.formId, capacityVersion);
|
|
576
608
|
try {
|
|
577
609
|
await client.createEntity({ partitionKey, rowKey: capacityRowKey, kind: "capacity", count: count2 });
|
|
578
610
|
} catch (createError) {
|
|
@@ -580,7 +612,7 @@ function createAzureTableStorage(options = {}) {
|
|
|
580
612
|
}
|
|
581
613
|
continue;
|
|
582
614
|
}
|
|
583
|
-
const count = await adapter.countSubmissions(stored.formId,
|
|
615
|
+
const count = await adapter.countSubmissions(stored.formId, capacityVersion);
|
|
584
616
|
if (count >= maxResponses) return { status: "limit_reached" };
|
|
585
617
|
if (typeof capacity.etag !== "string") throw new Error("Azure Table capacity entity is missing an ETag.");
|
|
586
618
|
const actions = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@form-engine-ts/storage-azure-table",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.2.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": "8.
|
|
42
|
+
"@form-engine-ts/core": "8.2.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@azure/data-tables": "^13.3.2"
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@azure/data-tables": "^13.3.2",
|
|
54
|
-
"@form-engine-ts/storage": "8.
|
|
54
|
+
"@form-engine-ts/storage": "8.2.0"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "tsup src/index.ts --format esm,cjs --dts --clean --external @form-engine-ts/core",
|