@form-engine-ts/storage-azure-table 2.6.0 → 2.8.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 +15 -6
- package/dist/index.cjs +301 -65
- package/dist/index.d.cts +25 -6
- package/dist/index.d.ts +25 -6
- package/dist/index.js +299 -64
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -25,9 +25,18 @@ const storage = createAzureTableStorage({ schemasTableClient, submissionsTableCl
|
|
|
25
25
|
const page = await storage.listSubmissionPage("contact", { pageSize: 500, locale: "ja" });
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
The default codec uses `formId` as `PartitionKey` and `submittedAt_responseId` as `RowKey`. Supply an
|
|
29
|
+
`AzureTableSubmissionCodec` to define a completely different entity layout, key strategy, deserializer, and client-side
|
|
30
|
+
matcher; custom entities are not polluted with default discriminator fields. `clientResolver` can select a table client
|
|
31
|
+
per form and operation. Date, locale, version, cursor, metadata, and supported filter-AST constraints are sent as OData;
|
|
32
|
+
unsupported expressions remain client-filtered with identical semantics.
|
|
33
|
+
|
|
34
|
+
`listSubmissionPage` follows opaque Azure continuation tokens and scans at most `maxScanPages` native pages (default 5)
|
|
35
|
+
to fill the requested logical page after client-side filtering. `buildSubmissionFilter` can replace filter generation.
|
|
36
|
+
The deprecated `client`, `submissionCodec`, and `toODataFilter` options remain available for compatibility. The caller
|
|
37
|
+
owns table creation, credentials, retries, and client lifecycle.
|
|
38
|
+
|
|
39
|
+
`listTextAnswerPage` accepts either the legacy single field ID or `TextAnswerPageQueryOptions.fieldIds`. Page size counts
|
|
40
|
+
emitted text items rather than entities. Its opaque Base64 JSON cursor retains the Azure continuation token plus entity
|
|
41
|
+
and field indexes, so a page can resume inside a multi-answer entity without gaps or duplicates. Empty answers do not
|
|
42
|
+
consume the item limit, and scanning remains bounded by `maxScanPages`.
|
package/dist/index.cjs
CHANGED
|
@@ -22,10 +22,75 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
createAzureTableStorage: () => createAzureTableStorage,
|
|
24
24
|
defaultAzureTableSubmissionCodec: () => defaultAzureTableSubmissionCodec,
|
|
25
|
-
metadataFiltersToOData: () => metadataFiltersToOData
|
|
25
|
+
metadataFiltersToOData: () => metadataFiltersToOData,
|
|
26
|
+
submissionFilterToOData: () => submissionFilterToOData
|
|
26
27
|
});
|
|
27
28
|
module.exports = __toCommonJS(index_exports);
|
|
28
29
|
var import_core = require("@form-engine-ts/core");
|
|
30
|
+
var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
31
|
+
function encodeBase64(bytes) {
|
|
32
|
+
let result = "";
|
|
33
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
34
|
+
const first = bytes[index] ?? 0;
|
|
35
|
+
const second = bytes[index + 1] ?? 0;
|
|
36
|
+
const third = bytes[index + 2] ?? 0;
|
|
37
|
+
const combined = first << 16 | second << 8 | third;
|
|
38
|
+
result += BASE64_ALPHABET[combined >> 18 & 63] ?? "";
|
|
39
|
+
result += BASE64_ALPHABET[combined >> 12 & 63] ?? "";
|
|
40
|
+
result += index + 1 < bytes.length ? BASE64_ALPHABET[combined >> 6 & 63] ?? "" : "=";
|
|
41
|
+
result += index + 2 < bytes.length ? BASE64_ALPHABET[combined & 63] ?? "" : "=";
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
function decodeBase64(value) {
|
|
46
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
|
|
47
|
+
throw new TypeError("cursor must be a valid Base64 token.");
|
|
48
|
+
}
|
|
49
|
+
const bytes = [];
|
|
50
|
+
for (let index = 0; index < value.length; index += 4) {
|
|
51
|
+
const characters = value.slice(index, index + 4);
|
|
52
|
+
const sextets = [...characters].map((character) => character === "=" ? 0 : BASE64_ALPHABET.indexOf(character));
|
|
53
|
+
const combined = (sextets[0] ?? 0) << 18 | (sextets[1] ?? 0) << 12 | (sextets[2] ?? 0) << 6 | (sextets[3] ?? 0);
|
|
54
|
+
bytes.push(combined >> 16 & 255);
|
|
55
|
+
if (characters[2] !== "=") bytes.push(combined >> 8 & 255);
|
|
56
|
+
if (characters[3] !== "=") bytes.push(combined & 255);
|
|
57
|
+
}
|
|
58
|
+
return new Uint8Array(bytes);
|
|
59
|
+
}
|
|
60
|
+
function encodeAzureTextAnswerCursor(value) {
|
|
61
|
+
return encodeBase64(new TextEncoder().encode(JSON.stringify(value)));
|
|
62
|
+
}
|
|
63
|
+
function decodeAzureTextAnswerCursor(cursor) {
|
|
64
|
+
try {
|
|
65
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(decodeBase64(cursor)));
|
|
66
|
+
if (!isRecord(value) || value.tableContinuationToken !== null && typeof value.tableContinuationToken !== "string" || !Number.isSafeInteger(value.entityIndex) || value.entityIndex < 0 || !Number.isSafeInteger(value.fieldIndex) || value.fieldIndex < 0) {
|
|
67
|
+
throw new TypeError("Azure text answer cursor payload is invalid.");
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
tableContinuationToken: value.tableContinuationToken,
|
|
71
|
+
entityIndex: value.entityIndex,
|
|
72
|
+
fieldIndex: value.fieldIndex
|
|
73
|
+
};
|
|
74
|
+
} catch (cause) {
|
|
75
|
+
if (cause instanceof TypeError && cause.message === "Azure text answer cursor payload is invalid.") throw cause;
|
|
76
|
+
throw new TypeError("cursor must be a valid Azure text answer cursor.", { cause });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function textAnswerQuery(fieldIdOrOptions, options) {
|
|
80
|
+
const query = typeof fieldIdOrOptions === "string" ? options ?? {} : fieldIdOrOptions ?? {};
|
|
81
|
+
const requested = typeof fieldIdOrOptions === "string" ? [fieldIdOrOptions] : query.fieldIds;
|
|
82
|
+
if (requested?.some((fieldId) => fieldId.trim().length === 0)) {
|
|
83
|
+
throw new TypeError("fieldIds must not contain empty values.");
|
|
84
|
+
}
|
|
85
|
+
const fieldIds = requested === void 0 ? void 0 : [...new Set(requested)];
|
|
86
|
+
return { query, ...fieldIds === void 0 ? {} : { fieldIds } };
|
|
87
|
+
}
|
|
88
|
+
function submissionTextAnswers(submission, fieldIds) {
|
|
89
|
+
const entries = fieldIds === void 0 ? Object.entries(submission.values) : fieldIds.map((id) => [id, submission.values[id]]);
|
|
90
|
+
return entries.flatMap(
|
|
91
|
+
([fieldId, value]) => typeof value === "string" && value.length > 0 ? [{ fieldId, text: value }] : []
|
|
92
|
+
);
|
|
93
|
+
}
|
|
29
94
|
function cloneJson(value) {
|
|
30
95
|
return JSON.parse(JSON.stringify(value));
|
|
31
96
|
}
|
|
@@ -66,12 +131,38 @@ function scalarMetadata(metadata) {
|
|
|
66
131
|
);
|
|
67
132
|
}
|
|
68
133
|
var defaultAzureTableSubmissionCodec = {
|
|
134
|
+
createEntity: (submission) => ({
|
|
135
|
+
...scalarMetadata(submission.metadata),
|
|
136
|
+
kind: "submission",
|
|
137
|
+
formVersion: submission.formVersion,
|
|
138
|
+
locale: submission.locale,
|
|
139
|
+
submittedAt: submission.submittedAt,
|
|
140
|
+
responseId: submission.id,
|
|
141
|
+
payload: JSON.stringify(submission)
|
|
142
|
+
}),
|
|
143
|
+
deserialize: (entity) => parseSubmission(entity.payload, "submission entity"),
|
|
144
|
+
matchesEntity: (entity) => entity.kind === "submission",
|
|
69
145
|
createPartitionKey: (submission) => submission.formId,
|
|
70
|
-
|
|
71
|
-
createRowKey: defaultSubmissionRowKey
|
|
72
|
-
serialize: (submission) => ({ ...scalarMetadata(submission.metadata), payload: JSON.stringify(submission) }),
|
|
73
|
-
deserialize: (entity) => parseSubmission(entity.payload, "submission entity")
|
|
146
|
+
createPartitionKeyFromQuery: (formId) => formId,
|
|
147
|
+
createRowKey: defaultSubmissionRowKey
|
|
74
148
|
};
|
|
149
|
+
function legacyCodec(codec) {
|
|
150
|
+
return {
|
|
151
|
+
createEntity: (value) => ({
|
|
152
|
+
...codec.serialize(value),
|
|
153
|
+
kind: "submission",
|
|
154
|
+
formVersion: value.formVersion,
|
|
155
|
+
locale: value.locale,
|
|
156
|
+
submittedAt: value.submittedAt,
|
|
157
|
+
responseId: value.id
|
|
158
|
+
}),
|
|
159
|
+
deserialize: codec.deserialize,
|
|
160
|
+
matchesEntity: (entity) => entity.kind === "submission",
|
|
161
|
+
createPartitionKey: codec.createPartitionKey,
|
|
162
|
+
createPartitionKeyFromQuery: (formId) => codec.createPartitionKeyFromFormId?.(formId) ?? formId,
|
|
163
|
+
createRowKey: codec.createRowKey
|
|
164
|
+
};
|
|
165
|
+
}
|
|
75
166
|
function parseSchemaEntity(value) {
|
|
76
167
|
if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || value.kind !== "schema" || !Number.isInteger(value.formVersion) || typeof value.payload !== "string") {
|
|
77
168
|
throw new Error("Azure Table schema entity is invalid.");
|
|
@@ -84,40 +175,73 @@ function parseSchemaEntity(value) {
|
|
|
84
175
|
return cloneJson(schema);
|
|
85
176
|
}
|
|
86
177
|
function parseSubmissionEntity(value, codec) {
|
|
87
|
-
if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || value
|
|
178
|
+
if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || !codec.matchesEntity(value)) {
|
|
88
179
|
throw new Error("Azure Table submission entity is invalid.");
|
|
89
180
|
}
|
|
90
181
|
const submission = parseSubmission(codec.deserialize(value), `submission ${value.partitionKey}/${value.rowKey}`);
|
|
91
|
-
if (codec.createPartitionKey(submission) !== value.partitionKey || codec.createRowKey(submission) !== value.rowKey
|
|
92
|
-
throw new Error("Azure Table submission entity has inconsistent
|
|
182
|
+
if (codec.createPartitionKey(submission) !== value.partitionKey || codec.createRowKey(submission) !== value.rowKey) {
|
|
183
|
+
throw new Error("Azure Table submission entity has inconsistent keys.");
|
|
93
184
|
}
|
|
94
185
|
return submission;
|
|
95
186
|
}
|
|
96
187
|
function escapeOData(value) {
|
|
97
188
|
return value.replaceAll("'", "''");
|
|
98
189
|
}
|
|
99
|
-
function
|
|
190
|
+
function valueToOData(value) {
|
|
100
191
|
if (value === null) return "null";
|
|
101
192
|
if (typeof value === "string") return `'${escapeOData(value)}'`;
|
|
102
193
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
103
194
|
if (typeof value === "boolean") return String(value);
|
|
104
|
-
throw new TypeError("Azure Table
|
|
195
|
+
throw new TypeError("Azure Table OData filters support only scalar JSON values.");
|
|
105
196
|
}
|
|
106
197
|
function metadataFiltersToOData(options) {
|
|
107
198
|
return Object.entries(options.metadataFilters ?? {}).map(([key, value]) => {
|
|
108
199
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new TypeError(`Invalid Azure Table property name: ${key}`);
|
|
109
|
-
return `${key} eq ${
|
|
200
|
+
return `${key} eq ${valueToOData(value)}`;
|
|
110
201
|
}).join(" and ");
|
|
111
202
|
}
|
|
112
|
-
function
|
|
203
|
+
function odataProperty(path) {
|
|
204
|
+
if (path === "id" || path === "responseId") return "responseId";
|
|
205
|
+
if (["formVersion", "locale", "submittedAt"].includes(path)) return path;
|
|
206
|
+
if (path.startsWith("metadata.")) {
|
|
207
|
+
const property = path.slice("metadata.".length);
|
|
208
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(property) ? property : void 0;
|
|
209
|
+
}
|
|
210
|
+
return void 0;
|
|
211
|
+
}
|
|
212
|
+
function submissionFilterToOData(filter) {
|
|
213
|
+
if (filter.op === "and" || filter.op === "or") {
|
|
214
|
+
const converted = filter.filters.map(submissionFilterToOData);
|
|
215
|
+
if (filter.op === "or" && converted.some((value) => value === void 0)) return void 0;
|
|
216
|
+
const available = converted.filter((value) => value !== void 0 && value.length > 0);
|
|
217
|
+
if (available.length === 0) return void 0;
|
|
218
|
+
return available.map((value) => `(${value})`).join(filter.op === "and" ? " and " : " or ");
|
|
219
|
+
}
|
|
220
|
+
const property = odataProperty(filter.path);
|
|
221
|
+
if (property === void 0) return void 0;
|
|
222
|
+
if (filter.op === "eq") return `${property} eq ${valueToOData(filter.value)}`;
|
|
223
|
+
if (filter.op === "in") {
|
|
224
|
+
if (filter.values.length === 0) return "false";
|
|
225
|
+
return filter.values.map((value) => `${property} eq ${valueToOData(value)}`).join(" or ");
|
|
226
|
+
}
|
|
227
|
+
if (filter.op === "exists") return `${property} ${filter.value ? "ne" : "eq"} null`;
|
|
113
228
|
return [
|
|
114
|
-
...
|
|
115
|
-
|
|
229
|
+
...filter.from === void 0 ? [] : [`${property} ge ${valueToOData(filter.from)}`],
|
|
230
|
+
...filter.to === void 0 ? [] : [`${property} le ${valueToOData(filter.to)}`]
|
|
231
|
+
].join(" and ");
|
|
232
|
+
}
|
|
233
|
+
function defaultSubmissionFilter(codec, formId, options, legacyExtension) {
|
|
234
|
+
const partitionKey = codec.createPartitionKeyFromQuery(formId, options);
|
|
235
|
+
const ast = options.filter === void 0 || typeof options.filter === "function" ? void 0 : submissionFilterToOData(options.filter);
|
|
236
|
+
return [
|
|
237
|
+
...partitionKey === void 0 ? [] : [`PartitionKey eq '${escapeOData(partitionKey)}'`],
|
|
238
|
+
...codec === defaultAzureTableSubmissionCodec ? ["kind eq 'submission'"] : [],
|
|
116
239
|
...options.version === void 0 ? [] : [`formVersion eq ${options.version}`],
|
|
117
240
|
...options.since === void 0 ? [] : [`submittedAt ge '${escapeOData(options.since)}'`],
|
|
118
241
|
...options.until === void 0 ? [] : [`submittedAt le '${escapeOData(options.until)}'`],
|
|
119
242
|
...options.locale === void 0 ? [] : [`locale eq '${escapeOData(options.locale)}'`],
|
|
120
|
-
...
|
|
243
|
+
...ast === void 0 || ast.length === 0 ? [] : [`(${ast})`],
|
|
244
|
+
...legacyExtension.trim().length === 0 ? [] : [`(${legacyExtension})`]
|
|
121
245
|
].join(" and ");
|
|
122
246
|
}
|
|
123
247
|
function isNotFound(error) {
|
|
@@ -126,26 +250,41 @@ function isNotFound(error) {
|
|
|
126
250
|
function matchesBuiltInFilters(submission, formId, options) {
|
|
127
251
|
return submission.formId === formId && (options.version === void 0 || submission.formVersion === options.version) && (options.since === void 0 || submission.submittedAt >= options.since) && (options.until === void 0 || submission.submittedAt <= options.until) && (options.locale === void 0 || submission.locale === options.locale);
|
|
128
252
|
}
|
|
129
|
-
function
|
|
130
|
-
const client = preferred ?? fallback;
|
|
253
|
+
function requireClient(client, name) {
|
|
131
254
|
if (client === void 0) throw new TypeError(`${name} is required.`);
|
|
132
255
|
return client;
|
|
133
256
|
}
|
|
134
|
-
function createAzureTableStorage(options) {
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
const codec = options.
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
257
|
+
function createAzureTableStorage(options = {}) {
|
|
258
|
+
const staticSchemas = options.schemasTableClient ?? options.client;
|
|
259
|
+
const staticSubmissions = options.submissionsTableClient ?? options.client;
|
|
260
|
+
const codec = options.codec ?? (options.submissionCodec === void 0 ? defaultAzureTableSubmissionCodec : legacyCodec(options.submissionCodec));
|
|
261
|
+
const maxScanPages = options.maxScanPages ?? 5;
|
|
262
|
+
if (!Number.isSafeInteger(maxScanPages) || maxScanPages < 1) {
|
|
263
|
+
throw new TypeError("maxScanPages must be a positive safe integer.");
|
|
264
|
+
}
|
|
265
|
+
const resolveDynamicClient = async (formId, query) => options.clientResolver?.({ formId, ...query === void 0 ? {} : { query } });
|
|
266
|
+
const schemaClient = async (formId) => requireClient(staticSchemas ?? await resolveDynamicClient(formId), "schemasTableClient or clientResolver");
|
|
267
|
+
const submissionClient = async (formId, query) => requireClient(
|
|
268
|
+
await resolveDynamicClient(formId, query) ?? staticSubmissions,
|
|
269
|
+
"submissionsTableClient or clientResolver"
|
|
270
|
+
);
|
|
271
|
+
const queryFilter = (formId, query) => {
|
|
272
|
+
if (options.buildSubmissionFilter !== void 0) return options.buildSubmissionFilter(formId, query);
|
|
273
|
+
return defaultSubmissionFilter(
|
|
274
|
+
codec,
|
|
275
|
+
formId,
|
|
276
|
+
query,
|
|
277
|
+
options.toODataFilter?.(query) ?? metadataFiltersToOData(query)
|
|
278
|
+
);
|
|
279
|
+
};
|
|
280
|
+
const deserializeIfMatching = (entity) => codec.matchesEntity(entity) ? parseSubmissionEntity(entity, codec) : void 0;
|
|
141
281
|
const listSubmissionCandidates = async (formId, query) => {
|
|
282
|
+
const client = await submissionClient(formId, query);
|
|
142
283
|
const found = [];
|
|
143
|
-
for await (const raw of
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
if (!(0, import_core.matchesSubmissionPageFilters)(submission, query)) continue;
|
|
148
|
-
found.push(submission);
|
|
284
|
+
for await (const raw of client.listEntities({ queryOptions: { filter: queryFilter(formId, query) } })) {
|
|
285
|
+
const submission = deserializeIfMatching(raw);
|
|
286
|
+
if (submission === void 0 || !matchesBuiltInFilters(submission, formId, query)) continue;
|
|
287
|
+
if ((0, import_core.matchesSubmissionPageFilters)(submission, query)) found.push(submission);
|
|
149
288
|
}
|
|
150
289
|
return found.sort(
|
|
151
290
|
(left, right) => left.submittedAt.localeCompare(right.submittedAt) || left.id.localeCompare(right.id)
|
|
@@ -161,11 +300,11 @@ function createAzureTableStorage(options) {
|
|
|
161
300
|
formVersion: schema.version,
|
|
162
301
|
payload: JSON.stringify(schema)
|
|
163
302
|
};
|
|
164
|
-
await
|
|
303
|
+
await (await schemaClient(schema.id)).upsertEntity(entity, "Replace");
|
|
165
304
|
},
|
|
166
305
|
async getSchema(formId, formVersion) {
|
|
167
306
|
try {
|
|
168
|
-
return parseSchemaEntity(await
|
|
307
|
+
return parseSchemaEntity(await (await schemaClient(formId)).getEntity(formId, schemaRowKey(formVersion)));
|
|
169
308
|
} catch (error) {
|
|
170
309
|
if (isNotFound(error)) return null;
|
|
171
310
|
throw error;
|
|
@@ -173,25 +312,20 @@ function createAzureTableStorage(options) {
|
|
|
173
312
|
},
|
|
174
313
|
async listSchemas() {
|
|
175
314
|
const found = [];
|
|
176
|
-
for await (const raw of
|
|
315
|
+
for await (const raw of (await schemaClient("")).listEntities({ queryOptions: { filter: "kind eq 'schema'" } })) {
|
|
177
316
|
if (raw.kind === "schema") found.push(parseSchemaEntity(raw));
|
|
178
317
|
}
|
|
179
318
|
return found.sort((left, right) => left.id.localeCompare(right.id) || left.version - right.version);
|
|
180
319
|
},
|
|
181
320
|
async deleteSchema(formId, formVersion) {
|
|
182
|
-
await
|
|
321
|
+
await (await schemaClient(formId)).deleteEntity(formId, schemaRowKey(formVersion));
|
|
183
322
|
},
|
|
184
323
|
async saveSubmission(submission) {
|
|
185
324
|
const stored = parseSubmission(submission, `input ${String(submission?.id)}`);
|
|
186
|
-
await
|
|
187
|
-
...codec.
|
|
325
|
+
await (await submissionClient(stored.formId)).createEntity({
|
|
326
|
+
...codec.createEntity(stored),
|
|
188
327
|
partitionKey: codec.createPartitionKey(stored),
|
|
189
|
-
rowKey: codec.createRowKey(stored)
|
|
190
|
-
kind: "submission",
|
|
191
|
-
formVersion: stored.formVersion,
|
|
192
|
-
locale: stored.locale,
|
|
193
|
-
submittedAt: stored.submittedAt,
|
|
194
|
-
responseId: stored.id
|
|
328
|
+
rowKey: codec.createRowKey(stored)
|
|
195
329
|
});
|
|
196
330
|
},
|
|
197
331
|
async listSubmissions(formId, formVersion, queryOptions = {}) {
|
|
@@ -202,46 +336,147 @@ function createAzureTableStorage(options) {
|
|
|
202
336
|
},
|
|
203
337
|
async listSubmissionPage(formId, query = {}) {
|
|
204
338
|
const pageSize = (0, import_core.normalizeSubmissionPageSize)(query.pageSize);
|
|
205
|
-
const
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
339
|
+
const client = await submissionClient(formId, query);
|
|
340
|
+
const items = [];
|
|
341
|
+
let continuationToken = query.cursor;
|
|
342
|
+
let scannedPages = 0;
|
|
343
|
+
do {
|
|
344
|
+
const iterator = client.listEntities({ queryOptions: { filter: queryFilter(formId, query) } }).byPage({
|
|
345
|
+
maxPageSize: Math.max(1, pageSize - items.length),
|
|
346
|
+
...continuationToken === void 0 ? {} : { continuationToken }
|
|
347
|
+
});
|
|
348
|
+
const result = await iterator.next();
|
|
349
|
+
if (result.done === true) {
|
|
350
|
+
continuationToken = void 0;
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
scannedPages += 1;
|
|
354
|
+
for (const raw of result.value) {
|
|
355
|
+
const submission = deserializeIfMatching(raw);
|
|
356
|
+
if (submission === void 0 || !matchesBuiltInFilters(submission, formId, query)) continue;
|
|
357
|
+
if ((0, import_core.matchesSubmissionPageFilters)(submission, query)) items.push(submission);
|
|
358
|
+
}
|
|
359
|
+
continuationToken = result.value.continuationToken;
|
|
360
|
+
} while (items.length < pageSize && continuationToken !== void 0 && scannedPages < maxScanPages);
|
|
212
361
|
return {
|
|
213
362
|
items,
|
|
214
|
-
hasMore:
|
|
215
|
-
...
|
|
363
|
+
hasMore: continuationToken !== void 0 && continuationToken.length > 0,
|
|
364
|
+
...continuationToken === void 0 || continuationToken.length === 0 ? {} : { nextCursor: continuationToken }
|
|
216
365
|
};
|
|
217
366
|
},
|
|
218
|
-
async
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
367
|
+
async listTextAnswerPage(formId, fieldIdOrOptions, providedOptions) {
|
|
368
|
+
const { query, fieldIds } = textAnswerQuery(fieldIdOrOptions, providedOptions);
|
|
369
|
+
const pageSize = (0, import_core.normalizeSubmissionPageSize)(query.pageSize);
|
|
370
|
+
const cursor = query.cursor === void 0 ? void 0 : decodeAzureTextAnswerCursor(query.cursor);
|
|
371
|
+
const client = await submissionClient(formId, query);
|
|
372
|
+
const items = [];
|
|
373
|
+
let tableContinuationToken = cursor?.tableContinuationToken ?? void 0;
|
|
374
|
+
let entityStartIndex = cursor?.entityIndex ?? 0;
|
|
375
|
+
let fieldStartIndex = cursor?.fieldIndex ?? 0;
|
|
376
|
+
let scannedPages = 0;
|
|
377
|
+
while (scannedPages < maxScanPages) {
|
|
378
|
+
const requestToken = tableContinuationToken;
|
|
379
|
+
const iterator = client.listEntities({ queryOptions: { filter: queryFilter(formId, query) } }).byPage({
|
|
380
|
+
maxPageSize: pageSize,
|
|
381
|
+
...requestToken === void 0 ? {} : { continuationToken: requestToken }
|
|
382
|
+
});
|
|
383
|
+
const result = await iterator.next();
|
|
384
|
+
if (result.done === true) break;
|
|
385
|
+
scannedPages += 1;
|
|
386
|
+
const page = result.value;
|
|
387
|
+
for (let entityIndex = entityStartIndex; entityIndex < page.length; entityIndex += 1) {
|
|
388
|
+
const raw = page[entityIndex];
|
|
389
|
+
if (raw === void 0) continue;
|
|
390
|
+
const submission = deserializeIfMatching(raw);
|
|
391
|
+
if (submission === void 0 || !matchesBuiltInFilters(submission, formId, query) || !(0, import_core.matchesSubmissionPageFilters)(submission, query)) {
|
|
392
|
+
fieldStartIndex = 0;
|
|
393
|
+
continue;
|
|
223
394
|
}
|
|
224
|
-
|
|
225
|
-
|
|
395
|
+
const answers = submissionTextAnswers(submission, fieldIds);
|
|
396
|
+
for (let fieldIndex = entityIndex === entityStartIndex ? fieldStartIndex : 0; fieldIndex < answers.length; fieldIndex += 1) {
|
|
397
|
+
const answer = answers[fieldIndex];
|
|
398
|
+
if (answer === void 0) continue;
|
|
399
|
+
items.push({
|
|
400
|
+
responseId: submission.id,
|
|
401
|
+
formId: submission.formId,
|
|
402
|
+
formVersion: submission.formVersion,
|
|
403
|
+
fieldId: answer.fieldId,
|
|
404
|
+
text: answer.text,
|
|
405
|
+
locale: submission.locale,
|
|
406
|
+
submittedAt: submission.submittedAt,
|
|
407
|
+
...submission.metadata === void 0 ? {} : { metadata: submission.metadata }
|
|
408
|
+
});
|
|
409
|
+
if (items.length < pageSize) continue;
|
|
410
|
+
const nextFieldIndex = fieldIndex + 1;
|
|
411
|
+
const hasFieldsInEntity = nextFieldIndex < answers.length;
|
|
412
|
+
const hasEntitiesInPage = entityIndex + 1 < page.length;
|
|
413
|
+
const nextPageToken = page.continuationToken;
|
|
414
|
+
const hasMore = hasFieldsInEntity || hasEntitiesInPage || nextPageToken !== void 0;
|
|
415
|
+
if (!hasMore) return { items, hasMore: false };
|
|
416
|
+
const nextCursor = hasFieldsInEntity ? {
|
|
417
|
+
tableContinuationToken: requestToken ?? null,
|
|
418
|
+
entityIndex,
|
|
419
|
+
fieldIndex: nextFieldIndex
|
|
420
|
+
} : hasEntitiesInPage ? {
|
|
421
|
+
tableContinuationToken: requestToken ?? null,
|
|
422
|
+
entityIndex: entityIndex + 1,
|
|
423
|
+
fieldIndex: 0
|
|
424
|
+
} : {
|
|
425
|
+
tableContinuationToken: nextPageToken ?? null,
|
|
426
|
+
entityIndex: 0,
|
|
427
|
+
fieldIndex: 0
|
|
428
|
+
};
|
|
429
|
+
return { items, hasMore: true, nextCursor: encodeAzureTextAnswerCursor(nextCursor) };
|
|
430
|
+
}
|
|
431
|
+
fieldStartIndex = 0;
|
|
432
|
+
}
|
|
433
|
+
tableContinuationToken = page.continuationToken;
|
|
434
|
+
entityStartIndex = 0;
|
|
435
|
+
fieldStartIndex = 0;
|
|
436
|
+
if (tableContinuationToken === void 0) break;
|
|
437
|
+
}
|
|
438
|
+
if (tableContinuationToken === void 0) return { items, hasMore: false };
|
|
439
|
+
return {
|
|
440
|
+
items,
|
|
441
|
+
hasMore: true,
|
|
442
|
+
nextCursor: encodeAzureTextAnswerCursor({
|
|
443
|
+
tableContinuationToken,
|
|
444
|
+
entityIndex: 0,
|
|
445
|
+
fieldIndex: 0
|
|
446
|
+
})
|
|
447
|
+
};
|
|
448
|
+
},
|
|
449
|
+
async deleteSubmission(submissionId) {
|
|
450
|
+
const client = await submissionClient("");
|
|
451
|
+
for await (const raw of client.listEntities()) {
|
|
452
|
+
const submission = deserializeIfMatching(raw);
|
|
453
|
+
if (submission?.id !== submissionId) continue;
|
|
454
|
+
if (typeof raw.partitionKey !== "string" || typeof raw.rowKey !== "string") {
|
|
455
|
+
throw new Error("Azure Table submission entity is invalid.");
|
|
226
456
|
}
|
|
457
|
+
await client.deleteEntity(raw.partitionKey, raw.rowKey);
|
|
458
|
+
return;
|
|
227
459
|
}
|
|
228
460
|
},
|
|
229
461
|
async clearResponses(formId) {
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (
|
|
235
|
-
|
|
462
|
+
const query = {};
|
|
463
|
+
const client = await submissionClient(formId, query);
|
|
464
|
+
for await (const raw of client.listEntities({ queryOptions: { filter: queryFilter(formId, query) } })) {
|
|
465
|
+
const submission = deserializeIfMatching(raw);
|
|
466
|
+
if (submission?.formId !== formId) continue;
|
|
467
|
+
if (typeof raw.partitionKey === "string" && typeof raw.rowKey === "string") {
|
|
468
|
+
await client.deleteEntity(raw.partitionKey, raw.rowKey);
|
|
236
469
|
}
|
|
237
470
|
}
|
|
238
471
|
},
|
|
239
472
|
async clear() {
|
|
473
|
+
const schemas = await schemaClient("");
|
|
240
474
|
for await (const raw of schemas.listEntities()) {
|
|
241
475
|
if (typeof raw.partitionKey === "string" && typeof raw.rowKey === "string") {
|
|
242
476
|
await schemas.deleteEntity(raw.partitionKey, raw.rowKey);
|
|
243
477
|
}
|
|
244
478
|
}
|
|
479
|
+
const submissions = await submissionClient("");
|
|
245
480
|
if (submissions === schemas) return;
|
|
246
481
|
for await (const raw of submissions.listEntities()) {
|
|
247
482
|
if (typeof raw.partitionKey === "string" && typeof raw.rowKey === "string") {
|
|
@@ -255,5 +490,6 @@ function createAzureTableStorage(options) {
|
|
|
255
490
|
0 && (module.exports = {
|
|
256
491
|
createAzureTableStorage,
|
|
257
492
|
defaultAzureTableSubmissionCodec,
|
|
258
|
-
metadataFiltersToOData
|
|
493
|
+
metadataFiltersToOData,
|
|
494
|
+
submissionFilterToOData
|
|
259
495
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FormSubmission, SubmissionPageQueryOptions, PagedSubmissionStorageAdapter } from '@form-engine-ts/core';
|
|
1
|
+
import { FormSubmission, SubmissionPageQueryOptions, PagedSubmissionStorageAdapter, SubmissionFilter } from '@form-engine-ts/core';
|
|
2
2
|
|
|
3
3
|
interface AzureTableListOptions {
|
|
4
4
|
readonly queryOptions?: {
|
|
@@ -22,6 +22,7 @@ interface AzureTableClientLike {
|
|
|
22
22
|
listEntities(options?: AzureTableListOptions): AzureTableEntityIterator;
|
|
23
23
|
deleteEntity(partitionKey: string, rowKey: string): Promise<unknown>;
|
|
24
24
|
}
|
|
25
|
+
/** @deprecated Use AzureTableSubmissionCodec. */
|
|
25
26
|
interface AzureTableEntityCodec<T> {
|
|
26
27
|
readonly createPartitionKey: (submission: T) => string;
|
|
27
28
|
readonly createPartitionKeyFromFormId?: (formId: string) => string;
|
|
@@ -29,16 +30,34 @@ interface AzureTableEntityCodec<T> {
|
|
|
29
30
|
readonly serialize: (submission: T) => Record<string, unknown>;
|
|
30
31
|
readonly deserialize: (entity: Record<string, unknown>) => T;
|
|
31
32
|
}
|
|
32
|
-
interface
|
|
33
|
-
|
|
33
|
+
interface AzureTableSubmissionCodec<T = FormSubmission> {
|
|
34
|
+
readonly createEntity: (value: T) => Record<string, unknown>;
|
|
35
|
+
readonly deserialize: (entity: Record<string, unknown>) => T;
|
|
36
|
+
readonly matchesEntity: (entity: Record<string, unknown>) => boolean;
|
|
37
|
+
readonly createPartitionKey: (value: T) => string;
|
|
38
|
+
readonly createPartitionKeyFromQuery: (formId: string, query: SubmissionPageQueryOptions) => string | undefined;
|
|
39
|
+
readonly createRowKey: (value: T) => string;
|
|
40
|
+
}
|
|
41
|
+
interface AzureTableStorageOptions<T = FormSubmission> {
|
|
42
|
+
/** @deprecated Use schemasTableClient, submissionsTableClient, or clientResolver. */
|
|
34
43
|
readonly client?: AzureTableClientLike;
|
|
35
44
|
readonly schemasTableClient?: AzureTableClientLike;
|
|
36
45
|
readonly submissionsTableClient?: AzureTableClientLike;
|
|
46
|
+
readonly clientResolver?: (context: {
|
|
47
|
+
readonly formId: string;
|
|
48
|
+
readonly query?: SubmissionPageQueryOptions;
|
|
49
|
+
}) => AzureTableClientLike | Promise<AzureTableClientLike>;
|
|
50
|
+
readonly codec?: AzureTableSubmissionCodec<T>;
|
|
51
|
+
/** @deprecated Use codec. */
|
|
37
52
|
readonly submissionCodec?: AzureTableEntityCodec<FormSubmission>;
|
|
53
|
+
readonly buildSubmissionFilter?: (formId: string, query: SubmissionPageQueryOptions) => string;
|
|
54
|
+
/** @deprecated Use buildSubmissionFilter. */
|
|
38
55
|
readonly toODataFilter?: (options: SubmissionPageQueryOptions) => string;
|
|
56
|
+
readonly maxScanPages?: number;
|
|
39
57
|
}
|
|
40
|
-
declare const defaultAzureTableSubmissionCodec:
|
|
58
|
+
declare const defaultAzureTableSubmissionCodec: AzureTableSubmissionCodec<FormSubmission>;
|
|
41
59
|
declare function metadataFiltersToOData(options: SubmissionPageQueryOptions): string;
|
|
42
|
-
declare function
|
|
60
|
+
declare function submissionFilterToOData(filter: SubmissionFilter): string | undefined;
|
|
61
|
+
declare function createAzureTableStorage(options?: AzureTableStorageOptions): PagedSubmissionStorageAdapter;
|
|
43
62
|
|
|
44
|
-
export { type AzureTableClientLike, type AzureTableEntityCodec, type AzureTableEntityIterator, type AzureTableEntityPage, type AzureTableListOptions, type AzureTablePageSettings, type AzureTableStorageOptions, createAzureTableStorage, defaultAzureTableSubmissionCodec, metadataFiltersToOData };
|
|
63
|
+
export { type AzureTableClientLike, type AzureTableEntityCodec, type AzureTableEntityIterator, type AzureTableEntityPage, type AzureTableListOptions, type AzureTablePageSettings, type AzureTableStorageOptions, type AzureTableSubmissionCodec, createAzureTableStorage, defaultAzureTableSubmissionCodec, metadataFiltersToOData, submissionFilterToOData };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FormSubmission, SubmissionPageQueryOptions, PagedSubmissionStorageAdapter } from '@form-engine-ts/core';
|
|
1
|
+
import { FormSubmission, SubmissionPageQueryOptions, PagedSubmissionStorageAdapter, SubmissionFilter } from '@form-engine-ts/core';
|
|
2
2
|
|
|
3
3
|
interface AzureTableListOptions {
|
|
4
4
|
readonly queryOptions?: {
|
|
@@ -22,6 +22,7 @@ interface AzureTableClientLike {
|
|
|
22
22
|
listEntities(options?: AzureTableListOptions): AzureTableEntityIterator;
|
|
23
23
|
deleteEntity(partitionKey: string, rowKey: string): Promise<unknown>;
|
|
24
24
|
}
|
|
25
|
+
/** @deprecated Use AzureTableSubmissionCodec. */
|
|
25
26
|
interface AzureTableEntityCodec<T> {
|
|
26
27
|
readonly createPartitionKey: (submission: T) => string;
|
|
27
28
|
readonly createPartitionKeyFromFormId?: (formId: string) => string;
|
|
@@ -29,16 +30,34 @@ interface AzureTableEntityCodec<T> {
|
|
|
29
30
|
readonly serialize: (submission: T) => Record<string, unknown>;
|
|
30
31
|
readonly deserialize: (entity: Record<string, unknown>) => T;
|
|
31
32
|
}
|
|
32
|
-
interface
|
|
33
|
-
|
|
33
|
+
interface AzureTableSubmissionCodec<T = FormSubmission> {
|
|
34
|
+
readonly createEntity: (value: T) => Record<string, unknown>;
|
|
35
|
+
readonly deserialize: (entity: Record<string, unknown>) => T;
|
|
36
|
+
readonly matchesEntity: (entity: Record<string, unknown>) => boolean;
|
|
37
|
+
readonly createPartitionKey: (value: T) => string;
|
|
38
|
+
readonly createPartitionKeyFromQuery: (formId: string, query: SubmissionPageQueryOptions) => string | undefined;
|
|
39
|
+
readonly createRowKey: (value: T) => string;
|
|
40
|
+
}
|
|
41
|
+
interface AzureTableStorageOptions<T = FormSubmission> {
|
|
42
|
+
/** @deprecated Use schemasTableClient, submissionsTableClient, or clientResolver. */
|
|
34
43
|
readonly client?: AzureTableClientLike;
|
|
35
44
|
readonly schemasTableClient?: AzureTableClientLike;
|
|
36
45
|
readonly submissionsTableClient?: AzureTableClientLike;
|
|
46
|
+
readonly clientResolver?: (context: {
|
|
47
|
+
readonly formId: string;
|
|
48
|
+
readonly query?: SubmissionPageQueryOptions;
|
|
49
|
+
}) => AzureTableClientLike | Promise<AzureTableClientLike>;
|
|
50
|
+
readonly codec?: AzureTableSubmissionCodec<T>;
|
|
51
|
+
/** @deprecated Use codec. */
|
|
37
52
|
readonly submissionCodec?: AzureTableEntityCodec<FormSubmission>;
|
|
53
|
+
readonly buildSubmissionFilter?: (formId: string, query: SubmissionPageQueryOptions) => string;
|
|
54
|
+
/** @deprecated Use buildSubmissionFilter. */
|
|
38
55
|
readonly toODataFilter?: (options: SubmissionPageQueryOptions) => string;
|
|
56
|
+
readonly maxScanPages?: number;
|
|
39
57
|
}
|
|
40
|
-
declare const defaultAzureTableSubmissionCodec:
|
|
58
|
+
declare const defaultAzureTableSubmissionCodec: AzureTableSubmissionCodec<FormSubmission>;
|
|
41
59
|
declare function metadataFiltersToOData(options: SubmissionPageQueryOptions): string;
|
|
42
|
-
declare function
|
|
60
|
+
declare function submissionFilterToOData(filter: SubmissionFilter): string | undefined;
|
|
61
|
+
declare function createAzureTableStorage(options?: AzureTableStorageOptions): PagedSubmissionStorageAdapter;
|
|
43
62
|
|
|
44
|
-
export { type AzureTableClientLike, type AzureTableEntityCodec, type AzureTableEntityIterator, type AzureTableEntityPage, type AzureTableListOptions, type AzureTablePageSettings, type AzureTableStorageOptions, createAzureTableStorage, defaultAzureTableSubmissionCodec, metadataFiltersToOData };
|
|
63
|
+
export { type AzureTableClientLike, type AzureTableEntityCodec, type AzureTableEntityIterator, type AzureTableEntityPage, type AzureTableListOptions, type AzureTablePageSettings, type AzureTableStorageOptions, type AzureTableSubmissionCodec, createAzureTableStorage, defaultAzureTableSubmissionCodec, metadataFiltersToOData, submissionFilterToOData };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { assertValidFormSchema, matchesSubmissionPageFilters, normalizeSubmissionPageSize } from "@form-engine-ts/core";
|
|
3
|
+
var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
4
|
+
function encodeBase64(bytes) {
|
|
5
|
+
let result = "";
|
|
6
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
7
|
+
const first = bytes[index] ?? 0;
|
|
8
|
+
const second = bytes[index + 1] ?? 0;
|
|
9
|
+
const third = bytes[index + 2] ?? 0;
|
|
10
|
+
const combined = first << 16 | second << 8 | third;
|
|
11
|
+
result += BASE64_ALPHABET[combined >> 18 & 63] ?? "";
|
|
12
|
+
result += BASE64_ALPHABET[combined >> 12 & 63] ?? "";
|
|
13
|
+
result += index + 1 < bytes.length ? BASE64_ALPHABET[combined >> 6 & 63] ?? "" : "=";
|
|
14
|
+
result += index + 2 < bytes.length ? BASE64_ALPHABET[combined & 63] ?? "" : "=";
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
function decodeBase64(value) {
|
|
19
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
|
|
20
|
+
throw new TypeError("cursor must be a valid Base64 token.");
|
|
21
|
+
}
|
|
22
|
+
const bytes = [];
|
|
23
|
+
for (let index = 0; index < value.length; index += 4) {
|
|
24
|
+
const characters = value.slice(index, index + 4);
|
|
25
|
+
const sextets = [...characters].map((character) => character === "=" ? 0 : BASE64_ALPHABET.indexOf(character));
|
|
26
|
+
const combined = (sextets[0] ?? 0) << 18 | (sextets[1] ?? 0) << 12 | (sextets[2] ?? 0) << 6 | (sextets[3] ?? 0);
|
|
27
|
+
bytes.push(combined >> 16 & 255);
|
|
28
|
+
if (characters[2] !== "=") bytes.push(combined >> 8 & 255);
|
|
29
|
+
if (characters[3] !== "=") bytes.push(combined & 255);
|
|
30
|
+
}
|
|
31
|
+
return new Uint8Array(bytes);
|
|
32
|
+
}
|
|
33
|
+
function encodeAzureTextAnswerCursor(value) {
|
|
34
|
+
return encodeBase64(new TextEncoder().encode(JSON.stringify(value)));
|
|
35
|
+
}
|
|
36
|
+
function decodeAzureTextAnswerCursor(cursor) {
|
|
37
|
+
try {
|
|
38
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(decodeBase64(cursor)));
|
|
39
|
+
if (!isRecord(value) || value.tableContinuationToken !== null && typeof value.tableContinuationToken !== "string" || !Number.isSafeInteger(value.entityIndex) || value.entityIndex < 0 || !Number.isSafeInteger(value.fieldIndex) || value.fieldIndex < 0) {
|
|
40
|
+
throw new TypeError("Azure text answer cursor payload is invalid.");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
tableContinuationToken: value.tableContinuationToken,
|
|
44
|
+
entityIndex: value.entityIndex,
|
|
45
|
+
fieldIndex: value.fieldIndex
|
|
46
|
+
};
|
|
47
|
+
} catch (cause) {
|
|
48
|
+
if (cause instanceof TypeError && cause.message === "Azure text answer cursor payload is invalid.") throw cause;
|
|
49
|
+
throw new TypeError("cursor must be a valid Azure text answer cursor.", { cause });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function textAnswerQuery(fieldIdOrOptions, options) {
|
|
53
|
+
const query = typeof fieldIdOrOptions === "string" ? options ?? {} : fieldIdOrOptions ?? {};
|
|
54
|
+
const requested = typeof fieldIdOrOptions === "string" ? [fieldIdOrOptions] : query.fieldIds;
|
|
55
|
+
if (requested?.some((fieldId) => fieldId.trim().length === 0)) {
|
|
56
|
+
throw new TypeError("fieldIds must not contain empty values.");
|
|
57
|
+
}
|
|
58
|
+
const fieldIds = requested === void 0 ? void 0 : [...new Set(requested)];
|
|
59
|
+
return { query, ...fieldIds === void 0 ? {} : { fieldIds } };
|
|
60
|
+
}
|
|
61
|
+
function submissionTextAnswers(submission, fieldIds) {
|
|
62
|
+
const entries = fieldIds === void 0 ? Object.entries(submission.values) : fieldIds.map((id) => [id, submission.values[id]]);
|
|
63
|
+
return entries.flatMap(
|
|
64
|
+
([fieldId, value]) => typeof value === "string" && value.length > 0 ? [{ fieldId, text: value }] : []
|
|
65
|
+
);
|
|
66
|
+
}
|
|
3
67
|
function cloneJson(value) {
|
|
4
68
|
return JSON.parse(JSON.stringify(value));
|
|
5
69
|
}
|
|
@@ -40,12 +104,38 @@ function scalarMetadata(metadata) {
|
|
|
40
104
|
);
|
|
41
105
|
}
|
|
42
106
|
var defaultAzureTableSubmissionCodec = {
|
|
107
|
+
createEntity: (submission) => ({
|
|
108
|
+
...scalarMetadata(submission.metadata),
|
|
109
|
+
kind: "submission",
|
|
110
|
+
formVersion: submission.formVersion,
|
|
111
|
+
locale: submission.locale,
|
|
112
|
+
submittedAt: submission.submittedAt,
|
|
113
|
+
responseId: submission.id,
|
|
114
|
+
payload: JSON.stringify(submission)
|
|
115
|
+
}),
|
|
116
|
+
deserialize: (entity) => parseSubmission(entity.payload, "submission entity"),
|
|
117
|
+
matchesEntity: (entity) => entity.kind === "submission",
|
|
43
118
|
createPartitionKey: (submission) => submission.formId,
|
|
44
|
-
|
|
45
|
-
createRowKey: defaultSubmissionRowKey
|
|
46
|
-
serialize: (submission) => ({ ...scalarMetadata(submission.metadata), payload: JSON.stringify(submission) }),
|
|
47
|
-
deserialize: (entity) => parseSubmission(entity.payload, "submission entity")
|
|
119
|
+
createPartitionKeyFromQuery: (formId) => formId,
|
|
120
|
+
createRowKey: defaultSubmissionRowKey
|
|
48
121
|
};
|
|
122
|
+
function legacyCodec(codec) {
|
|
123
|
+
return {
|
|
124
|
+
createEntity: (value) => ({
|
|
125
|
+
...codec.serialize(value),
|
|
126
|
+
kind: "submission",
|
|
127
|
+
formVersion: value.formVersion,
|
|
128
|
+
locale: value.locale,
|
|
129
|
+
submittedAt: value.submittedAt,
|
|
130
|
+
responseId: value.id
|
|
131
|
+
}),
|
|
132
|
+
deserialize: codec.deserialize,
|
|
133
|
+
matchesEntity: (entity) => entity.kind === "submission",
|
|
134
|
+
createPartitionKey: codec.createPartitionKey,
|
|
135
|
+
createPartitionKeyFromQuery: (formId) => codec.createPartitionKeyFromFormId?.(formId) ?? formId,
|
|
136
|
+
createRowKey: codec.createRowKey
|
|
137
|
+
};
|
|
138
|
+
}
|
|
49
139
|
function parseSchemaEntity(value) {
|
|
50
140
|
if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || value.kind !== "schema" || !Number.isInteger(value.formVersion) || typeof value.payload !== "string") {
|
|
51
141
|
throw new Error("Azure Table schema entity is invalid.");
|
|
@@ -58,40 +148,73 @@ function parseSchemaEntity(value) {
|
|
|
58
148
|
return cloneJson(schema);
|
|
59
149
|
}
|
|
60
150
|
function parseSubmissionEntity(value, codec) {
|
|
61
|
-
if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || value
|
|
151
|
+
if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || !codec.matchesEntity(value)) {
|
|
62
152
|
throw new Error("Azure Table submission entity is invalid.");
|
|
63
153
|
}
|
|
64
154
|
const submission = parseSubmission(codec.deserialize(value), `submission ${value.partitionKey}/${value.rowKey}`);
|
|
65
|
-
if (codec.createPartitionKey(submission) !== value.partitionKey || codec.createRowKey(submission) !== value.rowKey
|
|
66
|
-
throw new Error("Azure Table submission entity has inconsistent
|
|
155
|
+
if (codec.createPartitionKey(submission) !== value.partitionKey || codec.createRowKey(submission) !== value.rowKey) {
|
|
156
|
+
throw new Error("Azure Table submission entity has inconsistent keys.");
|
|
67
157
|
}
|
|
68
158
|
return submission;
|
|
69
159
|
}
|
|
70
160
|
function escapeOData(value) {
|
|
71
161
|
return value.replaceAll("'", "''");
|
|
72
162
|
}
|
|
73
|
-
function
|
|
163
|
+
function valueToOData(value) {
|
|
74
164
|
if (value === null) return "null";
|
|
75
165
|
if (typeof value === "string") return `'${escapeOData(value)}'`;
|
|
76
166
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
77
167
|
if (typeof value === "boolean") return String(value);
|
|
78
|
-
throw new TypeError("Azure Table
|
|
168
|
+
throw new TypeError("Azure Table OData filters support only scalar JSON values.");
|
|
79
169
|
}
|
|
80
170
|
function metadataFiltersToOData(options) {
|
|
81
171
|
return Object.entries(options.metadataFilters ?? {}).map(([key, value]) => {
|
|
82
172
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new TypeError(`Invalid Azure Table property name: ${key}`);
|
|
83
|
-
return `${key} eq ${
|
|
173
|
+
return `${key} eq ${valueToOData(value)}`;
|
|
84
174
|
}).join(" and ");
|
|
85
175
|
}
|
|
86
|
-
function
|
|
176
|
+
function odataProperty(path) {
|
|
177
|
+
if (path === "id" || path === "responseId") return "responseId";
|
|
178
|
+
if (["formVersion", "locale", "submittedAt"].includes(path)) return path;
|
|
179
|
+
if (path.startsWith("metadata.")) {
|
|
180
|
+
const property = path.slice("metadata.".length);
|
|
181
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(property) ? property : void 0;
|
|
182
|
+
}
|
|
183
|
+
return void 0;
|
|
184
|
+
}
|
|
185
|
+
function submissionFilterToOData(filter) {
|
|
186
|
+
if (filter.op === "and" || filter.op === "or") {
|
|
187
|
+
const converted = filter.filters.map(submissionFilterToOData);
|
|
188
|
+
if (filter.op === "or" && converted.some((value) => value === void 0)) return void 0;
|
|
189
|
+
const available = converted.filter((value) => value !== void 0 && value.length > 0);
|
|
190
|
+
if (available.length === 0) return void 0;
|
|
191
|
+
return available.map((value) => `(${value})`).join(filter.op === "and" ? " and " : " or ");
|
|
192
|
+
}
|
|
193
|
+
const property = odataProperty(filter.path);
|
|
194
|
+
if (property === void 0) return void 0;
|
|
195
|
+
if (filter.op === "eq") return `${property} eq ${valueToOData(filter.value)}`;
|
|
196
|
+
if (filter.op === "in") {
|
|
197
|
+
if (filter.values.length === 0) return "false";
|
|
198
|
+
return filter.values.map((value) => `${property} eq ${valueToOData(value)}`).join(" or ");
|
|
199
|
+
}
|
|
200
|
+
if (filter.op === "exists") return `${property} ${filter.value ? "ne" : "eq"} null`;
|
|
87
201
|
return [
|
|
88
|
-
...
|
|
89
|
-
|
|
202
|
+
...filter.from === void 0 ? [] : [`${property} ge ${valueToOData(filter.from)}`],
|
|
203
|
+
...filter.to === void 0 ? [] : [`${property} le ${valueToOData(filter.to)}`]
|
|
204
|
+
].join(" and ");
|
|
205
|
+
}
|
|
206
|
+
function defaultSubmissionFilter(codec, formId, options, legacyExtension) {
|
|
207
|
+
const partitionKey = codec.createPartitionKeyFromQuery(formId, options);
|
|
208
|
+
const ast = options.filter === void 0 || typeof options.filter === "function" ? void 0 : submissionFilterToOData(options.filter);
|
|
209
|
+
return [
|
|
210
|
+
...partitionKey === void 0 ? [] : [`PartitionKey eq '${escapeOData(partitionKey)}'`],
|
|
211
|
+
...codec === defaultAzureTableSubmissionCodec ? ["kind eq 'submission'"] : [],
|
|
90
212
|
...options.version === void 0 ? [] : [`formVersion eq ${options.version}`],
|
|
91
213
|
...options.since === void 0 ? [] : [`submittedAt ge '${escapeOData(options.since)}'`],
|
|
92
214
|
...options.until === void 0 ? [] : [`submittedAt le '${escapeOData(options.until)}'`],
|
|
93
215
|
...options.locale === void 0 ? [] : [`locale eq '${escapeOData(options.locale)}'`],
|
|
94
|
-
...
|
|
216
|
+
...ast === void 0 || ast.length === 0 ? [] : [`(${ast})`],
|
|
217
|
+
...legacyExtension.trim().length === 0 ? [] : [`(${legacyExtension})`]
|
|
95
218
|
].join(" and ");
|
|
96
219
|
}
|
|
97
220
|
function isNotFound(error) {
|
|
@@ -100,26 +223,41 @@ function isNotFound(error) {
|
|
|
100
223
|
function matchesBuiltInFilters(submission, formId, options) {
|
|
101
224
|
return submission.formId === formId && (options.version === void 0 || submission.formVersion === options.version) && (options.since === void 0 || submission.submittedAt >= options.since) && (options.until === void 0 || submission.submittedAt <= options.until) && (options.locale === void 0 || submission.locale === options.locale);
|
|
102
225
|
}
|
|
103
|
-
function
|
|
104
|
-
const client = preferred ?? fallback;
|
|
226
|
+
function requireClient(client, name) {
|
|
105
227
|
if (client === void 0) throw new TypeError(`${name} is required.`);
|
|
106
228
|
return client;
|
|
107
229
|
}
|
|
108
|
-
function createAzureTableStorage(options) {
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
const codec = options.
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
230
|
+
function createAzureTableStorage(options = {}) {
|
|
231
|
+
const staticSchemas = options.schemasTableClient ?? options.client;
|
|
232
|
+
const staticSubmissions = options.submissionsTableClient ?? options.client;
|
|
233
|
+
const codec = options.codec ?? (options.submissionCodec === void 0 ? defaultAzureTableSubmissionCodec : legacyCodec(options.submissionCodec));
|
|
234
|
+
const maxScanPages = options.maxScanPages ?? 5;
|
|
235
|
+
if (!Number.isSafeInteger(maxScanPages) || maxScanPages < 1) {
|
|
236
|
+
throw new TypeError("maxScanPages must be a positive safe integer.");
|
|
237
|
+
}
|
|
238
|
+
const resolveDynamicClient = async (formId, query) => options.clientResolver?.({ formId, ...query === void 0 ? {} : { query } });
|
|
239
|
+
const schemaClient = async (formId) => requireClient(staticSchemas ?? await resolveDynamicClient(formId), "schemasTableClient or clientResolver");
|
|
240
|
+
const submissionClient = async (formId, query) => requireClient(
|
|
241
|
+
await resolveDynamicClient(formId, query) ?? staticSubmissions,
|
|
242
|
+
"submissionsTableClient or clientResolver"
|
|
243
|
+
);
|
|
244
|
+
const queryFilter = (formId, query) => {
|
|
245
|
+
if (options.buildSubmissionFilter !== void 0) return options.buildSubmissionFilter(formId, query);
|
|
246
|
+
return defaultSubmissionFilter(
|
|
247
|
+
codec,
|
|
248
|
+
formId,
|
|
249
|
+
query,
|
|
250
|
+
options.toODataFilter?.(query) ?? metadataFiltersToOData(query)
|
|
251
|
+
);
|
|
252
|
+
};
|
|
253
|
+
const deserializeIfMatching = (entity) => codec.matchesEntity(entity) ? parseSubmissionEntity(entity, codec) : void 0;
|
|
115
254
|
const listSubmissionCandidates = async (formId, query) => {
|
|
255
|
+
const client = await submissionClient(formId, query);
|
|
116
256
|
const found = [];
|
|
117
|
-
for await (const raw of
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
if (!matchesSubmissionPageFilters(submission, query)) continue;
|
|
122
|
-
found.push(submission);
|
|
257
|
+
for await (const raw of client.listEntities({ queryOptions: { filter: queryFilter(formId, query) } })) {
|
|
258
|
+
const submission = deserializeIfMatching(raw);
|
|
259
|
+
if (submission === void 0 || !matchesBuiltInFilters(submission, formId, query)) continue;
|
|
260
|
+
if (matchesSubmissionPageFilters(submission, query)) found.push(submission);
|
|
123
261
|
}
|
|
124
262
|
return found.sort(
|
|
125
263
|
(left, right) => left.submittedAt.localeCompare(right.submittedAt) || left.id.localeCompare(right.id)
|
|
@@ -135,11 +273,11 @@ function createAzureTableStorage(options) {
|
|
|
135
273
|
formVersion: schema.version,
|
|
136
274
|
payload: JSON.stringify(schema)
|
|
137
275
|
};
|
|
138
|
-
await
|
|
276
|
+
await (await schemaClient(schema.id)).upsertEntity(entity, "Replace");
|
|
139
277
|
},
|
|
140
278
|
async getSchema(formId, formVersion) {
|
|
141
279
|
try {
|
|
142
|
-
return parseSchemaEntity(await
|
|
280
|
+
return parseSchemaEntity(await (await schemaClient(formId)).getEntity(formId, schemaRowKey(formVersion)));
|
|
143
281
|
} catch (error) {
|
|
144
282
|
if (isNotFound(error)) return null;
|
|
145
283
|
throw error;
|
|
@@ -147,25 +285,20 @@ function createAzureTableStorage(options) {
|
|
|
147
285
|
},
|
|
148
286
|
async listSchemas() {
|
|
149
287
|
const found = [];
|
|
150
|
-
for await (const raw of
|
|
288
|
+
for await (const raw of (await schemaClient("")).listEntities({ queryOptions: { filter: "kind eq 'schema'" } })) {
|
|
151
289
|
if (raw.kind === "schema") found.push(parseSchemaEntity(raw));
|
|
152
290
|
}
|
|
153
291
|
return found.sort((left, right) => left.id.localeCompare(right.id) || left.version - right.version);
|
|
154
292
|
},
|
|
155
293
|
async deleteSchema(formId, formVersion) {
|
|
156
|
-
await
|
|
294
|
+
await (await schemaClient(formId)).deleteEntity(formId, schemaRowKey(formVersion));
|
|
157
295
|
},
|
|
158
296
|
async saveSubmission(submission) {
|
|
159
297
|
const stored = parseSubmission(submission, `input ${String(submission?.id)}`);
|
|
160
|
-
await
|
|
161
|
-
...codec.
|
|
298
|
+
await (await submissionClient(stored.formId)).createEntity({
|
|
299
|
+
...codec.createEntity(stored),
|
|
162
300
|
partitionKey: codec.createPartitionKey(stored),
|
|
163
|
-
rowKey: codec.createRowKey(stored)
|
|
164
|
-
kind: "submission",
|
|
165
|
-
formVersion: stored.formVersion,
|
|
166
|
-
locale: stored.locale,
|
|
167
|
-
submittedAt: stored.submittedAt,
|
|
168
|
-
responseId: stored.id
|
|
301
|
+
rowKey: codec.createRowKey(stored)
|
|
169
302
|
});
|
|
170
303
|
},
|
|
171
304
|
async listSubmissions(formId, formVersion, queryOptions = {}) {
|
|
@@ -176,46 +309,147 @@ function createAzureTableStorage(options) {
|
|
|
176
309
|
},
|
|
177
310
|
async listSubmissionPage(formId, query = {}) {
|
|
178
311
|
const pageSize = normalizeSubmissionPageSize(query.pageSize);
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
312
|
+
const client = await submissionClient(formId, query);
|
|
313
|
+
const items = [];
|
|
314
|
+
let continuationToken = query.cursor;
|
|
315
|
+
let scannedPages = 0;
|
|
316
|
+
do {
|
|
317
|
+
const iterator = client.listEntities({ queryOptions: { filter: queryFilter(formId, query) } }).byPage({
|
|
318
|
+
maxPageSize: Math.max(1, pageSize - items.length),
|
|
319
|
+
...continuationToken === void 0 ? {} : { continuationToken }
|
|
320
|
+
});
|
|
321
|
+
const result = await iterator.next();
|
|
322
|
+
if (result.done === true) {
|
|
323
|
+
continuationToken = void 0;
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
scannedPages += 1;
|
|
327
|
+
for (const raw of result.value) {
|
|
328
|
+
const submission = deserializeIfMatching(raw);
|
|
329
|
+
if (submission === void 0 || !matchesBuiltInFilters(submission, formId, query)) continue;
|
|
330
|
+
if (matchesSubmissionPageFilters(submission, query)) items.push(submission);
|
|
331
|
+
}
|
|
332
|
+
continuationToken = result.value.continuationToken;
|
|
333
|
+
} while (items.length < pageSize && continuationToken !== void 0 && scannedPages < maxScanPages);
|
|
186
334
|
return {
|
|
187
335
|
items,
|
|
188
|
-
hasMore:
|
|
189
|
-
...
|
|
336
|
+
hasMore: continuationToken !== void 0 && continuationToken.length > 0,
|
|
337
|
+
...continuationToken === void 0 || continuationToken.length === 0 ? {} : { nextCursor: continuationToken }
|
|
190
338
|
};
|
|
191
339
|
},
|
|
192
|
-
async
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
340
|
+
async listTextAnswerPage(formId, fieldIdOrOptions, providedOptions) {
|
|
341
|
+
const { query, fieldIds } = textAnswerQuery(fieldIdOrOptions, providedOptions);
|
|
342
|
+
const pageSize = normalizeSubmissionPageSize(query.pageSize);
|
|
343
|
+
const cursor = query.cursor === void 0 ? void 0 : decodeAzureTextAnswerCursor(query.cursor);
|
|
344
|
+
const client = await submissionClient(formId, query);
|
|
345
|
+
const items = [];
|
|
346
|
+
let tableContinuationToken = cursor?.tableContinuationToken ?? void 0;
|
|
347
|
+
let entityStartIndex = cursor?.entityIndex ?? 0;
|
|
348
|
+
let fieldStartIndex = cursor?.fieldIndex ?? 0;
|
|
349
|
+
let scannedPages = 0;
|
|
350
|
+
while (scannedPages < maxScanPages) {
|
|
351
|
+
const requestToken = tableContinuationToken;
|
|
352
|
+
const iterator = client.listEntities({ queryOptions: { filter: queryFilter(formId, query) } }).byPage({
|
|
353
|
+
maxPageSize: pageSize,
|
|
354
|
+
...requestToken === void 0 ? {} : { continuationToken: requestToken }
|
|
355
|
+
});
|
|
356
|
+
const result = await iterator.next();
|
|
357
|
+
if (result.done === true) break;
|
|
358
|
+
scannedPages += 1;
|
|
359
|
+
const page = result.value;
|
|
360
|
+
for (let entityIndex = entityStartIndex; entityIndex < page.length; entityIndex += 1) {
|
|
361
|
+
const raw = page[entityIndex];
|
|
362
|
+
if (raw === void 0) continue;
|
|
363
|
+
const submission = deserializeIfMatching(raw);
|
|
364
|
+
if (submission === void 0 || !matchesBuiltInFilters(submission, formId, query) || !matchesSubmissionPageFilters(submission, query)) {
|
|
365
|
+
fieldStartIndex = 0;
|
|
366
|
+
continue;
|
|
197
367
|
}
|
|
198
|
-
|
|
199
|
-
|
|
368
|
+
const answers = submissionTextAnswers(submission, fieldIds);
|
|
369
|
+
for (let fieldIndex = entityIndex === entityStartIndex ? fieldStartIndex : 0; fieldIndex < answers.length; fieldIndex += 1) {
|
|
370
|
+
const answer = answers[fieldIndex];
|
|
371
|
+
if (answer === void 0) continue;
|
|
372
|
+
items.push({
|
|
373
|
+
responseId: submission.id,
|
|
374
|
+
formId: submission.formId,
|
|
375
|
+
formVersion: submission.formVersion,
|
|
376
|
+
fieldId: answer.fieldId,
|
|
377
|
+
text: answer.text,
|
|
378
|
+
locale: submission.locale,
|
|
379
|
+
submittedAt: submission.submittedAt,
|
|
380
|
+
...submission.metadata === void 0 ? {} : { metadata: submission.metadata }
|
|
381
|
+
});
|
|
382
|
+
if (items.length < pageSize) continue;
|
|
383
|
+
const nextFieldIndex = fieldIndex + 1;
|
|
384
|
+
const hasFieldsInEntity = nextFieldIndex < answers.length;
|
|
385
|
+
const hasEntitiesInPage = entityIndex + 1 < page.length;
|
|
386
|
+
const nextPageToken = page.continuationToken;
|
|
387
|
+
const hasMore = hasFieldsInEntity || hasEntitiesInPage || nextPageToken !== void 0;
|
|
388
|
+
if (!hasMore) return { items, hasMore: false };
|
|
389
|
+
const nextCursor = hasFieldsInEntity ? {
|
|
390
|
+
tableContinuationToken: requestToken ?? null,
|
|
391
|
+
entityIndex,
|
|
392
|
+
fieldIndex: nextFieldIndex
|
|
393
|
+
} : hasEntitiesInPage ? {
|
|
394
|
+
tableContinuationToken: requestToken ?? null,
|
|
395
|
+
entityIndex: entityIndex + 1,
|
|
396
|
+
fieldIndex: 0
|
|
397
|
+
} : {
|
|
398
|
+
tableContinuationToken: nextPageToken ?? null,
|
|
399
|
+
entityIndex: 0,
|
|
400
|
+
fieldIndex: 0
|
|
401
|
+
};
|
|
402
|
+
return { items, hasMore: true, nextCursor: encodeAzureTextAnswerCursor(nextCursor) };
|
|
403
|
+
}
|
|
404
|
+
fieldStartIndex = 0;
|
|
405
|
+
}
|
|
406
|
+
tableContinuationToken = page.continuationToken;
|
|
407
|
+
entityStartIndex = 0;
|
|
408
|
+
fieldStartIndex = 0;
|
|
409
|
+
if (tableContinuationToken === void 0) break;
|
|
410
|
+
}
|
|
411
|
+
if (tableContinuationToken === void 0) return { items, hasMore: false };
|
|
412
|
+
return {
|
|
413
|
+
items,
|
|
414
|
+
hasMore: true,
|
|
415
|
+
nextCursor: encodeAzureTextAnswerCursor({
|
|
416
|
+
tableContinuationToken,
|
|
417
|
+
entityIndex: 0,
|
|
418
|
+
fieldIndex: 0
|
|
419
|
+
})
|
|
420
|
+
};
|
|
421
|
+
},
|
|
422
|
+
async deleteSubmission(submissionId) {
|
|
423
|
+
const client = await submissionClient("");
|
|
424
|
+
for await (const raw of client.listEntities()) {
|
|
425
|
+
const submission = deserializeIfMatching(raw);
|
|
426
|
+
if (submission?.id !== submissionId) continue;
|
|
427
|
+
if (typeof raw.partitionKey !== "string" || typeof raw.rowKey !== "string") {
|
|
428
|
+
throw new Error("Azure Table submission entity is invalid.");
|
|
200
429
|
}
|
|
430
|
+
await client.deleteEntity(raw.partitionKey, raw.rowKey);
|
|
431
|
+
return;
|
|
201
432
|
}
|
|
202
433
|
},
|
|
203
434
|
async clearResponses(formId) {
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
|
|
435
|
+
const query = {};
|
|
436
|
+
const client = await submissionClient(formId, query);
|
|
437
|
+
for await (const raw of client.listEntities({ queryOptions: { filter: queryFilter(formId, query) } })) {
|
|
438
|
+
const submission = deserializeIfMatching(raw);
|
|
439
|
+
if (submission?.formId !== formId) continue;
|
|
440
|
+
if (typeof raw.partitionKey === "string" && typeof raw.rowKey === "string") {
|
|
441
|
+
await client.deleteEntity(raw.partitionKey, raw.rowKey);
|
|
210
442
|
}
|
|
211
443
|
}
|
|
212
444
|
},
|
|
213
445
|
async clear() {
|
|
446
|
+
const schemas = await schemaClient("");
|
|
214
447
|
for await (const raw of schemas.listEntities()) {
|
|
215
448
|
if (typeof raw.partitionKey === "string" && typeof raw.rowKey === "string") {
|
|
216
449
|
await schemas.deleteEntity(raw.partitionKey, raw.rowKey);
|
|
217
450
|
}
|
|
218
451
|
}
|
|
452
|
+
const submissions = await submissionClient("");
|
|
219
453
|
if (submissions === schemas) return;
|
|
220
454
|
for await (const raw of submissions.listEntities()) {
|
|
221
455
|
if (typeof raw.partitionKey === "string" && typeof raw.rowKey === "string") {
|
|
@@ -228,5 +462,6 @@ function createAzureTableStorage(options) {
|
|
|
228
462
|
export {
|
|
229
463
|
createAzureTableStorage,
|
|
230
464
|
defaultAzureTableSubmissionCodec,
|
|
231
|
-
metadataFiltersToOData
|
|
465
|
+
metadataFiltersToOData,
|
|
466
|
+
submissionFilterToOData
|
|
232
467
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@form-engine-ts/storage-azure-table",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.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": "2.
|
|
42
|
+
"@form-engine-ts/core": "2.8.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@azure/data-tables": "^13.3.2"
|