@form-engine-ts/storage-azure-table 2.5.1 → 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 +16 -5
- package/dist/index.cjs +227 -94
- package/dist/index.d.cts +49 -6
- package/dist/index.d.ts +49 -6
- package/dist/index.js +224 -100
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -15,12 +15,23 @@ pnpm add @form-engine-ts/core @form-engine-ts/storage-azure-table @azure/data-ta
|
|
|
15
15
|
import { TableClient } from "@azure/data-tables";
|
|
16
16
|
import { createAzureTableStorage } from "@form-engine-ts/storage-azure-table";
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
const
|
|
18
|
+
const schemasTableClient = TableClient.fromConnectionString(process.env.AZURE_STORAGE_CONNECTION_STRING!, "forms");
|
|
19
|
+
const submissionsTableClient = TableClient.fromConnectionString(
|
|
20
|
+
process.env.AZURE_STORAGE_CONNECTION_STRING!,
|
|
21
|
+
"responses"
|
|
22
|
+
);
|
|
23
|
+
const storage = createAzureTableStorage({ schemasTableClient, submissionsTableClient });
|
|
20
24
|
|
|
21
25
|
const page = await storage.listSubmissionPage("contact", { pageSize: 500, locale: "ja" });
|
|
22
26
|
```
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
@@ -20,7 +20,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
-
createAzureTableStorage: () => createAzureTableStorage
|
|
23
|
+
createAzureTableStorage: () => createAzureTableStorage,
|
|
24
|
+
defaultAzureTableSubmissionCodec: () => defaultAzureTableSubmissionCodec,
|
|
25
|
+
metadataFiltersToOData: () => metadataFiltersToOData,
|
|
26
|
+
submissionFilterToOData: () => submissionFilterToOData
|
|
24
27
|
});
|
|
25
28
|
module.exports = __toCommonJS(index_exports);
|
|
26
29
|
var import_core = require("@form-engine-ts/core");
|
|
@@ -42,127 +45,223 @@ function parseJson(value, location) {
|
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
function parseSubmission(value, location) {
|
|
45
|
-
const parsed = parseJson(value, location);
|
|
48
|
+
const parsed = typeof value === "string" ? parseJson(value, location) : value;
|
|
46
49
|
if (!isRecord(parsed) || typeof parsed.id !== "string" || typeof parsed.formId !== "string" || !Number.isInteger(parsed.formVersion) || typeof parsed.locale !== "string" || typeof parsed.submittedAt !== "string" || !isRecord(parsed.values) || !Object.values(parsed.values).every(isFormValue)) {
|
|
47
50
|
throw new Error(`Azure Table ${location} submission is invalid.`);
|
|
48
51
|
}
|
|
49
52
|
return cloneJson(parsed);
|
|
50
53
|
}
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return
|
|
54
|
+
function schemaRowKey(version) {
|
|
55
|
+
return `schema_${version}`;
|
|
56
|
+
}
|
|
57
|
+
function defaultSubmissionRowKey(submission) {
|
|
58
|
+
return `${submission.submittedAt}_${submission.id}`;
|
|
59
|
+
}
|
|
60
|
+
function scalarMetadata(metadata) {
|
|
61
|
+
if (metadata === void 0) return {};
|
|
62
|
+
return Object.fromEntries(
|
|
63
|
+
Object.entries(metadata).filter((entry) => {
|
|
64
|
+
const value = entry[1];
|
|
65
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
66
|
+
})
|
|
67
|
+
);
|
|
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",
|
|
81
|
+
createPartitionKey: (submission) => submission.formId,
|
|
82
|
+
createPartitionKeyFromQuery: (formId) => formId,
|
|
83
|
+
createRowKey: defaultSubmissionRowKey
|
|
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
|
+
};
|
|
56
101
|
}
|
|
57
102
|
function parseSchemaEntity(value) {
|
|
58
|
-
|
|
59
|
-
|
|
103
|
+
if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || value.kind !== "schema" || !Number.isInteger(value.formVersion) || typeof value.payload !== "string") {
|
|
104
|
+
throw new Error("Azure Table schema entity is invalid.");
|
|
105
|
+
}
|
|
106
|
+
const schema = parseJson(value.payload, `schema ${value.partitionKey}/${value.rowKey}`);
|
|
60
107
|
(0, import_core.assertValidFormSchema)(schema);
|
|
61
|
-
if (
|
|
108
|
+
if (schema.id !== value.partitionKey || schema.version !== value.formVersion) {
|
|
62
109
|
throw new Error("Azure Table schema entity has inconsistent metadata.");
|
|
63
110
|
}
|
|
64
111
|
return cloneJson(schema);
|
|
65
112
|
}
|
|
66
|
-
function parseSubmissionEntity(value) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
113
|
+
function parseSubmissionEntity(value, codec) {
|
|
114
|
+
if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || !codec.matchesEntity(value)) {
|
|
115
|
+
throw new Error("Azure Table submission entity is invalid.");
|
|
116
|
+
}
|
|
117
|
+
const submission = parseSubmission(codec.deserialize(value), `submission ${value.partitionKey}/${value.rowKey}`);
|
|
118
|
+
if (codec.createPartitionKey(submission) !== value.partitionKey || codec.createRowKey(submission) !== value.rowKey) {
|
|
119
|
+
throw new Error("Azure Table submission entity has inconsistent keys.");
|
|
71
120
|
}
|
|
72
121
|
return submission;
|
|
73
122
|
}
|
|
74
|
-
function schemaRowKey(version) {
|
|
75
|
-
return `schema_${version}`;
|
|
76
|
-
}
|
|
77
|
-
function submissionRowKey(submission) {
|
|
78
|
-
return `${submission.submittedAt}_${submission.id}`;
|
|
79
|
-
}
|
|
80
123
|
function escapeOData(value) {
|
|
81
124
|
return value.replaceAll("'", "''");
|
|
82
125
|
}
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
126
|
+
function valueToOData(value) {
|
|
127
|
+
if (value === null) return "null";
|
|
128
|
+
if (typeof value === "string") return `'${escapeOData(value)}'`;
|
|
129
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
130
|
+
if (typeof value === "boolean") return String(value);
|
|
131
|
+
throw new TypeError("Azure Table OData filters support only scalar JSON values.");
|
|
132
|
+
}
|
|
133
|
+
function metadataFiltersToOData(options) {
|
|
134
|
+
return Object.entries(options.metadataFilters ?? {}).map(([key, value]) => {
|
|
135
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new TypeError(`Invalid Azure Table property name: ${key}`);
|
|
136
|
+
return `${key} eq ${valueToOData(value)}`;
|
|
137
|
+
}).join(" and ");
|
|
138
|
+
}
|
|
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);
|
|
172
|
+
return [
|
|
173
|
+
...partitionKey === void 0 ? [] : [`PartitionKey eq '${escapeOData(partitionKey)}'`],
|
|
174
|
+
...codec === defaultAzureTableSubmissionCodec ? ["kind eq 'submission'"] : [],
|
|
87
175
|
...options.version === void 0 ? [] : [`formVersion eq ${options.version}`],
|
|
88
176
|
...options.since === void 0 ? [] : [`submittedAt ge '${escapeOData(options.since)}'`],
|
|
89
177
|
...options.until === void 0 ? [] : [`submittedAt le '${escapeOData(options.until)}'`],
|
|
90
|
-
...options.locale === void 0 ? [] : [`locale eq '${escapeOData(options.locale)}'`]
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
filters.push(`RowKey gt '${escapeOData(`${cursor.submittedAt}_${cursor.responseId}`)}'`);
|
|
95
|
-
}
|
|
96
|
-
return filters.join(" and ");
|
|
178
|
+
...options.locale === void 0 ? [] : [`locale eq '${escapeOData(options.locale)}'`],
|
|
179
|
+
...ast === void 0 || ast.length === 0 ? [] : [`(${ast})`],
|
|
180
|
+
...legacyExtension.trim().length === 0 ? [] : [`(${legacyExtension})`]
|
|
181
|
+
].join(" and ");
|
|
97
182
|
}
|
|
98
183
|
function isNotFound(error) {
|
|
99
184
|
return isRecord(error) && (error.statusCode === 404 || error.code === "ResourceNotFound" || error.code === "EntityNotFound");
|
|
100
185
|
}
|
|
101
186
|
function matchesBuiltInFilters(submission, formId, options) {
|
|
102
|
-
|
|
103
|
-
|
|
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);
|
|
188
|
+
}
|
|
189
|
+
function requireClient(client, name) {
|
|
190
|
+
if (client === void 0) throw new TypeError(`${name} is required.`);
|
|
191
|
+
return client;
|
|
104
192
|
}
|
|
105
|
-
function createAzureTableStorage(options) {
|
|
106
|
-
|
|
107
|
-
const
|
|
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;
|
|
108
217
|
const listSubmissionCandidates = async (formId, query) => {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
if (!(0, import_core.matchesSubmissionPageFilters)(submission, query)) continue;
|
|
116
|
-
submissions.push(submission);
|
|
218
|
+
const client = await submissionClient(formId, query);
|
|
219
|
+
const found = [];
|
|
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);
|
|
117
224
|
}
|
|
118
|
-
return
|
|
225
|
+
return found.sort(
|
|
119
226
|
(left, right) => left.submittedAt.localeCompare(right.submittedAt) || left.id.localeCompare(right.id)
|
|
120
227
|
);
|
|
121
228
|
};
|
|
122
229
|
return {
|
|
123
230
|
async saveSchema(schema) {
|
|
124
231
|
(0, import_core.assertValidFormSchema)(schema);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
"Replace"
|
|
134
|
-
);
|
|
232
|
+
const entity = {
|
|
233
|
+
partitionKey: schema.id,
|
|
234
|
+
rowKey: schemaRowKey(schema.version),
|
|
235
|
+
kind: "schema",
|
|
236
|
+
formVersion: schema.version,
|
|
237
|
+
payload: JSON.stringify(schema)
|
|
238
|
+
};
|
|
239
|
+
await (await schemaClient(schema.id)).upsertEntity(entity, "Replace");
|
|
135
240
|
},
|
|
136
241
|
async getSchema(formId, formVersion) {
|
|
137
242
|
try {
|
|
138
|
-
return parseSchemaEntity(await
|
|
243
|
+
return parseSchemaEntity(await (await schemaClient(formId)).getEntity(formId, schemaRowKey(formVersion)));
|
|
139
244
|
} catch (error) {
|
|
140
245
|
if (isNotFound(error)) return null;
|
|
141
246
|
throw error;
|
|
142
247
|
}
|
|
143
248
|
},
|
|
144
249
|
async listSchemas() {
|
|
145
|
-
const
|
|
146
|
-
for await (const raw of
|
|
147
|
-
|
|
148
|
-
if (entity.kind === "schema") schemas.push(parseSchemaEntity(entity));
|
|
250
|
+
const found = [];
|
|
251
|
+
for await (const raw of (await schemaClient("")).listEntities({ queryOptions: { filter: "kind eq 'schema'" } })) {
|
|
252
|
+
if (raw.kind === "schema") found.push(parseSchemaEntity(raw));
|
|
149
253
|
}
|
|
150
|
-
return
|
|
254
|
+
return found.sort((left, right) => left.id.localeCompare(right.id) || left.version - right.version);
|
|
151
255
|
},
|
|
152
256
|
async deleteSchema(formId, formVersion) {
|
|
153
|
-
await
|
|
257
|
+
await (await schemaClient(formId)).deleteEntity(formId, schemaRowKey(formVersion));
|
|
154
258
|
},
|
|
155
259
|
async saveSubmission(submission) {
|
|
156
|
-
const stored = parseSubmission(
|
|
157
|
-
await
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
formVersion: stored.formVersion,
|
|
162
|
-
locale: stored.locale,
|
|
163
|
-
submittedAt: stored.submittedAt,
|
|
164
|
-
responseId: stored.id,
|
|
165
|
-
payload: JSON.stringify(stored)
|
|
260
|
+
const stored = parseSubmission(submission, `input ${String(submission?.id)}`);
|
|
261
|
+
await (await submissionClient(stored.formId)).createEntity({
|
|
262
|
+
...codec.createEntity(stored),
|
|
263
|
+
partitionKey: codec.createPartitionKey(stored),
|
|
264
|
+
rowKey: codec.createRowKey(stored)
|
|
166
265
|
});
|
|
167
266
|
},
|
|
168
267
|
async listSubmissions(formId, formVersion, queryOptions = {}) {
|
|
@@ -173,44 +272,78 @@ function createAzureTableStorage(options) {
|
|
|
173
272
|
},
|
|
174
273
|
async listSubmissionPage(formId, query = {}) {
|
|
175
274
|
const pageSize = (0, import_core.normalizeSubmissionPageSize)(query.pageSize);
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
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);
|
|
180
297
|
return {
|
|
181
298
|
items,
|
|
182
|
-
hasMore,
|
|
183
|
-
...
|
|
299
|
+
hasMore: continuationToken !== void 0 && continuationToken.length > 0,
|
|
300
|
+
...continuationToken === void 0 || continuationToken.length === 0 ? {} : { nextCursor: continuationToken }
|
|
184
301
|
};
|
|
185
302
|
},
|
|
186
303
|
async deleteSubmission(submissionId) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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.");
|
|
192
310
|
}
|
|
311
|
+
await client.deleteEntity(raw.partitionKey, raw.rowKey);
|
|
312
|
+
return;
|
|
193
313
|
}
|
|
194
314
|
},
|
|
195
315
|
async clearResponses(formId) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
})) {
|
|
199
|
-
const
|
|
200
|
-
if (
|
|
201
|
-
|
|
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);
|
|
202
323
|
}
|
|
203
324
|
}
|
|
204
325
|
},
|
|
205
326
|
async clear() {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
327
|
+
const schemas = await schemaClient("");
|
|
328
|
+
for await (const raw of schemas.listEntities()) {
|
|
329
|
+
if (typeof raw.partitionKey === "string" && typeof raw.rowKey === "string") {
|
|
330
|
+
await schemas.deleteEntity(raw.partitionKey, raw.rowKey);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const submissions = await submissionClient("");
|
|
334
|
+
if (submissions === schemas) return;
|
|
335
|
+
for await (const raw of submissions.listEntities()) {
|
|
336
|
+
if (typeof raw.partitionKey === "string" && typeof raw.rowKey === "string") {
|
|
337
|
+
await submissions.deleteEntity(raw.partitionKey, raw.rowKey);
|
|
338
|
+
}
|
|
209
339
|
}
|
|
210
340
|
}
|
|
211
341
|
};
|
|
212
342
|
}
|
|
213
343
|
// Annotate the CommonJS export names for ESM import in node:
|
|
214
344
|
0 && (module.exports = {
|
|
215
|
-
createAzureTableStorage
|
|
345
|
+
createAzureTableStorage,
|
|
346
|
+
defaultAzureTableSubmissionCodec,
|
|
347
|
+
metadataFiltersToOData,
|
|
348
|
+
submissionFilterToOData
|
|
216
349
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,20 +1,63 @@
|
|
|
1
|
-
import { 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?: {
|
|
5
5
|
readonly filter?: string;
|
|
6
6
|
};
|
|
7
7
|
}
|
|
8
|
+
interface AzureTablePageSettings {
|
|
9
|
+
readonly maxPageSize?: number;
|
|
10
|
+
readonly continuationToken?: string;
|
|
11
|
+
}
|
|
12
|
+
interface AzureTableEntityPage extends ReadonlyArray<Record<string, unknown>> {
|
|
13
|
+
readonly continuationToken?: string;
|
|
14
|
+
}
|
|
15
|
+
interface AzureTableEntityIterator extends AsyncIterable<Record<string, unknown>> {
|
|
16
|
+
byPage(settings?: AzureTablePageSettings): AsyncIterableIterator<AzureTableEntityPage>;
|
|
17
|
+
}
|
|
8
18
|
interface AzureTableClientLike {
|
|
9
19
|
createEntity(entity: Record<string, unknown>): Promise<unknown>;
|
|
10
20
|
upsertEntity(entity: Record<string, unknown>, mode?: "Merge" | "Replace"): Promise<unknown>;
|
|
11
21
|
getEntity(partitionKey: string, rowKey: string): Promise<Record<string, unknown>>;
|
|
12
|
-
listEntities(options?: AzureTableListOptions):
|
|
22
|
+
listEntities(options?: AzureTableListOptions): AzureTableEntityIterator;
|
|
13
23
|
deleteEntity(partitionKey: string, rowKey: string): Promise<unknown>;
|
|
14
24
|
}
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
/** @deprecated Use AzureTableSubmissionCodec. */
|
|
26
|
+
interface AzureTableEntityCodec<T> {
|
|
27
|
+
readonly createPartitionKey: (submission: T) => string;
|
|
28
|
+
readonly createPartitionKeyFromFormId?: (formId: string) => string;
|
|
29
|
+
readonly createRowKey: (submission: T) => string;
|
|
30
|
+
readonly serialize: (submission: T) => Record<string, unknown>;
|
|
31
|
+
readonly deserialize: (entity: Record<string, unknown>) => T;
|
|
32
|
+
}
|
|
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. */
|
|
43
|
+
readonly client?: AzureTableClientLike;
|
|
44
|
+
readonly schemasTableClient?: AzureTableClientLike;
|
|
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. */
|
|
52
|
+
readonly submissionCodec?: AzureTableEntityCodec<FormSubmission>;
|
|
53
|
+
readonly buildSubmissionFilter?: (formId: string, query: SubmissionPageQueryOptions) => string;
|
|
54
|
+
/** @deprecated Use buildSubmissionFilter. */
|
|
55
|
+
readonly toODataFilter?: (options: SubmissionPageQueryOptions) => string;
|
|
56
|
+
readonly maxScanPages?: number;
|
|
17
57
|
}
|
|
18
|
-
declare
|
|
58
|
+
declare const defaultAzureTableSubmissionCodec: AzureTableSubmissionCodec<FormSubmission>;
|
|
59
|
+
declare function metadataFiltersToOData(options: SubmissionPageQueryOptions): string;
|
|
60
|
+
declare function submissionFilterToOData(filter: SubmissionFilter): string | undefined;
|
|
61
|
+
declare function createAzureTableStorage(options?: AzureTableStorageOptions): PagedSubmissionStorageAdapter;
|
|
19
62
|
|
|
20
|
-
export { type AzureTableClientLike, type AzureTableListOptions, type AzureTableStorageOptions, createAzureTableStorage };
|
|
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,20 +1,63 @@
|
|
|
1
|
-
import { 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?: {
|
|
5
5
|
readonly filter?: string;
|
|
6
6
|
};
|
|
7
7
|
}
|
|
8
|
+
interface AzureTablePageSettings {
|
|
9
|
+
readonly maxPageSize?: number;
|
|
10
|
+
readonly continuationToken?: string;
|
|
11
|
+
}
|
|
12
|
+
interface AzureTableEntityPage extends ReadonlyArray<Record<string, unknown>> {
|
|
13
|
+
readonly continuationToken?: string;
|
|
14
|
+
}
|
|
15
|
+
interface AzureTableEntityIterator extends AsyncIterable<Record<string, unknown>> {
|
|
16
|
+
byPage(settings?: AzureTablePageSettings): AsyncIterableIterator<AzureTableEntityPage>;
|
|
17
|
+
}
|
|
8
18
|
interface AzureTableClientLike {
|
|
9
19
|
createEntity(entity: Record<string, unknown>): Promise<unknown>;
|
|
10
20
|
upsertEntity(entity: Record<string, unknown>, mode?: "Merge" | "Replace"): Promise<unknown>;
|
|
11
21
|
getEntity(partitionKey: string, rowKey: string): Promise<Record<string, unknown>>;
|
|
12
|
-
listEntities(options?: AzureTableListOptions):
|
|
22
|
+
listEntities(options?: AzureTableListOptions): AzureTableEntityIterator;
|
|
13
23
|
deleteEntity(partitionKey: string, rowKey: string): Promise<unknown>;
|
|
14
24
|
}
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
/** @deprecated Use AzureTableSubmissionCodec. */
|
|
26
|
+
interface AzureTableEntityCodec<T> {
|
|
27
|
+
readonly createPartitionKey: (submission: T) => string;
|
|
28
|
+
readonly createPartitionKeyFromFormId?: (formId: string) => string;
|
|
29
|
+
readonly createRowKey: (submission: T) => string;
|
|
30
|
+
readonly serialize: (submission: T) => Record<string, unknown>;
|
|
31
|
+
readonly deserialize: (entity: Record<string, unknown>) => T;
|
|
32
|
+
}
|
|
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. */
|
|
43
|
+
readonly client?: AzureTableClientLike;
|
|
44
|
+
readonly schemasTableClient?: AzureTableClientLike;
|
|
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. */
|
|
52
|
+
readonly submissionCodec?: AzureTableEntityCodec<FormSubmission>;
|
|
53
|
+
readonly buildSubmissionFilter?: (formId: string, query: SubmissionPageQueryOptions) => string;
|
|
54
|
+
/** @deprecated Use buildSubmissionFilter. */
|
|
55
|
+
readonly toODataFilter?: (options: SubmissionPageQueryOptions) => string;
|
|
56
|
+
readonly maxScanPages?: number;
|
|
17
57
|
}
|
|
18
|
-
declare
|
|
58
|
+
declare const defaultAzureTableSubmissionCodec: AzureTableSubmissionCodec<FormSubmission>;
|
|
59
|
+
declare function metadataFiltersToOData(options: SubmissionPageQueryOptions): string;
|
|
60
|
+
declare function submissionFilterToOData(filter: SubmissionFilter): string | undefined;
|
|
61
|
+
declare function createAzureTableStorage(options?: AzureTableStorageOptions): PagedSubmissionStorageAdapter;
|
|
19
62
|
|
|
20
|
-
export { type AzureTableClientLike, type AzureTableListOptions, type AzureTableStorageOptions, createAzureTableStorage };
|
|
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,11 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import {
|
|
3
|
-
assertValidFormSchema,
|
|
4
|
-
decodeSubmissionCursor,
|
|
5
|
-
encodeSubmissionCursor,
|
|
6
|
-
matchesSubmissionPageFilters,
|
|
7
|
-
normalizeSubmissionPageSize
|
|
8
|
-
} from "@form-engine-ts/core";
|
|
2
|
+
import { assertValidFormSchema, matchesSubmissionPageFilters, normalizeSubmissionPageSize } from "@form-engine-ts/core";
|
|
9
3
|
function cloneJson(value) {
|
|
10
4
|
return JSON.parse(JSON.stringify(value));
|
|
11
5
|
}
|
|
@@ -24,127 +18,223 @@ function parseJson(value, location) {
|
|
|
24
18
|
}
|
|
25
19
|
}
|
|
26
20
|
function parseSubmission(value, location) {
|
|
27
|
-
const parsed = parseJson(value, location);
|
|
21
|
+
const parsed = typeof value === "string" ? parseJson(value, location) : value;
|
|
28
22
|
if (!isRecord(parsed) || typeof parsed.id !== "string" || typeof parsed.formId !== "string" || !Number.isInteger(parsed.formVersion) || typeof parsed.locale !== "string" || typeof parsed.submittedAt !== "string" || !isRecord(parsed.values) || !Object.values(parsed.values).every(isFormValue)) {
|
|
29
23
|
throw new Error(`Azure Table ${location} submission is invalid.`);
|
|
30
24
|
}
|
|
31
25
|
return cloneJson(parsed);
|
|
32
26
|
}
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return
|
|
27
|
+
function schemaRowKey(version) {
|
|
28
|
+
return `schema_${version}`;
|
|
29
|
+
}
|
|
30
|
+
function defaultSubmissionRowKey(submission) {
|
|
31
|
+
return `${submission.submittedAt}_${submission.id}`;
|
|
32
|
+
}
|
|
33
|
+
function scalarMetadata(metadata) {
|
|
34
|
+
if (metadata === void 0) return {};
|
|
35
|
+
return Object.fromEntries(
|
|
36
|
+
Object.entries(metadata).filter((entry) => {
|
|
37
|
+
const value = entry[1];
|
|
38
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
}
|
|
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",
|
|
54
|
+
createPartitionKey: (submission) => submission.formId,
|
|
55
|
+
createPartitionKeyFromQuery: (formId) => formId,
|
|
56
|
+
createRowKey: defaultSubmissionRowKey
|
|
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
|
+
};
|
|
38
74
|
}
|
|
39
75
|
function parseSchemaEntity(value) {
|
|
40
|
-
|
|
41
|
-
|
|
76
|
+
if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || value.kind !== "schema" || !Number.isInteger(value.formVersion) || typeof value.payload !== "string") {
|
|
77
|
+
throw new Error("Azure Table schema entity is invalid.");
|
|
78
|
+
}
|
|
79
|
+
const schema = parseJson(value.payload, `schema ${value.partitionKey}/${value.rowKey}`);
|
|
42
80
|
assertValidFormSchema(schema);
|
|
43
|
-
if (
|
|
81
|
+
if (schema.id !== value.partitionKey || schema.version !== value.formVersion) {
|
|
44
82
|
throw new Error("Azure Table schema entity has inconsistent metadata.");
|
|
45
83
|
}
|
|
46
84
|
return cloneJson(schema);
|
|
47
85
|
}
|
|
48
|
-
function parseSubmissionEntity(value) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
86
|
+
function parseSubmissionEntity(value, codec) {
|
|
87
|
+
if (typeof value.partitionKey !== "string" || typeof value.rowKey !== "string" || !codec.matchesEntity(value)) {
|
|
88
|
+
throw new Error("Azure Table submission entity is invalid.");
|
|
89
|
+
}
|
|
90
|
+
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 keys.");
|
|
53
93
|
}
|
|
54
94
|
return submission;
|
|
55
95
|
}
|
|
56
|
-
function schemaRowKey(version) {
|
|
57
|
-
return `schema_${version}`;
|
|
58
|
-
}
|
|
59
|
-
function submissionRowKey(submission) {
|
|
60
|
-
return `${submission.submittedAt}_${submission.id}`;
|
|
61
|
-
}
|
|
62
96
|
function escapeOData(value) {
|
|
63
97
|
return value.replaceAll("'", "''");
|
|
64
98
|
}
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
99
|
+
function valueToOData(value) {
|
|
100
|
+
if (value === null) return "null";
|
|
101
|
+
if (typeof value === "string") return `'${escapeOData(value)}'`;
|
|
102
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
103
|
+
if (typeof value === "boolean") return String(value);
|
|
104
|
+
throw new TypeError("Azure Table OData filters support only scalar JSON values.");
|
|
105
|
+
}
|
|
106
|
+
function metadataFiltersToOData(options) {
|
|
107
|
+
return Object.entries(options.metadataFilters ?? {}).map(([key, value]) => {
|
|
108
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new TypeError(`Invalid Azure Table property name: ${key}`);
|
|
109
|
+
return `${key} eq ${valueToOData(value)}`;
|
|
110
|
+
}).join(" and ");
|
|
111
|
+
}
|
|
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);
|
|
145
|
+
return [
|
|
146
|
+
...partitionKey === void 0 ? [] : [`PartitionKey eq '${escapeOData(partitionKey)}'`],
|
|
147
|
+
...codec === defaultAzureTableSubmissionCodec ? ["kind eq 'submission'"] : [],
|
|
69
148
|
...options.version === void 0 ? [] : [`formVersion eq ${options.version}`],
|
|
70
149
|
...options.since === void 0 ? [] : [`submittedAt ge '${escapeOData(options.since)}'`],
|
|
71
150
|
...options.until === void 0 ? [] : [`submittedAt le '${escapeOData(options.until)}'`],
|
|
72
|
-
...options.locale === void 0 ? [] : [`locale eq '${escapeOData(options.locale)}'`]
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
filters.push(`RowKey gt '${escapeOData(`${cursor.submittedAt}_${cursor.responseId}`)}'`);
|
|
77
|
-
}
|
|
78
|
-
return filters.join(" and ");
|
|
151
|
+
...options.locale === void 0 ? [] : [`locale eq '${escapeOData(options.locale)}'`],
|
|
152
|
+
...ast === void 0 || ast.length === 0 ? [] : [`(${ast})`],
|
|
153
|
+
...legacyExtension.trim().length === 0 ? [] : [`(${legacyExtension})`]
|
|
154
|
+
].join(" and ");
|
|
79
155
|
}
|
|
80
156
|
function isNotFound(error) {
|
|
81
157
|
return isRecord(error) && (error.statusCode === 404 || error.code === "ResourceNotFound" || error.code === "EntityNotFound");
|
|
82
158
|
}
|
|
83
159
|
function matchesBuiltInFilters(submission, formId, options) {
|
|
84
|
-
|
|
85
|
-
|
|
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);
|
|
161
|
+
}
|
|
162
|
+
function requireClient(client, name) {
|
|
163
|
+
if (client === void 0) throw new TypeError(`${name} is required.`);
|
|
164
|
+
return client;
|
|
86
165
|
}
|
|
87
|
-
function createAzureTableStorage(options) {
|
|
88
|
-
|
|
89
|
-
const
|
|
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;
|
|
90
190
|
const listSubmissionCandidates = async (formId, query) => {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
if (!matchesSubmissionPageFilters(submission, query)) continue;
|
|
98
|
-
submissions.push(submission);
|
|
191
|
+
const client = await submissionClient(formId, query);
|
|
192
|
+
const found = [];
|
|
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);
|
|
99
197
|
}
|
|
100
|
-
return
|
|
198
|
+
return found.sort(
|
|
101
199
|
(left, right) => left.submittedAt.localeCompare(right.submittedAt) || left.id.localeCompare(right.id)
|
|
102
200
|
);
|
|
103
201
|
};
|
|
104
202
|
return {
|
|
105
203
|
async saveSchema(schema) {
|
|
106
204
|
assertValidFormSchema(schema);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
"Replace"
|
|
116
|
-
);
|
|
205
|
+
const entity = {
|
|
206
|
+
partitionKey: schema.id,
|
|
207
|
+
rowKey: schemaRowKey(schema.version),
|
|
208
|
+
kind: "schema",
|
|
209
|
+
formVersion: schema.version,
|
|
210
|
+
payload: JSON.stringify(schema)
|
|
211
|
+
};
|
|
212
|
+
await (await schemaClient(schema.id)).upsertEntity(entity, "Replace");
|
|
117
213
|
},
|
|
118
214
|
async getSchema(formId, formVersion) {
|
|
119
215
|
try {
|
|
120
|
-
return parseSchemaEntity(await
|
|
216
|
+
return parseSchemaEntity(await (await schemaClient(formId)).getEntity(formId, schemaRowKey(formVersion)));
|
|
121
217
|
} catch (error) {
|
|
122
218
|
if (isNotFound(error)) return null;
|
|
123
219
|
throw error;
|
|
124
220
|
}
|
|
125
221
|
},
|
|
126
222
|
async listSchemas() {
|
|
127
|
-
const
|
|
128
|
-
for await (const raw of
|
|
129
|
-
|
|
130
|
-
if (entity.kind === "schema") schemas.push(parseSchemaEntity(entity));
|
|
223
|
+
const found = [];
|
|
224
|
+
for await (const raw of (await schemaClient("")).listEntities({ queryOptions: { filter: "kind eq 'schema'" } })) {
|
|
225
|
+
if (raw.kind === "schema") found.push(parseSchemaEntity(raw));
|
|
131
226
|
}
|
|
132
|
-
return
|
|
227
|
+
return found.sort((left, right) => left.id.localeCompare(right.id) || left.version - right.version);
|
|
133
228
|
},
|
|
134
229
|
async deleteSchema(formId, formVersion) {
|
|
135
|
-
await
|
|
230
|
+
await (await schemaClient(formId)).deleteEntity(formId, schemaRowKey(formVersion));
|
|
136
231
|
},
|
|
137
232
|
async saveSubmission(submission) {
|
|
138
|
-
const stored = parseSubmission(
|
|
139
|
-
await
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
formVersion: stored.formVersion,
|
|
144
|
-
locale: stored.locale,
|
|
145
|
-
submittedAt: stored.submittedAt,
|
|
146
|
-
responseId: stored.id,
|
|
147
|
-
payload: JSON.stringify(stored)
|
|
233
|
+
const stored = parseSubmission(submission, `input ${String(submission?.id)}`);
|
|
234
|
+
await (await submissionClient(stored.formId)).createEntity({
|
|
235
|
+
...codec.createEntity(stored),
|
|
236
|
+
partitionKey: codec.createPartitionKey(stored),
|
|
237
|
+
rowKey: codec.createRowKey(stored)
|
|
148
238
|
});
|
|
149
239
|
},
|
|
150
240
|
async listSubmissions(formId, formVersion, queryOptions = {}) {
|
|
@@ -155,43 +245,77 @@ function createAzureTableStorage(options) {
|
|
|
155
245
|
},
|
|
156
246
|
async listSubmissionPage(formId, query = {}) {
|
|
157
247
|
const pageSize = normalizeSubmissionPageSize(query.pageSize);
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
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);
|
|
162
270
|
return {
|
|
163
271
|
items,
|
|
164
|
-
hasMore,
|
|
165
|
-
...
|
|
272
|
+
hasMore: continuationToken !== void 0 && continuationToken.length > 0,
|
|
273
|
+
...continuationToken === void 0 || continuationToken.length === 0 ? {} : { nextCursor: continuationToken }
|
|
166
274
|
};
|
|
167
275
|
},
|
|
168
276
|
async deleteSubmission(submissionId) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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.");
|
|
174
283
|
}
|
|
284
|
+
await client.deleteEntity(raw.partitionKey, raw.rowKey);
|
|
285
|
+
return;
|
|
175
286
|
}
|
|
176
287
|
},
|
|
177
288
|
async clearResponses(formId) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
})) {
|
|
181
|
-
const
|
|
182
|
-
if (
|
|
183
|
-
|
|
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);
|
|
184
296
|
}
|
|
185
297
|
}
|
|
186
298
|
},
|
|
187
299
|
async clear() {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
300
|
+
const schemas = await schemaClient("");
|
|
301
|
+
for await (const raw of schemas.listEntities()) {
|
|
302
|
+
if (typeof raw.partitionKey === "string" && typeof raw.rowKey === "string") {
|
|
303
|
+
await schemas.deleteEntity(raw.partitionKey, raw.rowKey);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const submissions = await submissionClient("");
|
|
307
|
+
if (submissions === schemas) return;
|
|
308
|
+
for await (const raw of submissions.listEntities()) {
|
|
309
|
+
if (typeof raw.partitionKey === "string" && typeof raw.rowKey === "string") {
|
|
310
|
+
await submissions.deleteEntity(raw.partitionKey, raw.rowKey);
|
|
311
|
+
}
|
|
191
312
|
}
|
|
192
313
|
}
|
|
193
314
|
};
|
|
194
315
|
}
|
|
195
316
|
export {
|
|
196
|
-
createAzureTableStorage
|
|
317
|
+
createAzureTableStorage,
|
|
318
|
+
defaultAzureTableSubmissionCodec,
|
|
319
|
+
metadataFiltersToOData,
|
|
320
|
+
submissionFilterToOData
|
|
197
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"
|