@form-engine-ts/storage-postgres 2.2.0 → 2.5.1

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
@@ -28,3 +28,7 @@ const submissions = await storage.listSubmissions(schema.id, schema.version, {
28
28
  `client` may be a `Pool`, `Client`, or a wrapper exposing the same `query(text, values)` shape. The caller owns connections and transactions. `autoMigrate` defaults to `false`; when enabled, the adapter lazily runs idempotent `CREATE TABLE/INDEX IF NOT EXISTS` statements once before its first operation. Use a migration framework for production schema evolution.
29
29
 
30
30
  Schemas use a `(form_id, form_version)` primary key and JSONB payload. Responses use their globally unique ID as the primary key, retain searchable form/version/locale/timestamp columns, and store the complete submission as JSONB. Custom table names must be safe SQL identifiers. Range boundaries are inclusive and results are ordered by submission time and then ID. Schema deletion never cascades to responses; `clearResponses(formId)` removes every version of one form, while `clear()` deletes rows only from the configured tables.
31
+
32
+ `listSubmissionPage(formId, { version, pageSize, cursor, since, until, locale })` uses a parameterized keyset query over
33
+ `submitted_at, response_id`. Its opaque cursor handles equal timestamps without `OFFSET` gaps or duplicates.
34
+ `metadataFilters` and custom `filter` predicates are applied before page sizing.
package/dist/index.cjs CHANGED
@@ -182,6 +182,54 @@ function createPostgresStorage(options) {
182
182
  );
183
183
  return result.rows.map(parseSubmissionRow);
184
184
  },
185
+ async listSubmissionPage(formId, queryOptions = {}) {
186
+ await ensureReady();
187
+ const pageSize = (0, import_core.normalizeSubmissionPageSize)(queryOptions.pageSize);
188
+ const cursor = queryOptions.cursor === void 0 ? void 0 : (0, import_core.decodeSubmissionCursor)(queryOptions.cursor);
189
+ const conditions = ["form_id = $1"];
190
+ const params = [formId];
191
+ if (queryOptions.version !== void 0) {
192
+ params.push(queryOptions.version);
193
+ conditions.push(`form_version = $${params.length}`);
194
+ }
195
+ if (queryOptions.since !== void 0) {
196
+ params.push(queryOptions.since);
197
+ conditions.push(`submitted_at >= $${params.length}::timestamptz`);
198
+ }
199
+ if (queryOptions.until !== void 0) {
200
+ params.push(queryOptions.until);
201
+ conditions.push(`submitted_at <= $${params.length}::timestamptz`);
202
+ }
203
+ if (queryOptions.locale !== void 0) {
204
+ params.push(queryOptions.locale);
205
+ conditions.push(`locale = $${params.length}`);
206
+ }
207
+ if (cursor !== void 0) {
208
+ params.push(cursor.submittedAt);
209
+ const timestampParameter = params.length;
210
+ params.push(cursor.responseId);
211
+ conditions.push(
212
+ `(submitted_at > $${timestampParameter}::timestamptz OR (submitted_at = $${timestampParameter}::timestamptz AND response_id > $${params.length}))`
213
+ );
214
+ }
215
+ const requiresClientFiltering = queryOptions.filter !== void 0 || queryOptions.metadataFilters !== void 0;
216
+ if (!requiresClientFiltering) params.push(pageSize + 1);
217
+ const result = await options.client.query(
218
+ `SELECT response_id, form_id, form_version, locale, submitted_at, submission_json
219
+ FROM ${responsesTable} WHERE ${conditions.join(" AND ")}
220
+ ORDER BY submitted_at, response_id${requiresClientFiltering ? "" : ` LIMIT $${params.length}`}`,
221
+ params
222
+ );
223
+ const candidates = result.rows.map(parseSubmissionRow).filter((item) => (0, import_core.matchesSubmissionPageFilters)(item, queryOptions));
224
+ const hasMore = candidates.length > pageSize;
225
+ const items = candidates.slice(0, pageSize);
226
+ const last = items.at(-1);
227
+ return {
228
+ items,
229
+ hasMore,
230
+ ...hasMore && last !== void 0 ? { nextCursor: (0, import_core.encodeSubmissionCursor)({ submittedAt: last.submittedAt, responseId: last.id }) } : {}
231
+ };
232
+ },
185
233
  async deleteSubmission(submissionId) {
186
234
  await ensureReady();
187
235
  await options.client.query(`DELETE FROM ${responsesTable} WHERE response_id = $1`, [submissionId]);
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { FormStorageAdapter } from '@form-engine-ts/core';
1
+ import { PagedSubmissionStorageAdapter } from '@form-engine-ts/core';
2
2
 
3
3
  interface PostgresClientLike {
4
4
  query(text: string, params?: unknown[]): Promise<{
@@ -11,6 +11,6 @@ interface PostgresStorageOptions {
11
11
  readonly responsesTable?: string;
12
12
  readonly autoMigrate?: boolean;
13
13
  }
14
- declare function createPostgresStorage(options: PostgresStorageOptions): FormStorageAdapter;
14
+ declare function createPostgresStorage(options: PostgresStorageOptions): PagedSubmissionStorageAdapter;
15
15
 
16
16
  export { type PostgresClientLike, type PostgresStorageOptions, createPostgresStorage };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { FormStorageAdapter } from '@form-engine-ts/core';
1
+ import { PagedSubmissionStorageAdapter } from '@form-engine-ts/core';
2
2
 
3
3
  interface PostgresClientLike {
4
4
  query(text: string, params?: unknown[]): Promise<{
@@ -11,6 +11,6 @@ interface PostgresStorageOptions {
11
11
  readonly responsesTable?: string;
12
12
  readonly autoMigrate?: boolean;
13
13
  }
14
- declare function createPostgresStorage(options: PostgresStorageOptions): FormStorageAdapter;
14
+ declare function createPostgresStorage(options: PostgresStorageOptions): PagedSubmissionStorageAdapter;
15
15
 
16
16
  export { type PostgresClientLike, type PostgresStorageOptions, createPostgresStorage };
package/dist/index.js CHANGED
@@ -1,5 +1,11 @@
1
1
  // src/index.ts
2
- import { assertValidFormSchema } from "@form-engine-ts/core";
2
+ import {
3
+ assertValidFormSchema,
4
+ decodeSubmissionCursor,
5
+ encodeSubmissionCursor,
6
+ matchesSubmissionPageFilters,
7
+ normalizeSubmissionPageSize
8
+ } from "@form-engine-ts/core";
3
9
  function isRecord(value) {
4
10
  return typeof value === "object" && value !== null && !Array.isArray(value);
5
11
  }
@@ -158,6 +164,54 @@ function createPostgresStorage(options) {
158
164
  );
159
165
  return result.rows.map(parseSubmissionRow);
160
166
  },
167
+ async listSubmissionPage(formId, queryOptions = {}) {
168
+ await ensureReady();
169
+ const pageSize = normalizeSubmissionPageSize(queryOptions.pageSize);
170
+ const cursor = queryOptions.cursor === void 0 ? void 0 : decodeSubmissionCursor(queryOptions.cursor);
171
+ const conditions = ["form_id = $1"];
172
+ const params = [formId];
173
+ if (queryOptions.version !== void 0) {
174
+ params.push(queryOptions.version);
175
+ conditions.push(`form_version = $${params.length}`);
176
+ }
177
+ if (queryOptions.since !== void 0) {
178
+ params.push(queryOptions.since);
179
+ conditions.push(`submitted_at >= $${params.length}::timestamptz`);
180
+ }
181
+ if (queryOptions.until !== void 0) {
182
+ params.push(queryOptions.until);
183
+ conditions.push(`submitted_at <= $${params.length}::timestamptz`);
184
+ }
185
+ if (queryOptions.locale !== void 0) {
186
+ params.push(queryOptions.locale);
187
+ conditions.push(`locale = $${params.length}`);
188
+ }
189
+ if (cursor !== void 0) {
190
+ params.push(cursor.submittedAt);
191
+ const timestampParameter = params.length;
192
+ params.push(cursor.responseId);
193
+ conditions.push(
194
+ `(submitted_at > $${timestampParameter}::timestamptz OR (submitted_at = $${timestampParameter}::timestamptz AND response_id > $${params.length}))`
195
+ );
196
+ }
197
+ const requiresClientFiltering = queryOptions.filter !== void 0 || queryOptions.metadataFilters !== void 0;
198
+ if (!requiresClientFiltering) params.push(pageSize + 1);
199
+ const result = await options.client.query(
200
+ `SELECT response_id, form_id, form_version, locale, submitted_at, submission_json
201
+ FROM ${responsesTable} WHERE ${conditions.join(" AND ")}
202
+ ORDER BY submitted_at, response_id${requiresClientFiltering ? "" : ` LIMIT $${params.length}`}`,
203
+ params
204
+ );
205
+ const candidates = result.rows.map(parseSubmissionRow).filter((item) => matchesSubmissionPageFilters(item, queryOptions));
206
+ const hasMore = candidates.length > pageSize;
207
+ const items = candidates.slice(0, pageSize);
208
+ const last = items.at(-1);
209
+ return {
210
+ items,
211
+ hasMore,
212
+ ...hasMore && last !== void 0 ? { nextCursor: encodeSubmissionCursor({ submittedAt: last.submittedAt, responseId: last.id }) } : {}
213
+ };
214
+ },
161
215
  async deleteSubmission(submissionId) {
162
216
  await ensureReady();
163
217
  await options.client.query(`DELETE FROM ${responsesTable} WHERE response_id = $1`, [submissionId]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/storage-postgres",
3
- "version": "2.2.0",
3
+ "version": "2.5.1",
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.2.0"
42
+ "@form-engine-ts/core": "2.5.1"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "pg": "^8.11.0"