@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 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
- Submission entities use `formId` as `PartitionKey` and `submittedAt_responseId` as `RowKey`. Built-in date, locale,
29
- version, and cursor constraints are sent as OData filters. `listSubmissionPage` calls the Azure iterator's
30
- `.byPage({ maxPageSize, continuationToken })` and consumes exactly one native page per request; the returned service token
31
- remains opaque. Scalar metadata filters can be converted to OData with `metadataFiltersToOData` or a custom
32
- `toODataFilter`. Supply `submissionCodec` for custom entity layouts. The deprecated single `client` option remains
33
- available for compatibility. The caller owns table creation, credentials, retries, and the client lifecycle.
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
- createPartitionKeyFromFormId: (formId) => formId,
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.kind !== "submission") {
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 || value.formVersion !== submission.formVersion || value.locale !== submission.locale || value.submittedAt !== submission.submittedAt || value.responseId !== submission.id) {
92
- throw new Error("Azure Table submission entity has inconsistent metadata.");
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 metadataValueToOData(value) {
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 metadata OData filters support only scalar JSON values.");
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 ${metadataValueToOData(value)}`;
136
+ return `${key} eq ${valueToOData(value)}`;
110
137
  }).join(" and ");
111
138
  }
112
- function odataFilter(formId, options, extension) {
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
- ...formId === void 0 ? [] : [`PartitionKey eq '${escapeOData(formId)}'`],
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
- ...extension.trim().length === 0 ? [] : [`(${extension})`]
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 resolveClient(preferred, fallback, name) {
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 schemas = resolveClient(options?.schemasTableClient, options?.client, "schemasTableClient");
136
- const submissions = resolveClient(options?.submissionsTableClient, options?.client, "submissionsTableClient");
137
- const codec = options.submissionCodec ?? defaultAzureTableSubmissionCodec;
138
- const toODataFilter = options.toODataFilter ?? metadataFiltersToOData;
139
- const partitionKeyFromFormId = codec.createPartitionKeyFromFormId ?? ((formId) => formId);
140
- const queryFilter = (formId, query) => odataFilter(formId === void 0 ? void 0 : partitionKeyFromFormId(formId), query, toODataFilter(query));
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 submissions.listEntities({ queryOptions: { filter: queryFilter(formId, query) } })) {
144
- if (raw.kind !== "submission") continue;
145
- const submission = parseSubmissionEntity(raw, codec);
146
- if (!matchesBuiltInFilters(submission, formId, query)) continue;
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 schemas.upsertEntity(entity, "Replace");
239
+ await (await schemaClient(schema.id)).upsertEntity(entity, "Replace");
165
240
  },
166
241
  async getSchema(formId, formVersion) {
167
242
  try {
168
- return parseSchemaEntity(await schemas.getEntity(formId, schemaRowKey(formVersion)));
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 schemas.listEntities({ queryOptions: { filter: "kind eq 'schema'" } })) {
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 schemas.deleteEntity(formId, schemaRowKey(formVersion));
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 submissions.createEntity({
187
- ...codec.serialize(stored),
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 iterator = submissions.listEntities({ queryOptions: { filter: queryFilter(formId, query) } }).byPage({ maxPageSize: pageSize, ...query.cursor === void 0 ? {} : { continuationToken: query.cursor } });
206
- const result = await iterator.next();
207
- if (result.done === true) return { items: [], hasMore: false };
208
- const items = result.value.filter((raw) => raw.kind === "submission").map((raw) => parseSubmissionEntity(raw, codec)).filter(
209
- (submission) => matchesBuiltInFilters(submission, formId, query) && (0, import_core.matchesSubmissionPageFilters)(submission, query)
210
- );
211
- const nextCursor = result.value.continuationToken;
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: nextCursor !== void 0 && nextCursor.length > 0,
215
- ...nextCursor === void 0 || nextCursor.length === 0 ? {} : { nextCursor }
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
- for await (const raw of submissions.listEntities({ queryOptions: { filter: "kind eq 'submission'" } })) {
220
- if (raw.kind === "submission" && raw.responseId === submissionId) {
221
- if (typeof raw.partitionKey !== "string" || typeof raw.rowKey !== "string") {
222
- throw new Error("Azure Table submission entity is invalid.");
223
- }
224
- await submissions.deleteEntity(raw.partitionKey, raw.rowKey);
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 partitionKey = partitionKeyFromFormId(formId);
231
- for await (const raw of submissions.listEntities({
232
- queryOptions: { filter: `PartitionKey eq '${escapeOData(partitionKey)}' and kind eq 'submission'` }
233
- })) {
234
- if (raw.partitionKey === partitionKey && raw.kind === "submission" && typeof raw.rowKey === "string") {
235
- await submissions.deleteEntity(partitionKey, raw.rowKey);
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 AzureTableStorageOptions {
33
- /** @deprecated Use schemasTableClient and submissionsTableClient. */
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: AzureTableEntityCodec<FormSubmission>;
58
+ declare const defaultAzureTableSubmissionCodec: AzureTableSubmissionCodec<FormSubmission>;
41
59
  declare function metadataFiltersToOData(options: SubmissionPageQueryOptions): string;
42
- declare function createAzureTableStorage(options: AzureTableStorageOptions): PagedSubmissionStorageAdapter;
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 AzureTableStorageOptions {
33
- /** @deprecated Use schemasTableClient and submissionsTableClient. */
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: AzureTableEntityCodec<FormSubmission>;
58
+ declare const defaultAzureTableSubmissionCodec: AzureTableSubmissionCodec<FormSubmission>;
41
59
  declare function metadataFiltersToOData(options: SubmissionPageQueryOptions): string;
42
- declare function createAzureTableStorage(options: AzureTableStorageOptions): PagedSubmissionStorageAdapter;
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
- createPartitionKeyFromFormId: (formId) => formId,
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.kind !== "submission") {
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 || value.formVersion !== submission.formVersion || value.locale !== submission.locale || value.submittedAt !== submission.submittedAt || value.responseId !== submission.id) {
66
- throw new Error("Azure Table submission entity has inconsistent metadata.");
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 metadataValueToOData(value) {
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 metadata OData filters support only scalar JSON values.");
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 ${metadataValueToOData(value)}`;
109
+ return `${key} eq ${valueToOData(value)}`;
84
110
  }).join(" and ");
85
111
  }
86
- function odataFilter(formId, options, extension) {
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
- ...formId === void 0 ? [] : [`PartitionKey eq '${escapeOData(formId)}'`],
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
- ...extension.trim().length === 0 ? [] : [`(${extension})`]
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 resolveClient(preferred, fallback, name) {
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 schemas = resolveClient(options?.schemasTableClient, options?.client, "schemasTableClient");
110
- const submissions = resolveClient(options?.submissionsTableClient, options?.client, "submissionsTableClient");
111
- const codec = options.submissionCodec ?? defaultAzureTableSubmissionCodec;
112
- const toODataFilter = options.toODataFilter ?? metadataFiltersToOData;
113
- const partitionKeyFromFormId = codec.createPartitionKeyFromFormId ?? ((formId) => formId);
114
- const queryFilter = (formId, query) => odataFilter(formId === void 0 ? void 0 : partitionKeyFromFormId(formId), query, toODataFilter(query));
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 submissions.listEntities({ queryOptions: { filter: queryFilter(formId, query) } })) {
118
- if (raw.kind !== "submission") continue;
119
- const submission = parseSubmissionEntity(raw, codec);
120
- if (!matchesBuiltInFilters(submission, formId, query)) continue;
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 schemas.upsertEntity(entity, "Replace");
212
+ await (await schemaClient(schema.id)).upsertEntity(entity, "Replace");
139
213
  },
140
214
  async getSchema(formId, formVersion) {
141
215
  try {
142
- return parseSchemaEntity(await schemas.getEntity(formId, schemaRowKey(formVersion)));
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 schemas.listEntities({ queryOptions: { filter: "kind eq 'schema'" } })) {
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 schemas.deleteEntity(formId, schemaRowKey(formVersion));
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 submissions.createEntity({
161
- ...codec.serialize(stored),
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 iterator = submissions.listEntities({ queryOptions: { filter: queryFilter(formId, query) } }).byPage({ maxPageSize: pageSize, ...query.cursor === void 0 ? {} : { continuationToken: query.cursor } });
180
- const result = await iterator.next();
181
- if (result.done === true) return { items: [], hasMore: false };
182
- const items = result.value.filter((raw) => raw.kind === "submission").map((raw) => parseSubmissionEntity(raw, codec)).filter(
183
- (submission) => matchesBuiltInFilters(submission, formId, query) && matchesSubmissionPageFilters(submission, query)
184
- );
185
- const nextCursor = result.value.continuationToken;
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: nextCursor !== void 0 && nextCursor.length > 0,
189
- ...nextCursor === void 0 || nextCursor.length === 0 ? {} : { nextCursor }
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
- for await (const raw of submissions.listEntities({ queryOptions: { filter: "kind eq 'submission'" } })) {
194
- if (raw.kind === "submission" && raw.responseId === submissionId) {
195
- if (typeof raw.partitionKey !== "string" || typeof raw.rowKey !== "string") {
196
- throw new Error("Azure Table submission entity is invalid.");
197
- }
198
- await submissions.deleteEntity(raw.partitionKey, raw.rowKey);
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 partitionKey = partitionKeyFromFormId(formId);
205
- for await (const raw of submissions.listEntities({
206
- queryOptions: { filter: `PartitionKey eq '${escapeOData(partitionKey)}' and kind eq 'submission'` }
207
- })) {
208
- if (raw.partitionKey === partitionKey && raw.kind === "submission" && typeof raw.rowKey === "string") {
209
- await submissions.deleteEntity(partitionKey, raw.rowKey);
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.6.0",
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.6.0"
42
+ "@form-engine-ts/core": "2.7.0"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "@azure/data-tables": "^13.3.2"