@form-engine-ts/storage-sqlite 8.1.7 → 8.2.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
@@ -30,4 +30,5 @@ const storage = createSqliteStorage({ db: executor, autoMigrate: true });
30
30
  `autoMigrate` defaults to `false`; when enabled, idempotent table/index DDL runs lazily once. Schemas use a `(form_id, form_version)` primary key. Complete schemas and submissions are stored as JSON text alongside searchable response metadata. `listSubmissions` accepts inclusive `since`/`until` ISO 8601 boundaries and orders by timestamp then ID. Schema deletion does not cascade; form-scoped and full clears retain the configured tables.
31
31
 
32
32
  `saveSubmissionWithinLimit(submission, maxResponses, options?)` requires `SqliteExecutor.transaction` so the capacity
33
- check and insert commit atomically. Without it, the method throws an error with `code: "transaction_unsupported"`.
33
+ check and insert commit atomically. `responseLimitScope: "form"` checks the total across versions. Without a transaction,
34
+ the method throws an error with `code: "transaction_unsupported"`.
package/dist/index.cjs CHANGED
@@ -81,6 +81,11 @@ function createSqliteStorage(options) {
81
81
  }
82
82
  const schemasTable = identifier(options.schemasTable, "form_schemas", "schemasTable");
83
83
  const responsesTableName = options.responsesTable ?? "form_responses";
84
+ const runtimeSettingsTable = identifier(
85
+ options.runtimeSettingsTable,
86
+ "form_runtime_settings",
87
+ "runtimeSettingsTable"
88
+ );
84
89
  const responsesTable = identifier(responsesTableName, "form_responses", "responsesTable");
85
90
  const responsesIndex = identifier(`${responsesTableName}_lookup_idx`, "form_responses_lookup_idx", "responsesTable");
86
91
  let migration;
@@ -102,6 +107,10 @@ function createSqliteStorage(options) {
102
107
  submitted_at TEXT NOT NULL,
103
108
  submission_json TEXT NOT NULL
104
109
  )`);
110
+ await options.db.run(`CREATE TABLE IF NOT EXISTS ${runtimeSettingsTable} (
111
+ form_id TEXT PRIMARY KEY,
112
+ settings_json TEXT NOT NULL
113
+ )`);
105
114
  await options.db.run(
106
115
  `CREATE INDEX IF NOT EXISTS ${responsesIndex} ON ${responsesTable} (form_id, submitted_at, response_id)`
107
116
  );
@@ -114,18 +123,18 @@ function createSqliteStorage(options) {
114
123
  };
115
124
  const marker = (_index) => "?";
116
125
  return {
117
- resources: ["schema", "submission"],
126
+ resources: ["schema", "submission", "runtimeSettings"],
118
127
  async list(formId) {
119
128
  await ensureReady();
120
129
  const records = [];
121
- for (const kind of ["schema", "submission"]) {
122
- const table = kind === "schema" ? schemasTable : responsesTable;
130
+ for (const kind of ["schema", "submission", "runtimeSettings"]) {
131
+ const table = kind === "schema" ? schemasTable : kind === "submission" ? responsesTable : runtimeSettingsTable;
123
132
  const rows = await query(`SELECT * FROM ${table} WHERE form_id = ${marker(1)}`, [formId]);
124
133
  for (const row of rows) {
125
134
  if (!isRecord(row)) throw new TypeError("Invalid lifecycle row.");
126
- const payload = row[kind === "schema" ? "schema_json" : "submission_json"];
135
+ const payload = row[kind === "schema" ? "schema_json" : kind === "submission" ? "submission_json" : "settings_json"];
127
136
  const value = parseJson(payload, "lifecycle");
128
- const id = kind === "schema" ? JSON.stringify([row.form_id, row.form_version]) : String(row.response_id);
137
+ const id = kind === "schema" ? JSON.stringify([row.form_id, row.form_version]) : kind === "submission" ? String(row.response_id) : formId;
129
138
  records.push({ kind, id, value: { row, value } });
130
139
  }
131
140
  }
@@ -136,14 +145,15 @@ function createSqliteStorage(options) {
136
145
  throw new TypeError("Invalid lifecycle resource.");
137
146
  const row = resource.value.row;
138
147
  const schema = resource.kind === "schema";
139
- const table = schema ? schemasTable : responsesTable;
140
- const payloadColumn = schema ? "schema_json" : "submission_json";
148
+ const settings = resource.kind === "runtimeSettings";
149
+ const table = schema ? schemasTable : settings ? runtimeSettingsTable : responsesTable;
150
+ const payloadColumn = schema ? "schema_json" : settings ? "settings_json" : "submission_json";
141
151
  const payload = row[payloadColumn];
142
- const params = schema ? [row.form_id, row.form_version] : [row.response_id, row.form_id];
143
- const identity = schema ? `form_id = ${marker(1)} AND form_version = ${marker(2)}` : `response_id = ${marker(1)} AND form_id = ${marker(2)}`;
152
+ const params = schema ? [row.form_id, row.form_version] : settings ? [row.form_id] : [row.response_id, row.form_id];
153
+ const identity = schema ? `form_id = ${marker(1)} AND form_version = ${marker(2)}` : settings ? `form_id = ${marker(1)}` : `response_id = ${marker(1)} AND form_id = ${marker(2)}`;
144
154
  params.push(typeof payload === "string" ? payload : JSON.stringify(payload));
145
155
  const deleted = await query(
146
- `DELETE FROM ${table} WHERE ${identity} AND ${payloadColumn} = ${marker(3)} RETURNING ${schema ? "form_id" : "response_id"}`,
156
+ `DELETE FROM ${table} WHERE ${identity} AND ${payloadColumn} = ${marker(params.length)} RETURNING ${schema ? "form_id" : settings ? "form_id" : "response_id"}`,
147
157
  params
148
158
  );
149
159
  if (deleted.length === 0) throw new Error("Deletion revision conflict or record disappeared.");
@@ -190,6 +200,26 @@ function createSqliteStorage(options) {
190
200
  );
191
201
  return row === void 0 ? null : parseSchemaRow(row, 0, options.schemaValidation);
192
202
  },
203
+ async getRuntimeSettings(formId) {
204
+ await ensureReady();
205
+ const row = await options.db.get(
206
+ `SELECT settings_json FROM ${runtimeSettingsTable} WHERE form_id = ?`,
207
+ [formId]
208
+ );
209
+ if (row === void 0) return null;
210
+ const settings = parseJson(row.settings_json, "runtime settings");
211
+ (0, import_core.assertValidFormRuntimeSettings)(settings);
212
+ return cloneJson(settings);
213
+ },
214
+ async saveRuntimeSettings(settings) {
215
+ await ensureReady();
216
+ (0, import_core.assertValidFormRuntimeSettings)(settings);
217
+ await options.db.run(
218
+ `INSERT INTO ${runtimeSettingsTable} (form_id, settings_json) VALUES (?, ?)
219
+ ON CONFLICT (form_id) DO UPDATE SET settings_json = excluded.settings_json`,
220
+ [settings.formId, JSON.stringify(settings)]
221
+ );
222
+ },
193
223
  async listSchemas() {
194
224
  await ensureReady();
195
225
  const rows = await options.db.all(
@@ -235,8 +265,8 @@ function createSqliteStorage(options) {
235
265
  return { status: "conflict", submissionId: stored.id, payloadHash, existingPayloadHash };
236
266
  }
237
267
  const row = await db.get(
238
- `SELECT COUNT(*) AS count FROM ${responsesTable} WHERE form_id = ? AND form_version = ?`,
239
- [stored.formId, stored.formVersion]
268
+ `SELECT COUNT(*) AS count FROM ${responsesTable} WHERE form_id = ?${saveOptions.responseLimitScope === "form" ? "" : " AND form_version = ?"}`,
269
+ saveOptions.responseLimitScope === "form" ? [stored.formId] : [stored.formId, stored.formVersion]
240
270
  );
241
271
  if (Number(row?.count ?? 0) >= maxResponses) return { status: "limit_reached" };
242
272
  await db.run(
@@ -305,6 +335,7 @@ function createSqliteStorage(options) {
305
335
  await ensureReady();
306
336
  await options.db.run(`DELETE FROM ${responsesTable}`);
307
337
  await options.db.run(`DELETE FROM ${schemasTable}`);
338
+ await options.db.run(`DELETE FROM ${runtimeSettingsTable}`);
308
339
  }
309
340
  };
310
341
  }
package/dist/index.d.cts CHANGED
@@ -12,6 +12,7 @@ interface SqliteStorageOptions {
12
12
  readonly db: SqliteExecutor;
13
13
  readonly schemasTable?: string;
14
14
  readonly responsesTable?: string;
15
+ readonly runtimeSettingsTable?: string;
15
16
  readonly autoMigrate?: boolean;
16
17
  }
17
18
  declare function createSqliteStorage(options: SqliteStorageOptions): FormStorageAdapter;
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ interface SqliteStorageOptions {
12
12
  readonly db: SqliteExecutor;
13
13
  readonly schemasTable?: string;
14
14
  readonly responsesTable?: string;
15
+ readonly runtimeSettingsTable?: string;
15
16
  readonly autoMigrate?: boolean;
16
17
  }
17
18
  declare function createSqliteStorage(options: SqliteStorageOptions): FormStorageAdapter;
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/index.ts
2
2
  import {
3
+ assertValidFormRuntimeSettings,
3
4
  assertValidFormSchema,
4
5
  createFormLifecycleAdapter,
5
6
  hashFormSubmissionPayload,
@@ -62,6 +63,11 @@ function createSqliteStorage(options) {
62
63
  }
63
64
  const schemasTable = identifier(options.schemasTable, "form_schemas", "schemasTable");
64
65
  const responsesTableName = options.responsesTable ?? "form_responses";
66
+ const runtimeSettingsTable = identifier(
67
+ options.runtimeSettingsTable,
68
+ "form_runtime_settings",
69
+ "runtimeSettingsTable"
70
+ );
65
71
  const responsesTable = identifier(responsesTableName, "form_responses", "responsesTable");
66
72
  const responsesIndex = identifier(`${responsesTableName}_lookup_idx`, "form_responses_lookup_idx", "responsesTable");
67
73
  let migration;
@@ -83,6 +89,10 @@ function createSqliteStorage(options) {
83
89
  submitted_at TEXT NOT NULL,
84
90
  submission_json TEXT NOT NULL
85
91
  )`);
92
+ await options.db.run(`CREATE TABLE IF NOT EXISTS ${runtimeSettingsTable} (
93
+ form_id TEXT PRIMARY KEY,
94
+ settings_json TEXT NOT NULL
95
+ )`);
86
96
  await options.db.run(
87
97
  `CREATE INDEX IF NOT EXISTS ${responsesIndex} ON ${responsesTable} (form_id, submitted_at, response_id)`
88
98
  );
@@ -95,18 +105,18 @@ function createSqliteStorage(options) {
95
105
  };
96
106
  const marker = (_index) => "?";
97
107
  return {
98
- resources: ["schema", "submission"],
108
+ resources: ["schema", "submission", "runtimeSettings"],
99
109
  async list(formId) {
100
110
  await ensureReady();
101
111
  const records = [];
102
- for (const kind of ["schema", "submission"]) {
103
- const table = kind === "schema" ? schemasTable : responsesTable;
112
+ for (const kind of ["schema", "submission", "runtimeSettings"]) {
113
+ const table = kind === "schema" ? schemasTable : kind === "submission" ? responsesTable : runtimeSettingsTable;
104
114
  const rows = await query(`SELECT * FROM ${table} WHERE form_id = ${marker(1)}`, [formId]);
105
115
  for (const row of rows) {
106
116
  if (!isRecord(row)) throw new TypeError("Invalid lifecycle row.");
107
- const payload = row[kind === "schema" ? "schema_json" : "submission_json"];
117
+ const payload = row[kind === "schema" ? "schema_json" : kind === "submission" ? "submission_json" : "settings_json"];
108
118
  const value = parseJson(payload, "lifecycle");
109
- const id = kind === "schema" ? JSON.stringify([row.form_id, row.form_version]) : String(row.response_id);
119
+ const id = kind === "schema" ? JSON.stringify([row.form_id, row.form_version]) : kind === "submission" ? String(row.response_id) : formId;
110
120
  records.push({ kind, id, value: { row, value } });
111
121
  }
112
122
  }
@@ -117,14 +127,15 @@ function createSqliteStorage(options) {
117
127
  throw new TypeError("Invalid lifecycle resource.");
118
128
  const row = resource.value.row;
119
129
  const schema = resource.kind === "schema";
120
- const table = schema ? schemasTable : responsesTable;
121
- const payloadColumn = schema ? "schema_json" : "submission_json";
130
+ const settings = resource.kind === "runtimeSettings";
131
+ const table = schema ? schemasTable : settings ? runtimeSettingsTable : responsesTable;
132
+ const payloadColumn = schema ? "schema_json" : settings ? "settings_json" : "submission_json";
122
133
  const payload = row[payloadColumn];
123
- const params = schema ? [row.form_id, row.form_version] : [row.response_id, row.form_id];
124
- const identity = schema ? `form_id = ${marker(1)} AND form_version = ${marker(2)}` : `response_id = ${marker(1)} AND form_id = ${marker(2)}`;
134
+ const params = schema ? [row.form_id, row.form_version] : settings ? [row.form_id] : [row.response_id, row.form_id];
135
+ const identity = schema ? `form_id = ${marker(1)} AND form_version = ${marker(2)}` : settings ? `form_id = ${marker(1)}` : `response_id = ${marker(1)} AND form_id = ${marker(2)}`;
125
136
  params.push(typeof payload === "string" ? payload : JSON.stringify(payload));
126
137
  const deleted = await query(
127
- `DELETE FROM ${table} WHERE ${identity} AND ${payloadColumn} = ${marker(3)} RETURNING ${schema ? "form_id" : "response_id"}`,
138
+ `DELETE FROM ${table} WHERE ${identity} AND ${payloadColumn} = ${marker(params.length)} RETURNING ${schema ? "form_id" : settings ? "form_id" : "response_id"}`,
128
139
  params
129
140
  );
130
141
  if (deleted.length === 0) throw new Error("Deletion revision conflict or record disappeared.");
@@ -171,6 +182,26 @@ function createSqliteStorage(options) {
171
182
  );
172
183
  return row === void 0 ? null : parseSchemaRow(row, 0, options.schemaValidation);
173
184
  },
185
+ async getRuntimeSettings(formId) {
186
+ await ensureReady();
187
+ const row = await options.db.get(
188
+ `SELECT settings_json FROM ${runtimeSettingsTable} WHERE form_id = ?`,
189
+ [formId]
190
+ );
191
+ if (row === void 0) return null;
192
+ const settings = parseJson(row.settings_json, "runtime settings");
193
+ assertValidFormRuntimeSettings(settings);
194
+ return cloneJson(settings);
195
+ },
196
+ async saveRuntimeSettings(settings) {
197
+ await ensureReady();
198
+ assertValidFormRuntimeSettings(settings);
199
+ await options.db.run(
200
+ `INSERT INTO ${runtimeSettingsTable} (form_id, settings_json) VALUES (?, ?)
201
+ ON CONFLICT (form_id) DO UPDATE SET settings_json = excluded.settings_json`,
202
+ [settings.formId, JSON.stringify(settings)]
203
+ );
204
+ },
174
205
  async listSchemas() {
175
206
  await ensureReady();
176
207
  const rows = await options.db.all(
@@ -216,8 +247,8 @@ function createSqliteStorage(options) {
216
247
  return { status: "conflict", submissionId: stored.id, payloadHash, existingPayloadHash };
217
248
  }
218
249
  const row = await db.get(
219
- `SELECT COUNT(*) AS count FROM ${responsesTable} WHERE form_id = ? AND form_version = ?`,
220
- [stored.formId, stored.formVersion]
250
+ `SELECT COUNT(*) AS count FROM ${responsesTable} WHERE form_id = ?${saveOptions.responseLimitScope === "form" ? "" : " AND form_version = ?"}`,
251
+ saveOptions.responseLimitScope === "form" ? [stored.formId] : [stored.formId, stored.formVersion]
221
252
  );
222
253
  if (Number(row?.count ?? 0) >= maxResponses) return { status: "limit_reached" };
223
254
  await db.run(
@@ -286,6 +317,7 @@ function createSqliteStorage(options) {
286
317
  await ensureReady();
287
318
  await options.db.run(`DELETE FROM ${responsesTable}`);
288
319
  await options.db.run(`DELETE FROM ${schemasTable}`);
320
+ await options.db.run(`DELETE FROM ${runtimeSettingsTable}`);
289
321
  }
290
322
  };
291
323
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/storage-sqlite",
3
- "version": "8.1.7",
3
+ "version": "8.2.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -40,10 +40,10 @@
40
40
  "typescript"
41
41
  ],
42
42
  "dependencies": {
43
- "@form-engine-ts/core": "8.1.7"
43
+ "@form-engine-ts/core": "8.2.1"
44
44
  },
45
45
  "devDependencies": {
46
- "@form-engine-ts/storage": "8.1.7"
46
+ "@form-engine-ts/storage": "8.2.1"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsup src/index.ts --format esm,cjs --dts --clean --external @form-engine-ts/core",