@form-engine-ts/storage-postgres 2.3.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 +4 -0
- package/dist/index.cjs +48 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +55 -1
- package/package.json +2 -2
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 {
|
|
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):
|
|
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 {
|
|
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):
|
|
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 {
|
|
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.
|
|
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.
|
|
42
|
+
"@form-engine-ts/core": "2.5.1"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"pg": "^8.11.0"
|