tina4-nodejs 3.1.2 → 3.4.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.
Files changed (34) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +30 -2
  3. package/package.json +1 -1
  4. package/packages/cli/src/bin.ts +13 -1
  5. package/packages/cli/src/commands/migrate.ts +19 -5
  6. package/packages/cli/src/commands/migrateCreate.ts +29 -28
  7. package/packages/cli/src/commands/migrateRollback.ts +59 -0
  8. package/packages/cli/src/commands/migrateStatus.ts +62 -0
  9. package/packages/core/public/js/tina4-dev-admin.min.js +1 -1
  10. package/packages/core/src/auth.ts +44 -10
  11. package/packages/core/src/devAdmin.ts +14 -16
  12. package/packages/core/src/errorOverlay.ts +17 -15
  13. package/packages/core/src/index.ts +9 -2
  14. package/packages/core/src/queue.ts +127 -25
  15. package/packages/core/src/queueBackends/mongoBackend.ts +223 -0
  16. package/packages/core/src/request.ts +3 -3
  17. package/packages/core/src/routeDiscovery.ts +2 -1
  18. package/packages/core/src/router.ts +90 -51
  19. package/packages/core/src/server.ts +62 -4
  20. package/packages/core/src/session.ts +17 -1
  21. package/packages/core/src/sessionHandlers/databaseHandler.ts +134 -0
  22. package/packages/core/src/sessionHandlers/redisHandler.ts +230 -0
  23. package/packages/core/src/types.ts +12 -6
  24. package/packages/core/src/websocket.ts +11 -2
  25. package/packages/core/src/websocketConnection.ts +4 -2
  26. package/packages/frond/src/engine.ts +66 -1
  27. package/packages/orm/src/autoCrud.ts +17 -12
  28. package/packages/orm/src/baseModel.ts +99 -21
  29. package/packages/orm/src/database.ts +197 -69
  30. package/packages/orm/src/databaseResult.ts +207 -0
  31. package/packages/orm/src/index.ts +6 -3
  32. package/packages/orm/src/migration.ts +296 -71
  33. package/packages/orm/src/model.ts +1 -0
  34. package/packages/orm/src/types.ts +1 -0
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Tina4 Redis Session Handler — Redis via `redis` npm package (optional dependency).
3
+ *
4
+ * Provides a session handler backed by the official `redis` npm package,
5
+ * complementing the built-in raw-TCP RedisSessionHandler in session.ts.
6
+ *
7
+ * This handler uses synchronous child-process execution (same pattern as
8
+ * valkeyHandler.ts) so it fits the synchronous SessionHandler interface
9
+ * without requiring async refactoring.
10
+ *
11
+ * Configure via environment variables:
12
+ * TINA4_SESSION_REDIS_HOST (default: "127.0.0.1")
13
+ * TINA4_SESSION_REDIS_PORT (default: 6379)
14
+ * TINA4_SESSION_REDIS_URL (optional — full redis:// URL, overrides host/port)
15
+ * TINA4_SESSION_REDIS_PASSWORD (optional)
16
+ * TINA4_SESSION_REDIS_PREFIX (default: "tina4:session:")
17
+ * TINA4_SESSION_REDIS_DB (default: 0)
18
+ */
19
+ import { execFileSync } from "node:child_process";
20
+ import type { SessionHandler } from "../session.js";
21
+
22
+ interface SessionData {
23
+ _created: number;
24
+ _accessed: number;
25
+ [key: string]: unknown;
26
+ }
27
+
28
+ export interface RedisNpmSessionConfig {
29
+ host?: string;
30
+ port?: number;
31
+ url?: string;
32
+ password?: string;
33
+ prefix?: string;
34
+ db?: number;
35
+ }
36
+
37
+ /**
38
+ * Redis session handler using the `redis` npm package.
39
+ *
40
+ * Falls back to raw TCP (RESP protocol) if the `redis` package is not
41
+ * installed, matching the approach used by the Valkey handler.
42
+ *
43
+ * Stores session data as JSON strings with Redis TTL for automatic expiry.
44
+ */
45
+ export class RedisNpmSessionHandler implements SessionHandler {
46
+ private host: string;
47
+ private port: number;
48
+ private url: string;
49
+ private password: string;
50
+ private prefix: string;
51
+ private db: number;
52
+
53
+ constructor(config?: RedisNpmSessionConfig) {
54
+ this.url = config?.url
55
+ ?? process.env.TINA4_SESSION_REDIS_URL
56
+ ?? "";
57
+ this.host = config?.host
58
+ ?? process.env.TINA4_SESSION_REDIS_HOST
59
+ ?? "127.0.0.1";
60
+ this.port = config?.port
61
+ ?? (process.env.TINA4_SESSION_REDIS_PORT
62
+ ? parseInt(process.env.TINA4_SESSION_REDIS_PORT, 10)
63
+ : 6379);
64
+ this.password = config?.password
65
+ ?? process.env.TINA4_SESSION_REDIS_PASSWORD
66
+ ?? "";
67
+ this.prefix = config?.prefix
68
+ ?? process.env.TINA4_SESSION_REDIS_PREFIX
69
+ ?? "tina4:session:";
70
+ this.db = config?.db
71
+ ?? (process.env.TINA4_SESSION_REDIS_DB
72
+ ? parseInt(process.env.TINA4_SESSION_REDIS_DB, 10)
73
+ : 0);
74
+ }
75
+
76
+ /**
77
+ * Execute a Redis command synchronously via a short-lived child process.
78
+ *
79
+ * Attempts to use the `redis` npm package first. If unavailable, falls
80
+ * back to raw TCP (RESP protocol) — same as valkeyHandler.ts.
81
+ */
82
+ private execSync(args: string[]): string {
83
+ const script = `
84
+ const net = require("node:net");
85
+ const host = ${JSON.stringify(this.url || this.host)};
86
+ const port = ${this.port};
87
+ const password = ${JSON.stringify(this.password)};
88
+ const db = ${this.db};
89
+ const useUrl = ${JSON.stringify(!!this.url)};
90
+ const url = ${JSON.stringify(this.url)};
91
+ const args = ${JSON.stringify(args)};
92
+
93
+ // Try the redis npm package first
94
+ let redisAvailable = false;
95
+ try {
96
+ require.resolve("redis");
97
+ redisAvailable = true;
98
+ } catch {}
99
+
100
+ if (redisAvailable) {
101
+ const redis = require("redis");
102
+ (async () => {
103
+ try {
104
+ const clientOpts = useUrl
105
+ ? { url }
106
+ : { socket: { host, port }, password: password || undefined, database: db };
107
+ const client = redis.createClient(clientOpts);
108
+ client.on("error", () => {});
109
+ await client.connect();
110
+
111
+ const cmd = args[0].toUpperCase();
112
+ let result;
113
+ if (cmd === "GET") {
114
+ result = await client.get(args[1]);
115
+ } else if (cmd === "SET") {
116
+ result = await client.set(args[1], args[2]);
117
+ } else if (cmd === "SETEX") {
118
+ result = await client.setEx(args[1], parseInt(args[2], 10), args[3]);
119
+ } else if (cmd === "DEL") {
120
+ result = await client.del(args[1]);
121
+ }
122
+ await client.quit();
123
+
124
+ if (result === null || result === undefined) {
125
+ process.stdout.write("__NULL__");
126
+ } else {
127
+ process.stdout.write(String(result));
128
+ }
129
+ } catch (err) {
130
+ process.stderr.write(err.message);
131
+ process.exit(1);
132
+ }
133
+ })();
134
+ } else {
135
+ // Fallback: raw TCP RESP protocol (no redis package needed)
136
+ const actualHost = useUrl ? (() => {
137
+ try { const u = new URL(url); return u.hostname || "127.0.0.1"; } catch { return "127.0.0.1"; }
138
+ })() : host;
139
+ const actualPort = useUrl ? (() => {
140
+ try { const u = new URL(url); return parseInt(u.port, 10) || 6379; } catch { return 6379; }
141
+ })() : port;
142
+
143
+ function buildCommand(a) {
144
+ let cmd = "*" + a.length + "\\r\\n";
145
+ for (const s of a) cmd += "$" + Buffer.byteLength(s) + "\\r\\n" + s + "\\r\\n";
146
+ return cmd;
147
+ }
148
+
149
+ const sock = net.createConnection({ host: actualHost, port: actualPort }, () => {
150
+ let commands = "";
151
+ if (password) commands += buildCommand(["AUTH", password]);
152
+ if (db !== 0) commands += buildCommand(["SELECT", String(db)]);
153
+ commands += buildCommand(args);
154
+ sock.write(commands);
155
+ });
156
+
157
+ let buffer = Buffer.alloc(0);
158
+ sock.on("data", (chunk) => {
159
+ buffer = Buffer.concat([buffer, chunk]);
160
+ });
161
+ sock.on("end", () => {
162
+ const lines = buffer.toString("utf-8").split("\\r\\n");
163
+ let responses = [];
164
+ let i = 0;
165
+ while (i < lines.length) {
166
+ const line = lines[i];
167
+ if (!line) { i++; continue; }
168
+ if (line.startsWith("+") || line.startsWith("-") || line.startsWith(":")) {
169
+ responses.push(line);
170
+ i++;
171
+ } else if (line.startsWith("$")) {
172
+ const len = parseInt(line.slice(1), 10);
173
+ if (len === -1) { responses.push(null); i++; }
174
+ else { responses.push(lines[i+1] || ""); i += 2; }
175
+ } else { i++; }
176
+ }
177
+ const result = responses[responses.length - 1];
178
+ if (result === null) process.stdout.write("__NULL__");
179
+ else if (typeof result === "string" && result.startsWith("-")) process.stdout.write("__ERR__" + result);
180
+ else process.stdout.write(String(result ?? "__NULL__"));
181
+ });
182
+ sock.on("error", (err) => {
183
+ process.stderr.write(err.message);
184
+ process.exit(1);
185
+ });
186
+ setTimeout(() => { sock.destroy(); process.exit(1); }, 3000);
187
+ }
188
+ `;
189
+
190
+ try {
191
+ const result = execFileSync(process.execPath, ["-e", script], {
192
+ encoding: "utf-8",
193
+ timeout: 5000,
194
+ stdio: ["pipe", "pipe", "pipe"],
195
+ });
196
+ if (result === "__NULL__") return "";
197
+ if (result.startsWith("__ERR__")) return "";
198
+ return result;
199
+ } catch {
200
+ return "";
201
+ }
202
+ }
203
+
204
+ private key(sessionId: string): string {
205
+ return `${this.prefix}${sessionId}`;
206
+ }
207
+
208
+ read(sessionId: string): SessionData | null {
209
+ const raw = this.execSync(["GET", this.key(sessionId)]);
210
+ if (!raw) return null;
211
+ try {
212
+ return JSON.parse(raw) as SessionData;
213
+ } catch {
214
+ return null;
215
+ }
216
+ }
217
+
218
+ write(sessionId: string, data: SessionData, ttl: number): void {
219
+ const json = JSON.stringify(data);
220
+ if (ttl > 0) {
221
+ this.execSync(["SETEX", this.key(sessionId), String(ttl), json]);
222
+ } else {
223
+ this.execSync(["SET", this.key(sessionId), json]);
224
+ }
225
+ }
226
+
227
+ destroy(sessionId: string): void {
228
+ this.execSync(["DEL", this.key(sessionId)]);
229
+ }
230
+ }
@@ -3,8 +3,8 @@ import type { IncomingMessage, ServerResponse } from "node:http";
3
3
  export interface UploadedFile {
4
4
  fieldName: string;
5
5
  filename: string;
6
- contentType: string;
7
- data: Buffer;
6
+ type: string;
7
+ content: Buffer;
8
8
  size: number;
9
9
  }
10
10
 
@@ -69,6 +69,10 @@ export interface RouteDefinition {
69
69
  middlewares?: Middleware[];
70
70
  /** Template file to render when handler returns a plain object */
71
71
  template?: string;
72
+ /** Whether this route requires bearer-token authentication */
73
+ secure?: boolean;
74
+ /** Whether this route's response should be cached */
75
+ cached?: boolean;
72
76
  }
73
77
 
74
78
  export interface RouteMeta {
@@ -103,12 +107,14 @@ export type Middleware = (
103
107
 
104
108
  /**
105
109
  * Handler for WebSocket routes.
106
- * conna connection-like object with send/close methods.
107
- * messagethe incoming text or binary message.
110
+ * connection — object with send/broadcast/close methods and route params.
111
+ * eventone of "open", "message", or "close".
112
+ * data — the incoming text message (only present for "message" events).
108
113
  */
109
114
  export type WebSocketRouteHandler = (
110
- conn: { id: string; send: (data: string) => void; close: () => void },
111
- message: string | Buffer,
115
+ connection: import("./websocketConnection.js").WebSocketConnection,
116
+ event: "open" | "message" | "close",
117
+ data: string,
112
118
  ) => void | Promise<void>;
113
119
 
114
120
  export interface WebSocketRouteDefinition {
@@ -52,6 +52,8 @@ export interface WebSocketClient {
52
52
  ip: string;
53
53
  connectedAt: number;
54
54
  closed: boolean;
55
+ /** The URL path this client connected on (e.g. "/chat", "/notifications"). */
56
+ path: string;
55
57
  }
56
58
 
57
59
  type EventHandler = (...args: unknown[]) => void;
@@ -185,14 +187,20 @@ export class WebSocketServer {
185
187
 
186
188
  /**
187
189
  * Broadcast a message to all connected clients.
190
+ *
191
+ * When `path` is provided, only clients connected on that specific path
192
+ * receive the message (matching PHP's WebSocket::broadcast behaviour).
193
+ * When `path` is omitted/undefined, all clients receive the message
194
+ * (backward compatible).
188
195
  */
189
- broadcast(message: string, excludeIds?: string[]): void {
196
+ broadcast(message: string, excludeIds?: string[], path?: string): void {
190
197
  const frame = buildFrame(OP_TEXT, Buffer.from(message, "utf-8"));
191
198
  const exclude = new Set(excludeIds ?? []);
192
199
 
193
200
  for (const [id, client] of this.clients) {
194
201
  if (exclude.has(id)) continue;
195
202
  if (client.closed) continue;
203
+ if (path !== undefined && client.path !== path) continue;
196
204
  try {
197
205
  client.socket.write(frame);
198
206
  } catch {
@@ -310,7 +318,7 @@ export class WebSocketServer {
310
318
 
311
319
  socket.write(response);
312
320
 
313
- // Create client
321
+ // Create client — track the URL path for path-scoped broadcast
314
322
  const clientId = randomUUID().slice(0, 8);
315
323
  const client: WebSocketClient = {
316
324
  id: clientId,
@@ -318,6 +326,7 @@ export class WebSocketServer {
318
326
  ip: (socket.remoteAddress ?? "unknown"),
319
327
  connectedAt: Date.now(),
320
328
  closed: false,
329
+ path: req.url ?? "/",
321
330
  };
322
331
 
323
332
  this.clients.set(clientId, client);
@@ -5,12 +5,14 @@
5
5
  export interface WebSocketConnection {
6
6
  /** Unique connection identifier */
7
7
  id: string;
8
- /** Send a message to this connection */
8
+ /** Send a message to this connection only */
9
9
  send(message: string): void;
10
- /** Broadcast a message to all other connections on the same path */
10
+ /** Broadcast a message to all connections on the same path (path-scoped) */
11
11
  broadcast(message: string): void;
12
12
  /** Close this connection */
13
13
  close(): void;
14
14
  /** The WebSocket route path this connection is on */
15
15
  path: string;
16
+ /** Route parameters extracted from `{param}` segments in the path */
17
+ params: Record<string, string>;
16
18
  }
@@ -757,8 +757,16 @@ const BUILTIN_FILTERS: Record<string, FilterFn> = {
757
757
  slug: (v) => String(v).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""),
758
758
  md5: (v) => createHash("md5").update(String(v)).digest("hex"),
759
759
  sha256: (v) => createHash("sha256").update(String(v)).digest("hex"),
760
- base64_encode: (v) => Buffer.from(String(v)).toString("base64"),
760
+ base64_encode: (v) => Buffer.isBuffer(v) ? v.toString("base64") : Buffer.from(String(v)).toString("base64"),
761
761
  base64_decode: (v) => Buffer.from(String(v), "base64").toString("utf-8"),
762
+ data_uri: (v) => {
763
+ if (v && typeof v === "object" && "content" in v) {
764
+ const ct = (v as any).type ?? "application/octet-stream";
765
+ const raw = Buffer.isBuffer((v as any).content) ? (v as any).content : Buffer.from(String((v as any).content));
766
+ return `data:${ct};base64,${raw.toString("base64")}`;
767
+ }
768
+ return String(v);
769
+ },
762
770
  url_encode: (v) => encodeURIComponent(String(v)),
763
771
  format: (v, ...args) => {
764
772
  let s = String(v);
@@ -1155,6 +1163,63 @@ export class Frond {
1155
1163
  }
1156
1164
 
1157
1165
  private evalVar(expr: string, context: Record<string, unknown>): unknown {
1166
+ // Check for top-level ternary BEFORE splitting filters so that
1167
+ // expressions like ``products|length != 1 ? "s" : ""`` work correctly.
1168
+ const ternaryIdx = findTernary(expr);
1169
+ if (ternaryIdx !== -1) {
1170
+ const condPart = expr.slice(0, ternaryIdx).trim();
1171
+ const rest = expr.slice(ternaryIdx + 1);
1172
+ const colonIdx = findColon(rest);
1173
+ if (colonIdx !== -1) {
1174
+ const truePart = rest.slice(0, colonIdx).trim();
1175
+ const falsePart = rest.slice(colonIdx + 1).trim();
1176
+ const cond = this.evalVarRaw(condPart, context);
1177
+ return cond ? this.evalVar(truePart, context) : this.evalVar(falsePart, context);
1178
+ }
1179
+ }
1180
+
1181
+ return this.evalVarInner(expr, context);
1182
+ }
1183
+
1184
+ private evalVarRaw(expr: string, context: Record<string, unknown>): unknown {
1185
+ const [varName, filters] = parseFilterChain(expr);
1186
+ let value = evalExpr(varName, context);
1187
+ for (const [fname, args] of filters) {
1188
+ if (fname === "raw" || fname === "safe") continue;
1189
+ const fn = this.filters[fname];
1190
+ if (fn) {
1191
+ value = fn(value, ...args);
1192
+ } else {
1193
+ // The filter name may include a trailing comparison operator,
1194
+ // e.g. "length != 1". Extract the real filter name and the
1195
+ // comparison suffix, apply the filter, then evaluate the comparison.
1196
+ const m = fname.match(/^(\w+)\s*(!=|==|>=|<=|>|<)\s*(.+)$/);
1197
+ if (m) {
1198
+ const realFilter = m[1];
1199
+ const op = m[2];
1200
+ const rightExpr = m[3].trim();
1201
+ const fn2 = this.filters[realFilter];
1202
+ if (fn2) {
1203
+ value = fn2(value, ...args);
1204
+ }
1205
+ const right = evalExpr(rightExpr, context);
1206
+ switch (op) {
1207
+ case "!=": value = value !== right; break;
1208
+ case "==": value = value === right; break;
1209
+ case ">=": value = (value as number) >= (right as number); break;
1210
+ case "<=": value = (value as number) <= (right as number); break;
1211
+ case ">": value = (value as number) > (right as number); break;
1212
+ case "<": value = (value as number) < (right as number); break;
1213
+ }
1214
+ } else {
1215
+ value = evalExpr(fname, context);
1216
+ }
1217
+ }
1218
+ }
1219
+ return value;
1220
+ }
1221
+
1222
+ private evalVarInner(expr: string, context: Record<string, unknown>): unknown {
1158
1223
  const [varName, filters] = parseFilterChain(expr);
1159
1224
 
1160
1225
  // Sandbox: check variable access
@@ -8,11 +8,16 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
8
8
  const routes: RouteDefinition[] = [];
9
9
 
10
10
  for (const { definition } of models) {
11
- const { tableName, fields, softDelete, tableFilter } = definition;
11
+ const { tableName, fields, softDelete, tableFilter, fieldMapping } = definition;
12
12
  const basePath = `/api/${tableName}`;
13
+ const mapping = fieldMapping ?? {};
13
14
 
14
- // Find primary key field
15
+ // Helper to get DB column name for a JS property name
16
+ const getDbCol = (prop: string): string => mapping[prop] ?? prop;
17
+
18
+ // Find primary key field (JS property name) and its DB column name
15
19
  const pkField = Object.entries(fields).find(([, def]) => def.primaryKey)?.[0] ?? "id";
20
+ const pkColumn = getDbCol(pkField);
16
21
 
17
22
  // Build extra WHERE conditions for soft delete and table filter
18
23
  const extraConditions: string[] = [];
@@ -65,7 +70,7 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
65
70
  },
66
71
  handler: async (req: Tina4Request, res: Tina4Response) => {
67
72
  const adapter = getAdapter();
68
- const conditions = [`"${pkField}" = ?`, ...extraConditions];
73
+ const conditions = [`"${pkColumn}" = ?`, ...extraConditions];
69
74
  const items = adapter.query(
70
75
  `SELECT * FROM "${tableName}" WHERE ${conditions.join(" AND ")}`,
71
76
  [req.params.id],
@@ -112,7 +117,7 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
112
117
  insertFields.push(["is_deleted", 0]);
113
118
  }
114
119
 
115
- const columns = insertFields.map(([k]) => `"${k}"`).join(", ");
120
+ const columns = insertFields.map(([k]) => `"${getDbCol(k)}"`).join(", ");
116
121
  const placeholders = insertFields.map(() => "?").join(", ");
117
122
  const values = insertFields.map(([, v]) => v);
118
123
 
@@ -123,7 +128,7 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
123
128
 
124
129
  const id = result.lastInsertRowid;
125
130
  const created = adapter.query(
126
- `SELECT * FROM "${tableName}" WHERE "${pkField}" = ?`,
131
+ `SELECT * FROM "${tableName}" WHERE "${pkColumn}" = ?`,
127
132
  [id],
128
133
  );
129
134
 
@@ -155,7 +160,7 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
155
160
  const adapter = getAdapter();
156
161
 
157
162
  // Check exists (respect soft delete)
158
- const conditions = [`"${pkField}" = ?`, ...extraConditions];
163
+ const conditions = [`"${pkColumn}" = ?`, ...extraConditions];
159
164
  const existing = adapter.query(
160
165
  `SELECT * FROM "${tableName}" WHERE ${conditions.join(" AND ")}`,
161
166
  [req.params.id],
@@ -172,16 +177,16 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
172
177
  return;
173
178
  }
174
179
 
175
- const setClause = updateFields.map(([k]) => `"${k}" = ?`).join(", ");
180
+ const setClause = updateFields.map(([k]) => `"${getDbCol(k)}" = ?`).join(", ");
176
181
  const values = [...updateFields.map(([, v]) => v), req.params.id];
177
182
 
178
183
  adapter.execute(
179
- `UPDATE "${tableName}" SET ${setClause} WHERE "${pkField}" = ?`,
184
+ `UPDATE "${tableName}" SET ${setClause} WHERE "${pkColumn}" = ?`,
180
185
  values,
181
186
  );
182
187
 
183
188
  const updated = adapter.query(
184
- `SELECT * FROM "${tableName}" WHERE "${pkField}" = ?`,
189
+ `SELECT * FROM "${tableName}" WHERE "${pkColumn}" = ?`,
185
190
  [req.params.id],
186
191
  );
187
192
 
@@ -200,7 +205,7 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
200
205
  handler: async (req: Tina4Request, res: Tina4Response) => {
201
206
  const adapter = getAdapter();
202
207
 
203
- const conditions = [`"${pkField}" = ?`, ...extraConditions];
208
+ const conditions = [`"${pkColumn}" = ?`, ...extraConditions];
204
209
  const existing = adapter.query(
205
210
  `SELECT * FROM "${tableName}" WHERE ${conditions.join(" AND ")}`,
206
211
  [req.params.id],
@@ -212,13 +217,13 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
212
217
 
213
218
  if (softDelete) {
214
219
  adapter.execute(
215
- `UPDATE "${tableName}" SET is_deleted = 1 WHERE "${pkField}" = ?`,
220
+ `UPDATE "${tableName}" SET is_deleted = 1 WHERE "${pkColumn}" = ?`,
216
221
  [req.params.id],
217
222
  );
218
223
  res.json({ message: "Deleted (soft)", data: existing[0] });
219
224
  } else {
220
225
  adapter.execute(
221
- `DELETE FROM "${tableName}" WHERE "${pkField}" = ?`,
226
+ `DELETE FROM "${tableName}" WHERE "${pkColumn}" = ?`,
222
227
  [req.params.id],
223
228
  );
224
229
  res.json({ message: "Deleted", data: existing[0] });