@metaobjectsdev/runtime-ts 0.15.20 → 0.15.21
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/dist/drizzle-fastify/index.d.ts +1 -1
- package/dist/drizzle-fastify/index.d.ts.map +1 -1
- package/dist/drizzle-fastify/index.js +26 -5
- package/dist/drizzle-fastify/index.js.map +1 -1
- package/dist/drizzle-fastify/mount-m2m.d.ts.map +1 -1
- package/dist/drizzle-fastify/mount-m2m.js +13 -6
- package/dist/drizzle-fastify/mount-m2m.js.map +1 -1
- package/dist/drizzle-fastify/mount-read-only.d.ts.map +1 -1
- package/dist/drizzle-fastify/mount-read-only.js +8 -7
- package/dist/drizzle-fastify/mount-read-only.js.map +1 -1
- package/dist/drizzle-fastify/util.d.ts +31 -1
- package/dist/drizzle-fastify/util.d.ts.map +1 -1
- package/dist/drizzle-fastify/util.js +43 -1
- package/dist/drizzle-fastify/util.js.map +1 -1
- package/dist/fastify/index.d.ts +1 -6
- package/dist/fastify/index.d.ts.map +1 -1
- package/dist/fastify/index.js +13 -11
- package/dist/fastify/index.js.map +1 -1
- package/dist/hono/index.d.ts +1 -1
- package/dist/hono/index.d.ts.map +1 -1
- package/dist/hono/index.js +25 -8
- package/dist/hono/index.js.map +1 -1
- package/dist/hono/mount-read-only.d.ts.map +1 -1
- package/dist/hono/mount-read-only.js +8 -7
- package/dist/hono/mount-read-only.js.map +1 -1
- package/dist/object-manager.d.ts +8 -0
- package/dist/object-manager.d.ts.map +1 -1
- package/dist/object-manager.js +22 -4
- package/dist/object-manager.js.map +1 -1
- package/package.json +3 -3
- package/src/drizzle-fastify/index.ts +26 -6
- package/src/drizzle-fastify/mount-m2m.ts +16 -7
- package/src/drizzle-fastify/mount-read-only.ts +8 -7
- package/src/drizzle-fastify/util.ts +44 -1
- package/src/fastify/index.ts +13 -11
- package/src/hono/index.ts +22 -10
- package/src/hono/mount-read-only.ts +8 -7
- package/src/object-manager.ts +21 -4
package/src/hono/index.ts
CHANGED
|
@@ -33,7 +33,10 @@ export type {
|
|
|
33
33
|
FilterAllowlist,
|
|
34
34
|
SortAllowlist,
|
|
35
35
|
} from "../drizzle-fastify/filter-allowlist.js";
|
|
36
|
-
import { isTruthyFlag } from "../drizzle-fastify/util.js";
|
|
36
|
+
import { isTruthyFlag, coerceIdForColumn } from "../drizzle-fastify/util.js";
|
|
37
|
+
// Back-compat re-export — the unsafe local copy was consolidated onto the one
|
|
38
|
+
// shared (deprecated) helper so the three adapters can't silently diverge.
|
|
39
|
+
export { parseId } from "../drizzle-fastify/util.js";
|
|
37
40
|
|
|
38
41
|
// ---------------------------------------------------------------------------
|
|
39
42
|
// Loose types — we don't bind to a specific Drizzle backend so the helper
|
|
@@ -156,10 +159,14 @@ export function mountListRoute(opts: VerbOptions): void {
|
|
|
156
159
|
export function mountGetRoute(opts: VerbOptions): void {
|
|
157
160
|
opts.app.get(`${opts.path}/:id`, async (c) => {
|
|
158
161
|
const id = c.req.param("id") ?? "";
|
|
162
|
+
// Compare against the PK's real type — a numeric-LOOKING id on a TEXT pk
|
|
163
|
+
// must stay a string ('0123' ≠ '123'), or affinity matches the WRONG row.
|
|
164
|
+
const idValue = coerceIdForColumn(opts.table.id, id);
|
|
165
|
+
if (idValue === undefined) return c.json({ error: "invalid_id" }, 400);
|
|
159
166
|
const row = await opts.db
|
|
160
167
|
.select()
|
|
161
168
|
.from(opts.table)
|
|
162
|
-
.where(eq(opts.table.id,
|
|
169
|
+
.where(eq(opts.table.id, idValue))
|
|
163
170
|
.get();
|
|
164
171
|
return row ? c.json(row) : c.json({ error: "not_found" }, 404);
|
|
165
172
|
});
|
|
@@ -186,10 +193,14 @@ export function mountUpdateRoute(opts: VerbOptions): void {
|
|
|
186
193
|
if (!parsed.success) {
|
|
187
194
|
return c.json({ error: "validation", issues: parsed.error.issues }, 400);
|
|
188
195
|
}
|
|
196
|
+
// Compare against the PK's real type (see mountGetRoute) — a numeric-
|
|
197
|
+
// LOOKING id on a TEXT pk would otherwise UPDATE the wrong row.
|
|
198
|
+
const idValue = coerceIdForColumn(opts.table.id, id);
|
|
199
|
+
if (idValue === undefined) return c.json({ error: "invalid_id" }, 400);
|
|
189
200
|
const result = await opts.db
|
|
190
201
|
.update(opts.table)
|
|
191
202
|
.set(parsed.data)
|
|
192
|
-
.where(eq(opts.table.id,
|
|
203
|
+
.where(eq(opts.table.id, idValue))
|
|
193
204
|
.returning();
|
|
194
205
|
const row = (result as unknown[])[0];
|
|
195
206
|
return row ? c.json(row) : c.json({ error: "not_found" }, 404);
|
|
@@ -205,9 +216,13 @@ export function mountUpdateRoute(opts: VerbOptions): void {
|
|
|
205
216
|
export function mountDeleteRoute(opts: VerbOptions): void {
|
|
206
217
|
opts.app.delete(`${opts.path}/:id`, async (c) => {
|
|
207
218
|
const id = c.req.param("id") ?? "";
|
|
219
|
+
// Compare against the PK's real type (see mountGetRoute) — a numeric-
|
|
220
|
+
// LOOKING id on a TEXT pk would otherwise DELETE the wrong row (data loss).
|
|
221
|
+
const idValue = coerceIdForColumn(opts.table.id, id);
|
|
222
|
+
if (idValue === undefined) return c.json({ error: "invalid_id" }, 400);
|
|
208
223
|
const result = await opts.db
|
|
209
224
|
.delete(opts.table)
|
|
210
|
-
.where(eq(opts.table.id,
|
|
225
|
+
.where(eq(opts.table.id, idValue));
|
|
211
226
|
const affected = extractRowCount(result);
|
|
212
227
|
if (affected > 0) {
|
|
213
228
|
// 204 No Content — body must be empty.
|
|
@@ -221,17 +236,14 @@ function extractRowCount(result: unknown): number {
|
|
|
221
236
|
if (typeof result === "number") return result;
|
|
222
237
|
if (Array.isArray(result)) return result.length;
|
|
223
238
|
if (result && typeof result === "object") {
|
|
224
|
-
const obj = result as { rowsAffected?: number | bigint; rowCount?: number };
|
|
239
|
+
const obj = result as { rowsAffected?: number | bigint; rowCount?: number; changes?: number };
|
|
225
240
|
if (typeof obj.rowsAffected === "number") return obj.rowsAffected;
|
|
226
241
|
if (typeof obj.rowsAffected === "bigint") return Number(obj.rowsAffected);
|
|
227
242
|
if (typeof obj.rowCount === "number") return obj.rowCount;
|
|
243
|
+
// bun:sqlite / better-sqlite3 run() result shape.
|
|
244
|
+
if (typeof obj.changes === "number") return obj.changes;
|
|
228
245
|
}
|
|
229
246
|
return 0;
|
|
230
247
|
}
|
|
231
248
|
|
|
232
|
-
export function parseId(raw: string): number | string {
|
|
233
|
-
const n = Number(raw);
|
|
234
|
-
return Number.isFinite(n) && raw.trim() !== "" ? n : raw;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
249
|
export { mountReadOnlyCrudRoutes, type MountReadOnlyOptions } from "./mount-read-only.js";
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
FilterAllowlist,
|
|
11
11
|
SortAllowlist,
|
|
12
12
|
} from "../drizzle-fastify/filter-allowlist.js";
|
|
13
|
-
import { isTruthyFlag } from "../drizzle-fastify/util.js";
|
|
13
|
+
import { isTruthyFlag, coerceIdForColumn, rawIdLiteral } from "../drizzle-fastify/util.js";
|
|
14
14
|
|
|
15
15
|
// biome-ignore lint/suspicious/noExplicitAny: dynamic dispatch over user-supplied views
|
|
16
16
|
type AnyView = any;
|
|
@@ -155,21 +155,22 @@ export function mountReadOnlyCrudRoutes(opts: MountReadOnlyOptions): void {
|
|
|
155
155
|
app.get(`${path}/:id`, async (c) => {
|
|
156
156
|
const id = c.req.param("id") ?? "";
|
|
157
157
|
if (useRawSql) {
|
|
158
|
-
const numericId = Number(id);
|
|
159
|
-
if (!Number.isFinite(numericId)) {
|
|
160
|
-
return c.json({ error: "invalid_id" }, 400);
|
|
161
|
-
}
|
|
162
158
|
// biome-ignore lint/suspicious/noExplicitAny: dynamic raw result
|
|
163
|
-
const rows = (await db.all(sql.raw(`SELECT * FROM "${viewName}" WHERE "${idCol}" = ${
|
|
159
|
+
const rows = (await db.all(sql.raw(`SELECT * FROM "${viewName}" WHERE "${idCol}" = ${rawIdLiteral(id)} LIMIT 1`))) as any[];
|
|
164
160
|
const row = rows[0] ? camelizeRow(rows[0]) : undefined;
|
|
165
161
|
return row ? c.json(row) : c.json({ error: "not_found" }, 404);
|
|
166
162
|
}
|
|
167
163
|
// biome-ignore lint/suspicious/noExplicitAny: Drizzle view column ref
|
|
168
164
|
const colRef = (view as any)[idCol];
|
|
165
|
+
// Compare against the PK's real type — a uuid/text key must NOT go through Number().
|
|
166
|
+
const idValue = coerceIdForColumn(colRef, id);
|
|
167
|
+
if (idValue === undefined) {
|
|
168
|
+
return c.json({ error: "invalid_id" }, 400);
|
|
169
|
+
}
|
|
169
170
|
const row = await db
|
|
170
171
|
.select()
|
|
171
172
|
.from(view)
|
|
172
|
-
.where(colRef !== undefined ? eq(colRef,
|
|
173
|
+
.where(colRef !== undefined ? eq(colRef, idValue) : undefined)
|
|
173
174
|
.get();
|
|
174
175
|
return row ? c.json(row) : c.json({ error: "not_found" }, 404);
|
|
175
176
|
});
|
package/src/object-manager.ts
CHANGED
|
@@ -96,10 +96,24 @@ export class ObjectManager {
|
|
|
96
96
|
return { $and: [filter, disc] } as Filter;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Coerce a raw string id (typically an HTTP path param) to the PK field's
|
|
101
|
+
* DECLARED type: a numeric-subtype pk gets Number(); a string/uuid pk keeps
|
|
102
|
+
* the string EXACTLY — '0123' and '123' are different keys, and Number()-ing
|
|
103
|
+
* them conflates the two (SQLite affinity then matches the WRONG row).
|
|
104
|
+
* Non-string ids and composite-pk entities pass through untouched.
|
|
105
|
+
*/
|
|
106
|
+
private coerceIdArg(entity: MetaData, id: unknown): unknown {
|
|
107
|
+
if (typeof id !== "string") return id;
|
|
108
|
+
const pkFields = resolvePkFields(entity);
|
|
109
|
+
if (pkFields.length !== 1) return id;
|
|
110
|
+
return coercePkValue(entity, pkFields[0]!, id);
|
|
111
|
+
}
|
|
112
|
+
|
|
99
113
|
async findById(entityName: string, id: unknown, opts: ReadOpts = {}): Promise<Row | null> {
|
|
100
114
|
const entity = this.requireEntity(entityName);
|
|
101
115
|
const pkField = resolvePkFields(entity)[0]!;
|
|
102
|
-
return this.findFirst(entityName, { [pkField]: id as string | number }, opts);
|
|
116
|
+
return this.findFirst(entityName, { [pkField]: this.coerceIdArg(entity, id) as string | number }, opts);
|
|
103
117
|
}
|
|
104
118
|
|
|
105
119
|
async findFirst(entityName: string, filter: Filter, opts: ReadOpts = {}): Promise<Row | null> {
|
|
@@ -204,7 +218,7 @@ export class ObjectManager {
|
|
|
204
218
|
|
|
205
219
|
const coerced = coerceRowOnWrite(entity, restricted, driver.dialect);
|
|
206
220
|
// FR-017 TPH: scope the by-id update to the subtype (cross-subtype → not found).
|
|
207
|
-
const spec = buildUpdateSpec(entity, coerced, id, this.columnNamingStrategy, this.tphScope(entity));
|
|
221
|
+
const spec = buildUpdateSpec(entity, coerced, this.coerceIdArg(entity, id), this.columnNamingStrategy, this.tphScope(entity));
|
|
208
222
|
const dbRow = await driver.update(spec);
|
|
209
223
|
if (dbRow === null) {
|
|
210
224
|
const mode = opts.ifMissing ?? DEFAULT_IF_MISSING;
|
|
@@ -218,7 +232,7 @@ export class ObjectManager {
|
|
|
218
232
|
const entity = this.requireEntity(entityName);
|
|
219
233
|
const driver = opts.tx ?? this.driver;
|
|
220
234
|
// FR-017 TPH: scope the by-id delete to the subtype (cross-subtype → not found).
|
|
221
|
-
const spec = buildDeleteSpec(entity, id, this.columnNamingStrategy, this.tphScope(entity));
|
|
235
|
+
const spec = buildDeleteSpec(entity, this.coerceIdArg(entity, id), this.columnNamingStrategy, this.tphScope(entity));
|
|
222
236
|
const n = await driver.delete(spec);
|
|
223
237
|
if (n === 0) {
|
|
224
238
|
const mode = opts.ifMissing ?? DEFAULT_IF_MISSING;
|
|
@@ -498,7 +512,10 @@ const NUMERIC_SUBTYPES = new Set([
|
|
|
498
512
|
FIELD_SUBTYPE_LONG, FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_FLOAT, FIELD_SUBTYPE_DECIMAL,
|
|
499
513
|
]);
|
|
500
514
|
|
|
501
|
-
//
|
|
515
|
+
// Coerce a raw string PK value to the field's declared type: numeric subtypes
|
|
516
|
+
// get Number(); everything else keeps the string exactly. Serves decodeRef
|
|
517
|
+
// (which always returns strings) AND raw HTTP path params via coerceIdArg —
|
|
518
|
+
// a string/uuid pk must never be Number()-ed ('0123' ≠ '123').
|
|
502
519
|
function coercePkValue(entity: MetaData, fieldName: string, rawValue: string): string | number {
|
|
503
520
|
// ADR-0039: resolving — the PK field may be inherited from a BaseEntity via extends.
|
|
504
521
|
const field = entity.children().find((c) => c.type === TYPE_FIELD && c.name === fieldName);
|