@form-engine-ts/storage-azure-table 2.6.0 → 2.7.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 +10 -6
- package/dist/index.cjs +155 -65
- package/dist/index.d.cts +25 -6
- package/dist/index.d.ts +25 -6
- package/dist/index.js +153 -64
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -25,9 +25,13 @@ 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.
|
package/dist/index.cjs
CHANGED
|
@@ -22,7 +22,8 @@ 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");
|
|
@@ -66,12 +67,38 @@ function scalarMetadata(metadata) {
|
|
|
66
67
|
);
|
|
67
68
|
}
|
|
68
69
|
var defaultAzureTableSubmissionCodec = {
|
|
70
|
+
createEntity: (submission) => ({
|
|
71
|
+
...scalarMetadata(submission.metadata),
|
|
72
|
+
kind: "submission",
|
|
73
|
+
formVersion: submission.formVersion,
|
|
74
|
+
locale: submission.locale,
|
|
75
|
+
submittedAt: submission.submittedAt,
|
|
76
|
+
responseId: submission.id,
|
|
77
|
+
payload: JSON.stringify(submission)
|
|
78
|
+
}),
|
|
79
|
+
deserialize: (entity) => parseSubmission(entity.payload, "submission entity"),
|
|
80
|
+
matchesEntity: (entity) => entity.kind === "submission",
|
|
69
81
|
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")
|
|
82
|
+
createPartitionKeyFromQuery: (formId) => formId,
|
|
83
|
+
createRowKey: defaultSubmissionRowKey
|
|
74
84
|
};
|
|
85
|
+
function legacyCodec(codec) {
|
|
86
|
+
return {
|
|
87
|
+
createEntity: (value) => ({
|
|
88
|
+
...codec.serialize(value),
|
|
89
|
+
kind: "submission",
|
|
90
|
+
formVersion: value.formVersion,
|
|
91
|
+
locale: value.locale,
|
|
92
|
+
submittedAt: value.submittedAt,
|
|
93
|
+
responseId: value.id
|
|
94
|
+
}),
|
|
95
|
+
deserialize: codec.deserialize,
|
|
96
|
+
matchesEntity: (entity) => entity.kind === "submission",
|
|
97
|
+
createPartitionKey: codec.createPartitionKey,
|
|
98
|
+
createPartitionKeyFromQuery: (formId) => codec.createPartitionKeyFromFormId?.(formId) ?? formId,
|
|
99
|
+
createRowKey: codec.createRowKey
|
|
100
|
+
};
|
|
101
|
+
}
|
|
75
102
|
function parseSchemaEntity(value) {
|
|
76
103
|
if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || value.kind !== "schema" || !Number.isInteger(value.formVersion) || typeof value.payload !== "string") {
|
|
77
104
|
throw new Error("Azure Table schema entity is invalid.");
|
|
@@ -84,40 +111,73 @@ function parseSchemaEntity(value) {
|
|
|
84
111
|
return cloneJson(schema);
|
|
85
112
|
}
|
|
86
113
|
function parseSubmissionEntity(value, codec) {
|
|
87
|
-
if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || value
|
|
114
|
+
if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || !codec.matchesEntity(value)) {
|
|
88
115
|
throw new Error("Azure Table submission entity is invalid.");
|
|
89
116
|
}
|
|
90
117
|
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
|
|
118
|
+
if (codec.createPartitionKey(submission) !== value.partitionKey || codec.createRowKey(submission) !== value.rowKey) {
|
|
119
|
+
throw new Error("Azure Table submission entity has inconsistent keys.");
|
|
93
120
|
}
|
|
94
121
|
return submission;
|
|
95
122
|
}
|
|
96
123
|
function escapeOData(value) {
|
|
97
124
|
return value.replaceAll("'", "''");
|
|
98
125
|
}
|
|
99
|
-
function
|
|
126
|
+
function valueToOData(value) {
|
|
100
127
|
if (value === null) return "null";
|
|
101
128
|
if (typeof value === "string") return `'${escapeOData(value)}'`;
|
|
102
129
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
103
130
|
if (typeof value === "boolean") return String(value);
|
|
104
|
-
throw new TypeError("Azure Table
|
|
131
|
+
throw new TypeError("Azure Table OData filters support only scalar JSON values.");
|
|
105
132
|
}
|
|
106
133
|
function metadataFiltersToOData(options) {
|
|
107
134
|
return Object.entries(options.metadataFilters ?? {}).map(([key, value]) => {
|
|
108
135
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new TypeError(`Invalid Azure Table property name: ${key}`);
|
|
109
|
-
return `${key} eq ${
|
|
136
|
+
return `${key} eq ${valueToOData(value)}`;
|
|
110
137
|
}).join(" and ");
|
|
111
138
|
}
|
|
112
|
-
function
|
|
139
|
+
function odataProperty(path) {
|
|
140
|
+
if (path === "id" || path === "responseId") return "responseId";
|
|
141
|
+
if (["formVersion", "locale", "submittedAt"].includes(path)) return path;
|
|
142
|
+
if (path.startsWith("metadata.")) {
|
|
143
|
+
const property = path.slice("metadata.".length);
|
|
144
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(property) ? property : void 0;
|
|
145
|
+
}
|
|
146
|
+
return void 0;
|
|
147
|
+
}
|
|
148
|
+
function submissionFilterToOData(filter) {
|
|
149
|
+
if (filter.op === "and" || filter.op === "or") {
|
|
150
|
+
const converted = filter.filters.map(submissionFilterToOData);
|
|
151
|
+
if (filter.op === "or" && converted.some((value) => value === void 0)) return void 0;
|
|
152
|
+
const available = converted.filter((value) => value !== void 0 && value.length > 0);
|
|
153
|
+
if (available.length === 0) return void 0;
|
|
154
|
+
return available.map((value) => `(${value})`).join(filter.op === "and" ? " and " : " or ");
|
|
155
|
+
}
|
|
156
|
+
const property = odataProperty(filter.path);
|
|
157
|
+
if (property === void 0) return void 0;
|
|
158
|
+
if (filter.op === "eq") return `${property} eq ${valueToOData(filter.value)}`;
|
|
159
|
+
if (filter.op === "in") {
|
|
160
|
+
if (filter.values.length === 0) return "false";
|
|
161
|
+
return filter.values.map((value) => `${property} eq ${valueToOData(value)}`).join(" or ");
|
|
162
|
+
}
|
|
163
|
+
if (filter.op === "exists") return `${property} ${filter.value ? "ne" : "eq"} null`;
|
|
164
|
+
return [
|
|
165
|
+
...filter.from === void 0 ? [] : [`${property} ge ${valueToOData(filter.from)}`],
|
|
166
|
+
...filter.to === void 0 ? [] : [`${property} le ${valueToOData(filter.to)}`]
|
|
167
|
+
].join(" and ");
|
|
168
|
+
}
|
|
169
|
+
function defaultSubmissionFilter(codec, formId, options, legacyExtension) {
|
|
170
|
+
const partitionKey = codec.createPartitionKeyFromQuery(formId, options);
|
|
171
|
+
const ast = options.filter === void 0 || typeof options.filter === "function" ? void 0 : submissionFilterToOData(options.filter);
|
|
113
172
|
return [
|
|
114
|
-
...
|
|
115
|
-
"kind eq 'submission'",
|
|
173
|
+
...partitionKey === void 0 ? [] : [`PartitionKey eq '${escapeOData(partitionKey)}'`],
|
|
174
|
+
...codec === defaultAzureTableSubmissionCodec ? ["kind eq 'submission'"] : [],
|
|
116
175
|
...options.version === void 0 ? [] : [`formVersion eq ${options.version}`],
|
|
117
176
|
...options.since === void 0 ? [] : [`submittedAt ge '${escapeOData(options.since)}'`],
|
|
118
177
|
...options.until === void 0 ? [] : [`submittedAt le '${escapeOData(options.until)}'`],
|
|
119
178
|
...options.locale === void 0 ? [] : [`locale eq '${escapeOData(options.locale)}'`],
|
|
120
|
-
...
|
|
179
|
+
...ast === void 0 || ast.length === 0 ? [] : [`(${ast})`],
|
|
180
|
+
...legacyExtension.trim().length === 0 ? [] : [`(${legacyExtension})`]
|
|
121
181
|
].join(" and ");
|
|
122
182
|
}
|
|
123
183
|
function isNotFound(error) {
|
|
@@ -126,26 +186,41 @@ function isNotFound(error) {
|
|
|
126
186
|
function matchesBuiltInFilters(submission, formId, options) {
|
|
127
187
|
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
188
|
}
|
|
129
|
-
function
|
|
130
|
-
const client = preferred ?? fallback;
|
|
189
|
+
function requireClient(client, name) {
|
|
131
190
|
if (client === void 0) throw new TypeError(`${name} is required.`);
|
|
132
191
|
return client;
|
|
133
192
|
}
|
|
134
|
-
function createAzureTableStorage(options) {
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
const codec = options.
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
193
|
+
function createAzureTableStorage(options = {}) {
|
|
194
|
+
const staticSchemas = options.schemasTableClient ?? options.client;
|
|
195
|
+
const staticSubmissions = options.submissionsTableClient ?? options.client;
|
|
196
|
+
const codec = options.codec ?? (options.submissionCodec === void 0 ? defaultAzureTableSubmissionCodec : legacyCodec(options.submissionCodec));
|
|
197
|
+
const maxScanPages = options.maxScanPages ?? 5;
|
|
198
|
+
if (!Number.isSafeInteger(maxScanPages) || maxScanPages < 1) {
|
|
199
|
+
throw new TypeError("maxScanPages must be a positive safe integer.");
|
|
200
|
+
}
|
|
201
|
+
const resolveDynamicClient = async (formId, query) => options.clientResolver?.({ formId, ...query === void 0 ? {} : { query } });
|
|
202
|
+
const schemaClient = async (formId) => requireClient(staticSchemas ?? await resolveDynamicClient(formId), "schemasTableClient or clientResolver");
|
|
203
|
+
const submissionClient = async (formId, query) => requireClient(
|
|
204
|
+
await resolveDynamicClient(formId, query) ?? staticSubmissions,
|
|
205
|
+
"submissionsTableClient or clientResolver"
|
|
206
|
+
);
|
|
207
|
+
const queryFilter = (formId, query) => {
|
|
208
|
+
if (options.buildSubmissionFilter !== void 0) return options.buildSubmissionFilter(formId, query);
|
|
209
|
+
return defaultSubmissionFilter(
|
|
210
|
+
codec,
|
|
211
|
+
formId,
|
|
212
|
+
query,
|
|
213
|
+
options.toODataFilter?.(query) ?? metadataFiltersToOData(query)
|
|
214
|
+
);
|
|
215
|
+
};
|
|
216
|
+
const deserializeIfMatching = (entity) => codec.matchesEntity(entity) ? parseSubmissionEntity(entity, codec) : void 0;
|
|
141
217
|
const listSubmissionCandidates = async (formId, query) => {
|
|
218
|
+
const client = await submissionClient(formId, query);
|
|
142
219
|
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);
|
|
220
|
+
for await (const raw of client.listEntities({ queryOptions: { filter: queryFilter(formId, query) } })) {
|
|
221
|
+
const submission = deserializeIfMatching(raw);
|
|
222
|
+
if (submission === void 0 || !matchesBuiltInFilters(submission, formId, query)) continue;
|
|
223
|
+
if ((0, import_core.matchesSubmissionPageFilters)(submission, query)) found.push(submission);
|
|
149
224
|
}
|
|
150
225
|
return found.sort(
|
|
151
226
|
(left, right) => left.submittedAt.localeCompare(right.submittedAt) || left.id.localeCompare(right.id)
|
|
@@ -161,11 +236,11 @@ function createAzureTableStorage(options) {
|
|
|
161
236
|
formVersion: schema.version,
|
|
162
237
|
payload: JSON.stringify(schema)
|
|
163
238
|
};
|
|
164
|
-
await
|
|
239
|
+
await (await schemaClient(schema.id)).upsertEntity(entity, "Replace");
|
|
165
240
|
},
|
|
166
241
|
async getSchema(formId, formVersion) {
|
|
167
242
|
try {
|
|
168
|
-
return parseSchemaEntity(await
|
|
243
|
+
return parseSchemaEntity(await (await schemaClient(formId)).getEntity(formId, schemaRowKey(formVersion)));
|
|
169
244
|
} catch (error) {
|
|
170
245
|
if (isNotFound(error)) return null;
|
|
171
246
|
throw error;
|
|
@@ -173,25 +248,20 @@ function createAzureTableStorage(options) {
|
|
|
173
248
|
},
|
|
174
249
|
async listSchemas() {
|
|
175
250
|
const found = [];
|
|
176
|
-
for await (const raw of
|
|
251
|
+
for await (const raw of (await schemaClient("")).listEntities({ queryOptions: { filter: "kind eq 'schema'" } })) {
|
|
177
252
|
if (raw.kind === "schema") found.push(parseSchemaEntity(raw));
|
|
178
253
|
}
|
|
179
254
|
return found.sort((left, right) => left.id.localeCompare(right.id) || left.version - right.version);
|
|
180
255
|
},
|
|
181
256
|
async deleteSchema(formId, formVersion) {
|
|
182
|
-
await
|
|
257
|
+
await (await schemaClient(formId)).deleteEntity(formId, schemaRowKey(formVersion));
|
|
183
258
|
},
|
|
184
259
|
async saveSubmission(submission) {
|
|
185
260
|
const stored = parseSubmission(submission, `input ${String(submission?.id)}`);
|
|
186
|
-
await
|
|
187
|
-
...codec.
|
|
261
|
+
await (await submissionClient(stored.formId)).createEntity({
|
|
262
|
+
...codec.createEntity(stored),
|
|
188
263
|
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
|
|
264
|
+
rowKey: codec.createRowKey(stored)
|
|
195
265
|
});
|
|
196
266
|
},
|
|
197
267
|
async listSubmissions(formId, formVersion, queryOptions = {}) {
|
|
@@ -202,46 +272,65 @@ function createAzureTableStorage(options) {
|
|
|
202
272
|
},
|
|
203
273
|
async listSubmissionPage(formId, query = {}) {
|
|
204
274
|
const pageSize = (0, import_core.normalizeSubmissionPageSize)(query.pageSize);
|
|
205
|
-
const
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
275
|
+
const client = await submissionClient(formId, query);
|
|
276
|
+
const items = [];
|
|
277
|
+
let continuationToken = query.cursor;
|
|
278
|
+
let scannedPages = 0;
|
|
279
|
+
do {
|
|
280
|
+
const iterator = client.listEntities({ queryOptions: { filter: queryFilter(formId, query) } }).byPage({
|
|
281
|
+
maxPageSize: Math.max(1, pageSize - items.length),
|
|
282
|
+
...continuationToken === void 0 ? {} : { continuationToken }
|
|
283
|
+
});
|
|
284
|
+
const result = await iterator.next();
|
|
285
|
+
if (result.done === true) {
|
|
286
|
+
continuationToken = void 0;
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
scannedPages += 1;
|
|
290
|
+
for (const raw of result.value) {
|
|
291
|
+
const submission = deserializeIfMatching(raw);
|
|
292
|
+
if (submission === void 0 || !matchesBuiltInFilters(submission, formId, query)) continue;
|
|
293
|
+
if ((0, import_core.matchesSubmissionPageFilters)(submission, query)) items.push(submission);
|
|
294
|
+
}
|
|
295
|
+
continuationToken = result.value.continuationToken;
|
|
296
|
+
} while (items.length < pageSize && continuationToken !== void 0 && scannedPages < maxScanPages);
|
|
212
297
|
return {
|
|
213
298
|
items,
|
|
214
|
-
hasMore:
|
|
215
|
-
...
|
|
299
|
+
hasMore: continuationToken !== void 0 && continuationToken.length > 0,
|
|
300
|
+
...continuationToken === void 0 || continuationToken.length === 0 ? {} : { nextCursor: continuationToken }
|
|
216
301
|
};
|
|
217
302
|
},
|
|
218
303
|
async deleteSubmission(submissionId) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
return;
|
|
304
|
+
const client = await submissionClient("");
|
|
305
|
+
for await (const raw of client.listEntities()) {
|
|
306
|
+
const submission = deserializeIfMatching(raw);
|
|
307
|
+
if (submission?.id !== submissionId) continue;
|
|
308
|
+
if (typeof raw.partitionKey !== "string" || typeof raw.rowKey !== "string") {
|
|
309
|
+
throw new Error("Azure Table submission entity is invalid.");
|
|
226
310
|
}
|
|
311
|
+
await client.deleteEntity(raw.partitionKey, raw.rowKey);
|
|
312
|
+
return;
|
|
227
313
|
}
|
|
228
314
|
},
|
|
229
315
|
async clearResponses(formId) {
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (
|
|
235
|
-
|
|
316
|
+
const query = {};
|
|
317
|
+
const client = await submissionClient(formId, query);
|
|
318
|
+
for await (const raw of client.listEntities({ queryOptions: { filter: queryFilter(formId, query) } })) {
|
|
319
|
+
const submission = deserializeIfMatching(raw);
|
|
320
|
+
if (submission?.formId !== formId) continue;
|
|
321
|
+
if (typeof raw.partitionKey === "string" && typeof raw.rowKey === "string") {
|
|
322
|
+
await client.deleteEntity(raw.partitionKey, raw.rowKey);
|
|
236
323
|
}
|
|
237
324
|
}
|
|
238
325
|
},
|
|
239
326
|
async clear() {
|
|
327
|
+
const schemas = await schemaClient("");
|
|
240
328
|
for await (const raw of schemas.listEntities()) {
|
|
241
329
|
if (typeof raw.partitionKey === "string" && typeof raw.rowKey === "string") {
|
|
242
330
|
await schemas.deleteEntity(raw.partitionKey, raw.rowKey);
|
|
243
331
|
}
|
|
244
332
|
}
|
|
333
|
+
const submissions = await submissionClient("");
|
|
245
334
|
if (submissions === schemas) return;
|
|
246
335
|
for await (const raw of submissions.listEntities()) {
|
|
247
336
|
if (typeof raw.partitionKey === "string" && typeof raw.rowKey === "string") {
|
|
@@ -255,5 +344,6 @@ function createAzureTableStorage(options) {
|
|
|
255
344
|
0 && (module.exports = {
|
|
256
345
|
createAzureTableStorage,
|
|
257
346
|
defaultAzureTableSubmissionCodec,
|
|
258
|
-
metadataFiltersToOData
|
|
347
|
+
metadataFiltersToOData,
|
|
348
|
+
submissionFilterToOData
|
|
259
349
|
});
|
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
|
@@ -40,12 +40,38 @@ function scalarMetadata(metadata) {
|
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
42
|
var defaultAzureTableSubmissionCodec = {
|
|
43
|
+
createEntity: (submission) => ({
|
|
44
|
+
...scalarMetadata(submission.metadata),
|
|
45
|
+
kind: "submission",
|
|
46
|
+
formVersion: submission.formVersion,
|
|
47
|
+
locale: submission.locale,
|
|
48
|
+
submittedAt: submission.submittedAt,
|
|
49
|
+
responseId: submission.id,
|
|
50
|
+
payload: JSON.stringify(submission)
|
|
51
|
+
}),
|
|
52
|
+
deserialize: (entity) => parseSubmission(entity.payload, "submission entity"),
|
|
53
|
+
matchesEntity: (entity) => entity.kind === "submission",
|
|
43
54
|
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")
|
|
55
|
+
createPartitionKeyFromQuery: (formId) => formId,
|
|
56
|
+
createRowKey: defaultSubmissionRowKey
|
|
48
57
|
};
|
|
58
|
+
function legacyCodec(codec) {
|
|
59
|
+
return {
|
|
60
|
+
createEntity: (value) => ({
|
|
61
|
+
...codec.serialize(value),
|
|
62
|
+
kind: "submission",
|
|
63
|
+
formVersion: value.formVersion,
|
|
64
|
+
locale: value.locale,
|
|
65
|
+
submittedAt: value.submittedAt,
|
|
66
|
+
responseId: value.id
|
|
67
|
+
}),
|
|
68
|
+
deserialize: codec.deserialize,
|
|
69
|
+
matchesEntity: (entity) => entity.kind === "submission",
|
|
70
|
+
createPartitionKey: codec.createPartitionKey,
|
|
71
|
+
createPartitionKeyFromQuery: (formId) => codec.createPartitionKeyFromFormId?.(formId) ?? formId,
|
|
72
|
+
createRowKey: codec.createRowKey
|
|
73
|
+
};
|
|
74
|
+
}
|
|
49
75
|
function parseSchemaEntity(value) {
|
|
50
76
|
if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || value.kind !== "schema" || !Number.isInteger(value.formVersion) || typeof value.payload !== "string") {
|
|
51
77
|
throw new Error("Azure Table schema entity is invalid.");
|
|
@@ -58,40 +84,73 @@ function parseSchemaEntity(value) {
|
|
|
58
84
|
return cloneJson(schema);
|
|
59
85
|
}
|
|
60
86
|
function parseSubmissionEntity(value, codec) {
|
|
61
|
-
if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || value
|
|
87
|
+
if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || !codec.matchesEntity(value)) {
|
|
62
88
|
throw new Error("Azure Table submission entity is invalid.");
|
|
63
89
|
}
|
|
64
90
|
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
|
|
91
|
+
if (codec.createPartitionKey(submission) !== value.partitionKey || codec.createRowKey(submission) !== value.rowKey) {
|
|
92
|
+
throw new Error("Azure Table submission entity has inconsistent keys.");
|
|
67
93
|
}
|
|
68
94
|
return submission;
|
|
69
95
|
}
|
|
70
96
|
function escapeOData(value) {
|
|
71
97
|
return value.replaceAll("'", "''");
|
|
72
98
|
}
|
|
73
|
-
function
|
|
99
|
+
function valueToOData(value) {
|
|
74
100
|
if (value === null) return "null";
|
|
75
101
|
if (typeof value === "string") return `'${escapeOData(value)}'`;
|
|
76
102
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
77
103
|
if (typeof value === "boolean") return String(value);
|
|
78
|
-
throw new TypeError("Azure Table
|
|
104
|
+
throw new TypeError("Azure Table OData filters support only scalar JSON values.");
|
|
79
105
|
}
|
|
80
106
|
function metadataFiltersToOData(options) {
|
|
81
107
|
return Object.entries(options.metadataFilters ?? {}).map(([key, value]) => {
|
|
82
108
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new TypeError(`Invalid Azure Table property name: ${key}`);
|
|
83
|
-
return `${key} eq ${
|
|
109
|
+
return `${key} eq ${valueToOData(value)}`;
|
|
84
110
|
}).join(" and ");
|
|
85
111
|
}
|
|
86
|
-
function
|
|
112
|
+
function odataProperty(path) {
|
|
113
|
+
if (path === "id" || path === "responseId") return "responseId";
|
|
114
|
+
if (["formVersion", "locale", "submittedAt"].includes(path)) return path;
|
|
115
|
+
if (path.startsWith("metadata.")) {
|
|
116
|
+
const property = path.slice("metadata.".length);
|
|
117
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(property) ? property : void 0;
|
|
118
|
+
}
|
|
119
|
+
return void 0;
|
|
120
|
+
}
|
|
121
|
+
function submissionFilterToOData(filter) {
|
|
122
|
+
if (filter.op === "and" || filter.op === "or") {
|
|
123
|
+
const converted = filter.filters.map(submissionFilterToOData);
|
|
124
|
+
if (filter.op === "or" && converted.some((value) => value === void 0)) return void 0;
|
|
125
|
+
const available = converted.filter((value) => value !== void 0 && value.length > 0);
|
|
126
|
+
if (available.length === 0) return void 0;
|
|
127
|
+
return available.map((value) => `(${value})`).join(filter.op === "and" ? " and " : " or ");
|
|
128
|
+
}
|
|
129
|
+
const property = odataProperty(filter.path);
|
|
130
|
+
if (property === void 0) return void 0;
|
|
131
|
+
if (filter.op === "eq") return `${property} eq ${valueToOData(filter.value)}`;
|
|
132
|
+
if (filter.op === "in") {
|
|
133
|
+
if (filter.values.length === 0) return "false";
|
|
134
|
+
return filter.values.map((value) => `${property} eq ${valueToOData(value)}`).join(" or ");
|
|
135
|
+
}
|
|
136
|
+
if (filter.op === "exists") return `${property} ${filter.value ? "ne" : "eq"} null`;
|
|
137
|
+
return [
|
|
138
|
+
...filter.from === void 0 ? [] : [`${property} ge ${valueToOData(filter.from)}`],
|
|
139
|
+
...filter.to === void 0 ? [] : [`${property} le ${valueToOData(filter.to)}`]
|
|
140
|
+
].join(" and ");
|
|
141
|
+
}
|
|
142
|
+
function defaultSubmissionFilter(codec, formId, options, legacyExtension) {
|
|
143
|
+
const partitionKey = codec.createPartitionKeyFromQuery(formId, options);
|
|
144
|
+
const ast = options.filter === void 0 || typeof options.filter === "function" ? void 0 : submissionFilterToOData(options.filter);
|
|
87
145
|
return [
|
|
88
|
-
...
|
|
89
|
-
"kind eq 'submission'",
|
|
146
|
+
...partitionKey === void 0 ? [] : [`PartitionKey eq '${escapeOData(partitionKey)}'`],
|
|
147
|
+
...codec === defaultAzureTableSubmissionCodec ? ["kind eq 'submission'"] : [],
|
|
90
148
|
...options.version === void 0 ? [] : [`formVersion eq ${options.version}`],
|
|
91
149
|
...options.since === void 0 ? [] : [`submittedAt ge '${escapeOData(options.since)}'`],
|
|
92
150
|
...options.until === void 0 ? [] : [`submittedAt le '${escapeOData(options.until)}'`],
|
|
93
151
|
...options.locale === void 0 ? [] : [`locale eq '${escapeOData(options.locale)}'`],
|
|
94
|
-
...
|
|
152
|
+
...ast === void 0 || ast.length === 0 ? [] : [`(${ast})`],
|
|
153
|
+
...legacyExtension.trim().length === 0 ? [] : [`(${legacyExtension})`]
|
|
95
154
|
].join(" and ");
|
|
96
155
|
}
|
|
97
156
|
function isNotFound(error) {
|
|
@@ -100,26 +159,41 @@ function isNotFound(error) {
|
|
|
100
159
|
function matchesBuiltInFilters(submission, formId, options) {
|
|
101
160
|
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
161
|
}
|
|
103
|
-
function
|
|
104
|
-
const client = preferred ?? fallback;
|
|
162
|
+
function requireClient(client, name) {
|
|
105
163
|
if (client === void 0) throw new TypeError(`${name} is required.`);
|
|
106
164
|
return client;
|
|
107
165
|
}
|
|
108
|
-
function createAzureTableStorage(options) {
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
const codec = options.
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
166
|
+
function createAzureTableStorage(options = {}) {
|
|
167
|
+
const staticSchemas = options.schemasTableClient ?? options.client;
|
|
168
|
+
const staticSubmissions = options.submissionsTableClient ?? options.client;
|
|
169
|
+
const codec = options.codec ?? (options.submissionCodec === void 0 ? defaultAzureTableSubmissionCodec : legacyCodec(options.submissionCodec));
|
|
170
|
+
const maxScanPages = options.maxScanPages ?? 5;
|
|
171
|
+
if (!Number.isSafeInteger(maxScanPages) || maxScanPages < 1) {
|
|
172
|
+
throw new TypeError("maxScanPages must be a positive safe integer.");
|
|
173
|
+
}
|
|
174
|
+
const resolveDynamicClient = async (formId, query) => options.clientResolver?.({ formId, ...query === void 0 ? {} : { query } });
|
|
175
|
+
const schemaClient = async (formId) => requireClient(staticSchemas ?? await resolveDynamicClient(formId), "schemasTableClient or clientResolver");
|
|
176
|
+
const submissionClient = async (formId, query) => requireClient(
|
|
177
|
+
await resolveDynamicClient(formId, query) ?? staticSubmissions,
|
|
178
|
+
"submissionsTableClient or clientResolver"
|
|
179
|
+
);
|
|
180
|
+
const queryFilter = (formId, query) => {
|
|
181
|
+
if (options.buildSubmissionFilter !== void 0) return options.buildSubmissionFilter(formId, query);
|
|
182
|
+
return defaultSubmissionFilter(
|
|
183
|
+
codec,
|
|
184
|
+
formId,
|
|
185
|
+
query,
|
|
186
|
+
options.toODataFilter?.(query) ?? metadataFiltersToOData(query)
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
const deserializeIfMatching = (entity) => codec.matchesEntity(entity) ? parseSubmissionEntity(entity, codec) : void 0;
|
|
115
190
|
const listSubmissionCandidates = async (formId, query) => {
|
|
191
|
+
const client = await submissionClient(formId, query);
|
|
116
192
|
const found = [];
|
|
117
|
-
for await (const raw of
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
if (!matchesSubmissionPageFilters(submission, query)) continue;
|
|
122
|
-
found.push(submission);
|
|
193
|
+
for await (const raw of client.listEntities({ queryOptions: { filter: queryFilter(formId, query) } })) {
|
|
194
|
+
const submission = deserializeIfMatching(raw);
|
|
195
|
+
if (submission === void 0 || !matchesBuiltInFilters(submission, formId, query)) continue;
|
|
196
|
+
if (matchesSubmissionPageFilters(submission, query)) found.push(submission);
|
|
123
197
|
}
|
|
124
198
|
return found.sort(
|
|
125
199
|
(left, right) => left.submittedAt.localeCompare(right.submittedAt) || left.id.localeCompare(right.id)
|
|
@@ -135,11 +209,11 @@ function createAzureTableStorage(options) {
|
|
|
135
209
|
formVersion: schema.version,
|
|
136
210
|
payload: JSON.stringify(schema)
|
|
137
211
|
};
|
|
138
|
-
await
|
|
212
|
+
await (await schemaClient(schema.id)).upsertEntity(entity, "Replace");
|
|
139
213
|
},
|
|
140
214
|
async getSchema(formId, formVersion) {
|
|
141
215
|
try {
|
|
142
|
-
return parseSchemaEntity(await
|
|
216
|
+
return parseSchemaEntity(await (await schemaClient(formId)).getEntity(formId, schemaRowKey(formVersion)));
|
|
143
217
|
} catch (error) {
|
|
144
218
|
if (isNotFound(error)) return null;
|
|
145
219
|
throw error;
|
|
@@ -147,25 +221,20 @@ function createAzureTableStorage(options) {
|
|
|
147
221
|
},
|
|
148
222
|
async listSchemas() {
|
|
149
223
|
const found = [];
|
|
150
|
-
for await (const raw of
|
|
224
|
+
for await (const raw of (await schemaClient("")).listEntities({ queryOptions: { filter: "kind eq 'schema'" } })) {
|
|
151
225
|
if (raw.kind === "schema") found.push(parseSchemaEntity(raw));
|
|
152
226
|
}
|
|
153
227
|
return found.sort((left, right) => left.id.localeCompare(right.id) || left.version - right.version);
|
|
154
228
|
},
|
|
155
229
|
async deleteSchema(formId, formVersion) {
|
|
156
|
-
await
|
|
230
|
+
await (await schemaClient(formId)).deleteEntity(formId, schemaRowKey(formVersion));
|
|
157
231
|
},
|
|
158
232
|
async saveSubmission(submission) {
|
|
159
233
|
const stored = parseSubmission(submission, `input ${String(submission?.id)}`);
|
|
160
|
-
await
|
|
161
|
-
...codec.
|
|
234
|
+
await (await submissionClient(stored.formId)).createEntity({
|
|
235
|
+
...codec.createEntity(stored),
|
|
162
236
|
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
|
|
237
|
+
rowKey: codec.createRowKey(stored)
|
|
169
238
|
});
|
|
170
239
|
},
|
|
171
240
|
async listSubmissions(formId, formVersion, queryOptions = {}) {
|
|
@@ -176,46 +245,65 @@ function createAzureTableStorage(options) {
|
|
|
176
245
|
},
|
|
177
246
|
async listSubmissionPage(formId, query = {}) {
|
|
178
247
|
const pageSize = normalizeSubmissionPageSize(query.pageSize);
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
248
|
+
const client = await submissionClient(formId, query);
|
|
249
|
+
const items = [];
|
|
250
|
+
let continuationToken = query.cursor;
|
|
251
|
+
let scannedPages = 0;
|
|
252
|
+
do {
|
|
253
|
+
const iterator = client.listEntities({ queryOptions: { filter: queryFilter(formId, query) } }).byPage({
|
|
254
|
+
maxPageSize: Math.max(1, pageSize - items.length),
|
|
255
|
+
...continuationToken === void 0 ? {} : { continuationToken }
|
|
256
|
+
});
|
|
257
|
+
const result = await iterator.next();
|
|
258
|
+
if (result.done === true) {
|
|
259
|
+
continuationToken = void 0;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
scannedPages += 1;
|
|
263
|
+
for (const raw of result.value) {
|
|
264
|
+
const submission = deserializeIfMatching(raw);
|
|
265
|
+
if (submission === void 0 || !matchesBuiltInFilters(submission, formId, query)) continue;
|
|
266
|
+
if (matchesSubmissionPageFilters(submission, query)) items.push(submission);
|
|
267
|
+
}
|
|
268
|
+
continuationToken = result.value.continuationToken;
|
|
269
|
+
} while (items.length < pageSize && continuationToken !== void 0 && scannedPages < maxScanPages);
|
|
186
270
|
return {
|
|
187
271
|
items,
|
|
188
|
-
hasMore:
|
|
189
|
-
...
|
|
272
|
+
hasMore: continuationToken !== void 0 && continuationToken.length > 0,
|
|
273
|
+
...continuationToken === void 0 || continuationToken.length === 0 ? {} : { nextCursor: continuationToken }
|
|
190
274
|
};
|
|
191
275
|
},
|
|
192
276
|
async deleteSubmission(submissionId) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return;
|
|
277
|
+
const client = await submissionClient("");
|
|
278
|
+
for await (const raw of client.listEntities()) {
|
|
279
|
+
const submission = deserializeIfMatching(raw);
|
|
280
|
+
if (submission?.id !== submissionId) continue;
|
|
281
|
+
if (typeof raw.partitionKey !== "string" || typeof raw.rowKey !== "string") {
|
|
282
|
+
throw new Error("Azure Table submission entity is invalid.");
|
|
200
283
|
}
|
|
284
|
+
await client.deleteEntity(raw.partitionKey, raw.rowKey);
|
|
285
|
+
return;
|
|
201
286
|
}
|
|
202
287
|
},
|
|
203
288
|
async clearResponses(formId) {
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
|
|
289
|
+
const query = {};
|
|
290
|
+
const client = await submissionClient(formId, query);
|
|
291
|
+
for await (const raw of client.listEntities({ queryOptions: { filter: queryFilter(formId, query) } })) {
|
|
292
|
+
const submission = deserializeIfMatching(raw);
|
|
293
|
+
if (submission?.formId !== formId) continue;
|
|
294
|
+
if (typeof raw.partitionKey === "string" && typeof raw.rowKey === "string") {
|
|
295
|
+
await client.deleteEntity(raw.partitionKey, raw.rowKey);
|
|
210
296
|
}
|
|
211
297
|
}
|
|
212
298
|
},
|
|
213
299
|
async clear() {
|
|
300
|
+
const schemas = await schemaClient("");
|
|
214
301
|
for await (const raw of schemas.listEntities()) {
|
|
215
302
|
if (typeof raw.partitionKey === "string" && typeof raw.rowKey === "string") {
|
|
216
303
|
await schemas.deleteEntity(raw.partitionKey, raw.rowKey);
|
|
217
304
|
}
|
|
218
305
|
}
|
|
306
|
+
const submissions = await submissionClient("");
|
|
219
307
|
if (submissions === schemas) return;
|
|
220
308
|
for await (const raw of submissions.listEntities()) {
|
|
221
309
|
if (typeof raw.partitionKey === "string" && typeof raw.rowKey === "string") {
|
|
@@ -228,5 +316,6 @@ function createAzureTableStorage(options) {
|
|
|
228
316
|
export {
|
|
229
317
|
createAzureTableStorage,
|
|
230
318
|
defaultAzureTableSubmissionCodec,
|
|
231
|
-
metadataFiltersToOData
|
|
319
|
+
metadataFiltersToOData,
|
|
320
|
+
submissionFilterToOData
|
|
232
321
|
};
|
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.7.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.7.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@azure/data-tables": "^13.3.2"
|