@metaobjectsdev/runtime-ts 0.7.0-rc.8 → 0.7.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.
- package/dist/hono/index.d.ts +44 -0
- package/dist/hono/index.d.ts.map +1 -0
- package/dist/hono/index.js +188 -0
- package/dist/hono/index.js.map +1 -0
- package/dist/hono/mount-read-only.d.ts +18 -0
- package/dist/hono/mount-read-only.d.ts.map +1 -0
- package/dist/hono/mount-read-only.js +151 -0
- package/dist/hono/mount-read-only.js.map +1 -0
- package/package.json +12 -2
- package/src/hono/index.ts +237 -0
- package/src/hono/mount-read-only.ts +189 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Hono, Context } from "hono";
|
|
2
|
+
import type { ZodTypeAny } from "zod";
|
|
3
|
+
import type { FilterAllowlist, SortAllowlist } from "../drizzle-fastify/filter-allowlist.js";
|
|
4
|
+
export type { FilterAllowlist, SortAllowlist, } from "../drizzle-fastify/filter-allowlist.js";
|
|
5
|
+
type AnyDrizzle = any;
|
|
6
|
+
type AnyTable = any;
|
|
7
|
+
type AnyHono = Hono<any, any, any>;
|
|
8
|
+
export type CrudVerb = "list" | "get" | "create" | "update" | "delete";
|
|
9
|
+
export interface CrudRoutesOptions {
|
|
10
|
+
/** Hono app instance. Bindings type is the consumer's — kept generic here. */
|
|
11
|
+
app: AnyHono;
|
|
12
|
+
/** REST resource path, e.g. "/subscribers". */
|
|
13
|
+
path: string;
|
|
14
|
+
/** User's Drizzle instance. */
|
|
15
|
+
db: AnyDrizzle;
|
|
16
|
+
/** Drizzle table const. Must expose an `id` column. */
|
|
17
|
+
table: AnyTable;
|
|
18
|
+
/** Zod schema for create payloads (typically `<Entity>InsertSchema`). */
|
|
19
|
+
insertSchema: ZodTypeAny;
|
|
20
|
+
/** Zod schema for update payloads (typically `<Entity>UpdateSchema`). */
|
|
21
|
+
updateSchema: ZodTypeAny;
|
|
22
|
+
/** Limit which verbs are mounted. Defaults to all five. */
|
|
23
|
+
expose?: readonly CrudVerb[];
|
|
24
|
+
/**
|
|
25
|
+
* HTTP method for the update verb. Defaults to "patch". Set to "put" to
|
|
26
|
+
* preserve a legacy API contract that already uses PUT for updates.
|
|
27
|
+
*/
|
|
28
|
+
updateMethod?: "patch" | "put";
|
|
29
|
+
filterAllowlist?: FilterAllowlist;
|
|
30
|
+
sortAllowlist?: SortAllowlist;
|
|
31
|
+
/** Dialect — required if filterAllowlist or sortAllowlist is set (for like/ilike dispatch). */
|
|
32
|
+
dialect?: "sqlite" | "postgres";
|
|
33
|
+
}
|
|
34
|
+
export declare function mountCrudRoutes(opts: CrudRoutesOptions): void;
|
|
35
|
+
type VerbOptions = Omit<CrudRoutesOptions, "expose">;
|
|
36
|
+
export declare function parseHonoFilterParams(c: Context): Record<string, unknown>;
|
|
37
|
+
export declare function mountListRoute(opts: VerbOptions): void;
|
|
38
|
+
export declare function mountGetRoute(opts: VerbOptions): void;
|
|
39
|
+
export declare function mountCreateRoute(opts: VerbOptions): void;
|
|
40
|
+
export declare function mountUpdateRoute(opts: VerbOptions): void;
|
|
41
|
+
export declare function mountDeleteRoute(opts: VerbOptions): void;
|
|
42
|
+
export declare function parseId(raw: string): number | string;
|
|
43
|
+
export { mountReadOnlyCrudRoutes, type MountReadOnlyOptions } from "./mount-read-only.js";
|
|
44
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hono/index.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAQtC,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACd,MAAM,wCAAwC,CAAC;AAChD,YAAY,EACV,eAAe,EACf,aAAa,GACd,MAAM,wCAAwC,CAAC;AAShD,KAAK,UAAU,GAAG,GAAG,CAAC;AAEtB,KAAK,QAAQ,GAAG,GAAG,CAAC;AAEpB,KAAK,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAEnC,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEvE,MAAM,WAAW,iBAAiB;IAChC,8EAA8E;IAC9E,GAAG,EAAE,OAAO,CAAC;IACb,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,EAAE,EAAE,UAAU,CAAC;IACf,uDAAuD;IACvD,KAAK,EAAE,QAAQ,CAAC;IAChB,yEAAyE;IACzE,YAAY,EAAE,UAAU,CAAC;IACzB,yEAAyE;IACzE,YAAY,EAAE,UAAU,CAAC;IACzB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,SAAS,QAAQ,EAAE,CAAC;IAC7B;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IAC/B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,aAAa,CAAC,EAAI,aAAa,CAAC;IAChC,+FAA+F;IAC/F,OAAO,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;CACjC;AAID,wBAAgB,eAAe,CAAC,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAO7D;AAED,KAAK,WAAW,GAAG,IAAI,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CAAC;AAWrD,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAKzE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CA+CtD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAUrD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAWxD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAsBxD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAaxD;AAcD,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAGpD;AAED,OAAO,EAAE,uBAAuB,EAAE,KAAK,oBAAoB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Drizzle-direct Hono adapter — Hono parallel of the drizzle-fastify mount
|
|
2
|
+
// helpers. Aimed at Workers / Bun / Node consumers running Hono on top of
|
|
3
|
+
// a Drizzle instance.
|
|
4
|
+
//
|
|
5
|
+
// Wire-format-identical to the Fastify flavor:
|
|
6
|
+
//
|
|
7
|
+
// GET {path} list with ?filter / ?sort / ?limit / ?offset / ?withCount=1
|
|
8
|
+
// GET {path}/:id findById, 404 if missing
|
|
9
|
+
// POST {path} create, 400 on Zod validation error, 201 on success
|
|
10
|
+
// PATCH {path}/:id update (default), 400 / 404 envelopes
|
|
11
|
+
// PUT {path}/:id update (when updateMethod === "put")
|
|
12
|
+
// DELETE {path}/:id delete, 204 on success, 404 if missing
|
|
13
|
+
//
|
|
14
|
+
// Filter / sort / withCount semantics are shared with drizzle-fastify via
|
|
15
|
+
// parseFilterParams and parseHonoFilterParams (qs.parse on the raw URL),
|
|
16
|
+
// so the two flavors emit byte-identical wire responses for the cross-port
|
|
17
|
+
// API-contract corpus.
|
|
18
|
+
import { eq, count, and } from "drizzle-orm";
|
|
19
|
+
import qs from "qs";
|
|
20
|
+
import { parseFilterParams, FilterParseError, } from "../drizzle-fastify/filter-parser.js";
|
|
21
|
+
import { isTruthyFlag } from "../drizzle-fastify/util.js";
|
|
22
|
+
const ALL_VERBS = ["list", "get", "create", "update", "delete"];
|
|
23
|
+
export function mountCrudRoutes(opts) {
|
|
24
|
+
const verbs = new Set(opts.expose ?? ALL_VERBS);
|
|
25
|
+
if (verbs.has("list"))
|
|
26
|
+
mountListRoute(opts);
|
|
27
|
+
if (verbs.has("get"))
|
|
28
|
+
mountGetRoute(opts);
|
|
29
|
+
if (verbs.has("create"))
|
|
30
|
+
mountCreateRoute(opts);
|
|
31
|
+
if (verbs.has("update"))
|
|
32
|
+
mountUpdateRoute(opts);
|
|
33
|
+
if (verbs.has("delete"))
|
|
34
|
+
mountDeleteRoute(opts);
|
|
35
|
+
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Hono-flavored qs.parse — analogue of Fastify's qs.parse(req.raw.url ...).
|
|
38
|
+
//
|
|
39
|
+
// Hono exposes the raw URL via c.req.url; we strip the prefix and feed the
|
|
40
|
+
// raw query string to qs so the bracketed filter notation (filter[a][eq]=...)
|
|
41
|
+
// is recognised. The plain c.req.query() helper drops bracket structure, so
|
|
42
|
+
// it can't replace this.
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
export function parseHonoFilterParams(c) {
|
|
45
|
+
const url = c.req.url;
|
|
46
|
+
const qIdx = url.indexOf("?");
|
|
47
|
+
const queryString = qIdx >= 0 ? url.slice(qIdx + 1) : "";
|
|
48
|
+
return qs.parse(queryString);
|
|
49
|
+
}
|
|
50
|
+
export function mountListRoute(opts) {
|
|
51
|
+
opts.app.get(opts.path, async (c) => {
|
|
52
|
+
try {
|
|
53
|
+
let q = opts.db.select().from(opts.table).$dynamic();
|
|
54
|
+
const qsParsed = parseHonoFilterParams(c);
|
|
55
|
+
const withCount = isTruthyFlag(qsParsed.withCount);
|
|
56
|
+
let where;
|
|
57
|
+
if (opts.filterAllowlist && opts.sortAllowlist) {
|
|
58
|
+
const parsed = parseFilterParams({
|
|
59
|
+
query: qsParsed,
|
|
60
|
+
table: opts.table,
|
|
61
|
+
allowlist: opts.filterAllowlist,
|
|
62
|
+
sortAllowlist: opts.sortAllowlist,
|
|
63
|
+
dialect: opts.dialect ?? "sqlite",
|
|
64
|
+
});
|
|
65
|
+
const combinedWhere = parsed.where && parsed.searchWhere
|
|
66
|
+
? and(parsed.where, parsed.searchWhere)
|
|
67
|
+
: (parsed.where ?? parsed.searchWhere);
|
|
68
|
+
if (combinedWhere) {
|
|
69
|
+
q = q.where(combinedWhere);
|
|
70
|
+
where = combinedWhere;
|
|
71
|
+
}
|
|
72
|
+
if (parsed.orderBy)
|
|
73
|
+
q = q.orderBy(...parsed.orderBy);
|
|
74
|
+
if (parsed.limit !== undefined)
|
|
75
|
+
q = q.limit(parsed.limit);
|
|
76
|
+
if (parsed.offset !== undefined)
|
|
77
|
+
q = q.offset(parsed.offset);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// No allowlists configured. Only limit/offset.
|
|
81
|
+
const flat = c.req.query();
|
|
82
|
+
if (flat.limit !== undefined)
|
|
83
|
+
q = q.limit(Number(flat.limit));
|
|
84
|
+
if (flat.offset !== undefined)
|
|
85
|
+
q = q.offset(Number(flat.offset));
|
|
86
|
+
}
|
|
87
|
+
const rows = await q.all();
|
|
88
|
+
if (!withCount)
|
|
89
|
+
return c.json(rows);
|
|
90
|
+
// Count query: same WHERE, no limit/offset/orderBy.
|
|
91
|
+
let cq = opts.db.select({ c: count() }).from(opts.table).$dynamic();
|
|
92
|
+
if (where)
|
|
93
|
+
cq = cq.where(where);
|
|
94
|
+
const countRow = (await cq.all())[0];
|
|
95
|
+
const total = countRow?.c ?? 0;
|
|
96
|
+
return c.json({ rows, total });
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
if (err instanceof FilterParseError) {
|
|
100
|
+
return c.json({ error: err.code, ...(err.details ?? {}) }, 400);
|
|
101
|
+
}
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
export function mountGetRoute(opts) {
|
|
107
|
+
opts.app.get(`${opts.path}/:id`, async (c) => {
|
|
108
|
+
const id = c.req.param("id") ?? "";
|
|
109
|
+
const row = await opts.db
|
|
110
|
+
.select()
|
|
111
|
+
.from(opts.table)
|
|
112
|
+
.where(eq(opts.table.id, parseId(id)))
|
|
113
|
+
.get();
|
|
114
|
+
return row ? c.json(row) : c.json({ error: "not_found" }, 404);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
export function mountCreateRoute(opts) {
|
|
118
|
+
opts.app.post(opts.path, async (c) => {
|
|
119
|
+
const body = await c.req.json().catch(() => undefined);
|
|
120
|
+
const parsed = opts.insertSchema.safeParse(body);
|
|
121
|
+
if (!parsed.success) {
|
|
122
|
+
return c.json({ error: "validation", issues: parsed.error.issues }, 400);
|
|
123
|
+
}
|
|
124
|
+
const result = await opts.db.insert(opts.table).values(parsed.data).returning();
|
|
125
|
+
const row = result[0];
|
|
126
|
+
return c.json(row, 201);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
export function mountUpdateRoute(opts) {
|
|
130
|
+
const handler = async (c) => {
|
|
131
|
+
const id = c.req.param("id") ?? "";
|
|
132
|
+
const body = await c.req.json().catch(() => undefined);
|
|
133
|
+
const parsed = opts.updateSchema.safeParse(body);
|
|
134
|
+
if (!parsed.success) {
|
|
135
|
+
return c.json({ error: "validation", issues: parsed.error.issues }, 400);
|
|
136
|
+
}
|
|
137
|
+
const result = await opts.db
|
|
138
|
+
.update(opts.table)
|
|
139
|
+
.set(parsed.data)
|
|
140
|
+
.where(eq(opts.table.id, parseId(id)))
|
|
141
|
+
.returning();
|
|
142
|
+
const row = result[0];
|
|
143
|
+
return row ? c.json(row) : c.json({ error: "not_found" }, 404);
|
|
144
|
+
};
|
|
145
|
+
const path = `${opts.path}/:id`;
|
|
146
|
+
if (opts.updateMethod === "put") {
|
|
147
|
+
opts.app.put(path, handler);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
opts.app.patch(path, handler);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function mountDeleteRoute(opts) {
|
|
154
|
+
opts.app.delete(`${opts.path}/:id`, async (c) => {
|
|
155
|
+
const id = c.req.param("id") ?? "";
|
|
156
|
+
const result = await opts.db
|
|
157
|
+
.delete(opts.table)
|
|
158
|
+
.where(eq(opts.table.id, parseId(id)));
|
|
159
|
+
const affected = extractRowCount(result);
|
|
160
|
+
if (affected > 0) {
|
|
161
|
+
// 204 No Content — body must be empty.
|
|
162
|
+
return c.body(null, 204);
|
|
163
|
+
}
|
|
164
|
+
return c.json({ error: "not_found" }, 404);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function extractRowCount(result) {
|
|
168
|
+
if (typeof result === "number")
|
|
169
|
+
return result;
|
|
170
|
+
if (Array.isArray(result))
|
|
171
|
+
return result.length;
|
|
172
|
+
if (result && typeof result === "object") {
|
|
173
|
+
const obj = result;
|
|
174
|
+
if (typeof obj.rowsAffected === "number")
|
|
175
|
+
return obj.rowsAffected;
|
|
176
|
+
if (typeof obj.rowsAffected === "bigint")
|
|
177
|
+
return Number(obj.rowsAffected);
|
|
178
|
+
if (typeof obj.rowCount === "number")
|
|
179
|
+
return obj.rowCount;
|
|
180
|
+
}
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
export function parseId(raw) {
|
|
184
|
+
const n = Number(raw);
|
|
185
|
+
return Number.isFinite(n) && raw.trim() !== "" ? n : raw;
|
|
186
|
+
}
|
|
187
|
+
export { mountReadOnlyCrudRoutes } from "./mount-read-only.js";
|
|
188
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hono/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Hono } from "hono";
|
|
2
|
+
import type { FilterAllowlist, SortAllowlist } from "../drizzle-fastify/filter-allowlist.js";
|
|
3
|
+
type AnyView = any;
|
|
4
|
+
type AnyHono = Hono<any, any, any>;
|
|
5
|
+
export interface MountReadOnlyOptions {
|
|
6
|
+
readonly app: AnyHono;
|
|
7
|
+
readonly path: string;
|
|
8
|
+
readonly db: any;
|
|
9
|
+
readonly view: AnyView;
|
|
10
|
+
readonly filterAllowlist: FilterAllowlist;
|
|
11
|
+
readonly sortAllowlist: SortAllowlist;
|
|
12
|
+
readonly dialect: "postgres" | "sqlite";
|
|
13
|
+
/** Override default ID column name (defaults to "id"). */
|
|
14
|
+
readonly idColumn?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function mountReadOnlyCrudRoutes(opts: MountReadOnlyOptions): void;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=mount-read-only.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mount-read-only.d.ts","sourceRoot":"","sources":["../../src/hono/mount-read-only.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAIjC,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACd,MAAM,wCAAwC,CAAC;AAIhD,KAAK,OAAO,GAAG,GAAG,CAAC;AAEnB,KAAK,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AASnC,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC;IACjB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAC;IAC1C,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,QAAQ,CAAC;IACxC,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AA6CD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,oBAAoB,GAAG,IAAI,CA0GxE"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Hono read-only mount — projection (view-backed) entities. GET list +
|
|
2
|
+
// GET :id only. POST/PATCH/DELETE return 405. Mirrors the drizzle-fastify
|
|
3
|
+
// equivalent so the cross-port API contract holds for projection endpoints.
|
|
4
|
+
import { sql, eq, and, count } from "drizzle-orm";
|
|
5
|
+
import qs from "qs";
|
|
6
|
+
import { parseFilterParams, FilterParseError } from "../drizzle-fastify/filter-parser.js";
|
|
7
|
+
import { isTruthyFlag } from "../drizzle-fastify/util.js";
|
|
8
|
+
/**
|
|
9
|
+
* Drizzle v0.45 stores view config under this well-known Symbol. Mirrors
|
|
10
|
+
* the same workaround the Fastify mount uses — accessing `view._` on a
|
|
11
|
+
* proxy-wrapped view throws.
|
|
12
|
+
*/
|
|
13
|
+
const VIEW_BASE_CONFIG = Symbol.for("drizzle:ViewBaseConfig");
|
|
14
|
+
function getViewConfig(view) {
|
|
15
|
+
try {
|
|
16
|
+
const cfg = view[VIEW_BASE_CONFIG];
|
|
17
|
+
if (cfg && typeof cfg === "object")
|
|
18
|
+
return cfg;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// ignore — proxy handler may throw on unexpected shapes
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
function resolveViewName(view) {
|
|
26
|
+
const cfg = getViewConfig(view);
|
|
27
|
+
if (cfg) {
|
|
28
|
+
if (typeof cfg["name"] === "string")
|
|
29
|
+
return cfg["name"];
|
|
30
|
+
}
|
|
31
|
+
const v = view;
|
|
32
|
+
return ((typeof v["__tableName"] === "string" ? v["__tableName"] : undefined) ??
|
|
33
|
+
(typeof v["_name"] === "string" ? v["_name"] : undefined));
|
|
34
|
+
}
|
|
35
|
+
function isEmptyColumnView(view) {
|
|
36
|
+
const cfg = getViewConfig(view);
|
|
37
|
+
if (cfg) {
|
|
38
|
+
const fields = cfg["selectedFields"];
|
|
39
|
+
return fields !== undefined && Object.keys(fields).length === 0;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
function snakeToCamel(s) {
|
|
44
|
+
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
45
|
+
}
|
|
46
|
+
function camelizeRow(row) {
|
|
47
|
+
const out = {};
|
|
48
|
+
for (const [k, v] of Object.entries(row)) {
|
|
49
|
+
out[snakeToCamel(k)] = v;
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
export function mountReadOnlyCrudRoutes(opts) {
|
|
54
|
+
const { app, path, db, view, filterAllowlist, sortAllowlist, dialect } = opts;
|
|
55
|
+
const idCol = opts.idColumn ?? "id";
|
|
56
|
+
const viewName = resolveViewName(view);
|
|
57
|
+
const useRawSql = isEmptyColumnView(view) && !!viewName;
|
|
58
|
+
// ── List ──────────────────────────────────────────────────────────────────
|
|
59
|
+
app.get(path, async (c) => {
|
|
60
|
+
try {
|
|
61
|
+
if (useRawSql) {
|
|
62
|
+
const url = c.req.url;
|
|
63
|
+
const qIdx = url.indexOf("?");
|
|
64
|
+
const queryString = qIdx >= 0 ? url.slice(qIdx + 1) : "";
|
|
65
|
+
const parsed = qs.parse(queryString);
|
|
66
|
+
const limitVal = Math.min(1000, Math.max(1, Number(typeof parsed["limit"] === "string" ? parsed["limit"] : 1000)));
|
|
67
|
+
const offsetVal = Math.max(0, Number(typeof parsed["offset"] === "string" ? parsed["offset"] : 0));
|
|
68
|
+
const withCount = isTruthyFlag(parsed["withCount"]);
|
|
69
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic raw result
|
|
70
|
+
const rows = (await db.all(sql.raw(`SELECT * FROM "${viewName}" LIMIT ${limitVal} OFFSET ${offsetVal}`)));
|
|
71
|
+
const camelRows = rows.map((r) => camelizeRow(r));
|
|
72
|
+
if (!withCount)
|
|
73
|
+
return c.json(camelRows);
|
|
74
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic raw result
|
|
75
|
+
const countRows = (await db.all(sql.raw(`SELECT COUNT(*) AS c FROM "${viewName}"`)));
|
|
76
|
+
const total = Number(countRows[0]?.c ?? 0);
|
|
77
|
+
return c.json({ rows: camelRows, total });
|
|
78
|
+
}
|
|
79
|
+
const url = c.req.url;
|
|
80
|
+
const qIdx = url.indexOf("?");
|
|
81
|
+
const queryString = qIdx >= 0 ? url.slice(qIdx + 1) : "";
|
|
82
|
+
const parsed = qs.parse(queryString);
|
|
83
|
+
const withCount = isTruthyFlag(parsed["withCount"]);
|
|
84
|
+
const result = parseFilterParams({
|
|
85
|
+
query: parsed,
|
|
86
|
+
table: view,
|
|
87
|
+
allowlist: filterAllowlist,
|
|
88
|
+
sortAllowlist,
|
|
89
|
+
dialect,
|
|
90
|
+
});
|
|
91
|
+
const combinedWhere = result.where && result.searchWhere
|
|
92
|
+
? and(result.where, result.searchWhere)
|
|
93
|
+
: (result.where ?? result.searchWhere);
|
|
94
|
+
let q = db.select().from(view);
|
|
95
|
+
if (combinedWhere)
|
|
96
|
+
q = q.where(combinedWhere);
|
|
97
|
+
if (result.orderBy)
|
|
98
|
+
q = q.orderBy(...result.orderBy);
|
|
99
|
+
if (result.limit !== undefined)
|
|
100
|
+
q = q.limit(result.limit);
|
|
101
|
+
if (result.offset !== undefined)
|
|
102
|
+
q = q.offset(result.offset);
|
|
103
|
+
const rows = await q.all();
|
|
104
|
+
if (!withCount)
|
|
105
|
+
return c.json(rows);
|
|
106
|
+
let cq = db.select({ c: count() }).from(view);
|
|
107
|
+
if (combinedWhere)
|
|
108
|
+
cq = cq.where(combinedWhere);
|
|
109
|
+
const countRow = (await cq.all())[0];
|
|
110
|
+
const total = countRow?.c ?? 0;
|
|
111
|
+
return c.json({ rows, total });
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
if (err instanceof FilterParseError) {
|
|
115
|
+
return c.json({ error: err.code, message: err.message }, 400);
|
|
116
|
+
}
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
// ── Get by ID ─────────────────────────────────────────────────────────────
|
|
121
|
+
app.get(`${path}/:id`, async (c) => {
|
|
122
|
+
const id = c.req.param("id") ?? "";
|
|
123
|
+
if (useRawSql) {
|
|
124
|
+
const numericId = Number(id);
|
|
125
|
+
if (!Number.isFinite(numericId)) {
|
|
126
|
+
return c.json({ error: "invalid_id" }, 400);
|
|
127
|
+
}
|
|
128
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic raw result
|
|
129
|
+
const rows = (await db.all(sql.raw(`SELECT * FROM "${viewName}" WHERE "${idCol}" = ${numericId} LIMIT 1`)));
|
|
130
|
+
const row = rows[0] ? camelizeRow(rows[0]) : undefined;
|
|
131
|
+
return row ? c.json(row) : c.json({ error: "not_found" }, 404);
|
|
132
|
+
}
|
|
133
|
+
// biome-ignore lint/suspicious/noExplicitAny: Drizzle view column ref
|
|
134
|
+
const colRef = view[idCol];
|
|
135
|
+
const row = await db
|
|
136
|
+
.select()
|
|
137
|
+
.from(view)
|
|
138
|
+
.where(colRef !== undefined ? eq(colRef, Number(id)) : undefined)
|
|
139
|
+
.get();
|
|
140
|
+
return row ? c.json(row) : c.json({ error: "not_found" }, 404);
|
|
141
|
+
});
|
|
142
|
+
// ── Mutations explicitly rejected (405) ───────────────────────────────────
|
|
143
|
+
const reject = (c) => c.json({ error: "method_not_allowed", message: `${c.req.method} is not supported on a projection (read-only).` }, 405);
|
|
144
|
+
// biome-ignore lint/suspicious/noExplicitAny: cross-version Hono typing
|
|
145
|
+
app.post(path, reject);
|
|
146
|
+
// biome-ignore lint/suspicious/noExplicitAny: cross-version Hono typing
|
|
147
|
+
app.patch(`${path}/:id`, reject);
|
|
148
|
+
// biome-ignore lint/suspicious/noExplicitAny: cross-version Hono typing
|
|
149
|
+
app.delete(`${path}/:id`, reject);
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=mount-read-only.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mount-read-only.js","sourceRoot":"","sources":["../../src/hono/mount-read-only.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metaobjectsdev/runtime-ts",
|
|
3
|
-
"version": "0.7.0
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Node-side runtime helpers for MetaObjects: Fastify route builders, Drizzle filter/sort integration, Kysely drivers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
"bun": "./src/drizzle-fastify/index.ts",
|
|
26
26
|
"types": "./dist/drizzle-fastify/index.d.ts",
|
|
27
27
|
"default": "./dist/drizzle-fastify/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./hono": {
|
|
30
|
+
"bun": "./src/hono/index.ts",
|
|
31
|
+
"types": "./dist/hono/index.d.ts",
|
|
32
|
+
"default": "./dist/hono/index.js"
|
|
28
33
|
}
|
|
29
34
|
},
|
|
30
35
|
"files": ["dist", "src", "README.md", "LICENSE"],
|
|
@@ -48,12 +53,13 @@
|
|
|
48
53
|
"access": "public"
|
|
49
54
|
},
|
|
50
55
|
"dependencies": {
|
|
51
|
-
"@metaobjectsdev/metadata": "0.7.0
|
|
56
|
+
"@metaobjectsdev/metadata": "0.7.0",
|
|
52
57
|
"qs": "^6.13.0"
|
|
53
58
|
},
|
|
54
59
|
"peerDependencies": {
|
|
55
60
|
"drizzle-orm": ">=0.36.0",
|
|
56
61
|
"fastify": ">=5.0.0",
|
|
62
|
+
"hono": ">=4.0.0",
|
|
57
63
|
"kysely": ">=0.27.0",
|
|
58
64
|
"zod": ">=3.23.0"
|
|
59
65
|
},
|
|
@@ -64,6 +70,9 @@
|
|
|
64
70
|
"fastify": {
|
|
65
71
|
"optional": true
|
|
66
72
|
},
|
|
73
|
+
"hono": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
67
76
|
"kysely": {
|
|
68
77
|
"optional": true
|
|
69
78
|
},
|
|
@@ -78,6 +87,7 @@
|
|
|
78
87
|
"bun-types": "latest",
|
|
79
88
|
"drizzle-orm": "^0.45.1",
|
|
80
89
|
"fastify": "^5.6.2",
|
|
90
|
+
"hono": "^4.6.0",
|
|
81
91
|
"jsdom": "^29.1.1",
|
|
82
92
|
"kysely": "^0.27.0",
|
|
83
93
|
"pg-mem": "^3.0.4",
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// Drizzle-direct Hono adapter — Hono parallel of the drizzle-fastify mount
|
|
2
|
+
// helpers. Aimed at Workers / Bun / Node consumers running Hono on top of
|
|
3
|
+
// a Drizzle instance.
|
|
4
|
+
//
|
|
5
|
+
// Wire-format-identical to the Fastify flavor:
|
|
6
|
+
//
|
|
7
|
+
// GET {path} list with ?filter / ?sort / ?limit / ?offset / ?withCount=1
|
|
8
|
+
// GET {path}/:id findById, 404 if missing
|
|
9
|
+
// POST {path} create, 400 on Zod validation error, 201 on success
|
|
10
|
+
// PATCH {path}/:id update (default), 400 / 404 envelopes
|
|
11
|
+
// PUT {path}/:id update (when updateMethod === "put")
|
|
12
|
+
// DELETE {path}/:id delete, 204 on success, 404 if missing
|
|
13
|
+
//
|
|
14
|
+
// Filter / sort / withCount semantics are shared with drizzle-fastify via
|
|
15
|
+
// parseFilterParams and parseHonoFilterParams (qs.parse on the raw URL),
|
|
16
|
+
// so the two flavors emit byte-identical wire responses for the cross-port
|
|
17
|
+
// API-contract corpus.
|
|
18
|
+
|
|
19
|
+
import type { Hono, Context } from "hono";
|
|
20
|
+
import type { ZodTypeAny } from "zod";
|
|
21
|
+
import { eq, count, and } from "drizzle-orm";
|
|
22
|
+
import qs from "qs";
|
|
23
|
+
import {
|
|
24
|
+
parseFilterParams,
|
|
25
|
+
FilterParseError,
|
|
26
|
+
type ParseFilterResult,
|
|
27
|
+
} from "../drizzle-fastify/filter-parser.js";
|
|
28
|
+
import type {
|
|
29
|
+
FilterAllowlist,
|
|
30
|
+
SortAllowlist,
|
|
31
|
+
} from "../drizzle-fastify/filter-allowlist.js";
|
|
32
|
+
export type {
|
|
33
|
+
FilterAllowlist,
|
|
34
|
+
SortAllowlist,
|
|
35
|
+
} from "../drizzle-fastify/filter-allowlist.js";
|
|
36
|
+
import { isTruthyFlag } from "../drizzle-fastify/util.js";
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Loose types — we don't bind to a specific Drizzle backend so the helper
|
|
40
|
+
// works across libsql / better-sqlite3 / D1 / pg / etc.
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic dispatch over user's Drizzle instance
|
|
44
|
+
type AnyDrizzle = any;
|
|
45
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic dispatch over user's Drizzle table
|
|
46
|
+
type AnyTable = any;
|
|
47
|
+
// biome-ignore lint/suspicious/noExplicitAny: generic Hono app — bindings/variables are the consumer's
|
|
48
|
+
type AnyHono = Hono<any, any, any>;
|
|
49
|
+
|
|
50
|
+
export type CrudVerb = "list" | "get" | "create" | "update" | "delete";
|
|
51
|
+
|
|
52
|
+
export interface CrudRoutesOptions {
|
|
53
|
+
/** Hono app instance. Bindings type is the consumer's — kept generic here. */
|
|
54
|
+
app: AnyHono;
|
|
55
|
+
/** REST resource path, e.g. "/subscribers". */
|
|
56
|
+
path: string;
|
|
57
|
+
/** User's Drizzle instance. */
|
|
58
|
+
db: AnyDrizzle;
|
|
59
|
+
/** Drizzle table const. Must expose an `id` column. */
|
|
60
|
+
table: AnyTable;
|
|
61
|
+
/** Zod schema for create payloads (typically `<Entity>InsertSchema`). */
|
|
62
|
+
insertSchema: ZodTypeAny;
|
|
63
|
+
/** Zod schema for update payloads (typically `<Entity>UpdateSchema`). */
|
|
64
|
+
updateSchema: ZodTypeAny;
|
|
65
|
+
/** Limit which verbs are mounted. Defaults to all five. */
|
|
66
|
+
expose?: readonly CrudVerb[];
|
|
67
|
+
/**
|
|
68
|
+
* HTTP method for the update verb. Defaults to "patch". Set to "put" to
|
|
69
|
+
* preserve a legacy API contract that already uses PUT for updates.
|
|
70
|
+
*/
|
|
71
|
+
updateMethod?: "patch" | "put";
|
|
72
|
+
filterAllowlist?: FilterAllowlist;
|
|
73
|
+
sortAllowlist?: SortAllowlist;
|
|
74
|
+
/** Dialect — required if filterAllowlist or sortAllowlist is set (for like/ilike dispatch). */
|
|
75
|
+
dialect?: "sqlite" | "postgres";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const ALL_VERBS: readonly CrudVerb[] = ["list", "get", "create", "update", "delete"];
|
|
79
|
+
|
|
80
|
+
export function mountCrudRoutes(opts: CrudRoutesOptions): void {
|
|
81
|
+
const verbs = new Set<CrudVerb>(opts.expose ?? ALL_VERBS);
|
|
82
|
+
if (verbs.has("list")) mountListRoute(opts);
|
|
83
|
+
if (verbs.has("get")) mountGetRoute(opts);
|
|
84
|
+
if (verbs.has("create")) mountCreateRoute(opts);
|
|
85
|
+
if (verbs.has("update")) mountUpdateRoute(opts);
|
|
86
|
+
if (verbs.has("delete")) mountDeleteRoute(opts);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type VerbOptions = Omit<CrudRoutesOptions, "expose">;
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Hono-flavored qs.parse — analogue of Fastify's qs.parse(req.raw.url ...).
|
|
93
|
+
//
|
|
94
|
+
// Hono exposes the raw URL via c.req.url; we strip the prefix and feed the
|
|
95
|
+
// raw query string to qs so the bracketed filter notation (filter[a][eq]=...)
|
|
96
|
+
// is recognised. The plain c.req.query() helper drops bracket structure, so
|
|
97
|
+
// it can't replace this.
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
export function parseHonoFilterParams(c: Context): Record<string, unknown> {
|
|
101
|
+
const url = c.req.url;
|
|
102
|
+
const qIdx = url.indexOf("?");
|
|
103
|
+
const queryString = qIdx >= 0 ? url.slice(qIdx + 1) : "";
|
|
104
|
+
return qs.parse(queryString) as Record<string, unknown>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function mountListRoute(opts: VerbOptions): void {
|
|
108
|
+
opts.app.get(opts.path, async (c) => {
|
|
109
|
+
try {
|
|
110
|
+
let q = opts.db.select().from(opts.table).$dynamic();
|
|
111
|
+
const qsParsed = parseHonoFilterParams(c);
|
|
112
|
+
const withCount = isTruthyFlag(qsParsed.withCount);
|
|
113
|
+
|
|
114
|
+
let where: ParseFilterResult["where"];
|
|
115
|
+
if (opts.filterAllowlist && opts.sortAllowlist) {
|
|
116
|
+
const parsed = parseFilterParams({
|
|
117
|
+
query: qsParsed,
|
|
118
|
+
table: opts.table,
|
|
119
|
+
allowlist: opts.filterAllowlist,
|
|
120
|
+
sortAllowlist: opts.sortAllowlist,
|
|
121
|
+
dialect: opts.dialect ?? "sqlite",
|
|
122
|
+
});
|
|
123
|
+
const combinedWhere = parsed.where && parsed.searchWhere
|
|
124
|
+
? and(parsed.where, parsed.searchWhere)
|
|
125
|
+
: (parsed.where ?? parsed.searchWhere);
|
|
126
|
+
if (combinedWhere) { q = q.where(combinedWhere); where = combinedWhere; }
|
|
127
|
+
if (parsed.orderBy) q = q.orderBy(...parsed.orderBy);
|
|
128
|
+
if (parsed.limit !== undefined) q = q.limit(parsed.limit);
|
|
129
|
+
if (parsed.offset !== undefined) q = q.offset(parsed.offset);
|
|
130
|
+
} else {
|
|
131
|
+
// No allowlists configured. Only limit/offset.
|
|
132
|
+
const flat = c.req.query();
|
|
133
|
+
if (flat.limit !== undefined) q = q.limit(Number(flat.limit));
|
|
134
|
+
if (flat.offset !== undefined) q = q.offset(Number(flat.offset));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const rows = await q.all();
|
|
138
|
+
|
|
139
|
+
if (!withCount) return c.json(rows);
|
|
140
|
+
|
|
141
|
+
// Count query: same WHERE, no limit/offset/orderBy.
|
|
142
|
+
let cq = opts.db.select({ c: count() }).from(opts.table).$dynamic();
|
|
143
|
+
if (where) cq = cq.where(where);
|
|
144
|
+
const countRow = (await cq.all())[0] as { c: number } | undefined;
|
|
145
|
+
const total = countRow?.c ?? 0;
|
|
146
|
+
return c.json({ rows, total });
|
|
147
|
+
} catch (err) {
|
|
148
|
+
if (err instanceof FilterParseError) {
|
|
149
|
+
return c.json({ error: err.code, ...(err.details ?? {}) }, 400);
|
|
150
|
+
}
|
|
151
|
+
throw err;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function mountGetRoute(opts: VerbOptions): void {
|
|
157
|
+
opts.app.get(`${opts.path}/:id`, async (c) => {
|
|
158
|
+
const id = c.req.param("id") ?? "";
|
|
159
|
+
const row = await opts.db
|
|
160
|
+
.select()
|
|
161
|
+
.from(opts.table)
|
|
162
|
+
.where(eq(opts.table.id, parseId(id)))
|
|
163
|
+
.get();
|
|
164
|
+
return row ? c.json(row) : c.json({ error: "not_found" }, 404);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function mountCreateRoute(opts: VerbOptions): void {
|
|
169
|
+
opts.app.post(opts.path, async (c) => {
|
|
170
|
+
const body = await c.req.json().catch(() => undefined);
|
|
171
|
+
const parsed = opts.insertSchema.safeParse(body);
|
|
172
|
+
if (!parsed.success) {
|
|
173
|
+
return c.json({ error: "validation", issues: parsed.error.issues }, 400);
|
|
174
|
+
}
|
|
175
|
+
const result = await opts.db.insert(opts.table).values(parsed.data).returning();
|
|
176
|
+
const row = (result as unknown[])[0];
|
|
177
|
+
return c.json(row, 201);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function mountUpdateRoute(opts: VerbOptions): void {
|
|
182
|
+
const handler = async (c: Context) => {
|
|
183
|
+
const id = c.req.param("id") ?? "";
|
|
184
|
+
const body = await c.req.json().catch(() => undefined);
|
|
185
|
+
const parsed = opts.updateSchema.safeParse(body);
|
|
186
|
+
if (!parsed.success) {
|
|
187
|
+
return c.json({ error: "validation", issues: parsed.error.issues }, 400);
|
|
188
|
+
}
|
|
189
|
+
const result = await opts.db
|
|
190
|
+
.update(opts.table)
|
|
191
|
+
.set(parsed.data)
|
|
192
|
+
.where(eq(opts.table.id, parseId(id)))
|
|
193
|
+
.returning();
|
|
194
|
+
const row = (result as unknown[])[0];
|
|
195
|
+
return row ? c.json(row) : c.json({ error: "not_found" }, 404);
|
|
196
|
+
};
|
|
197
|
+
const path = `${opts.path}/:id`;
|
|
198
|
+
if (opts.updateMethod === "put") {
|
|
199
|
+
opts.app.put(path, handler);
|
|
200
|
+
} else {
|
|
201
|
+
opts.app.patch(path, handler);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function mountDeleteRoute(opts: VerbOptions): void {
|
|
206
|
+
opts.app.delete(`${opts.path}/:id`, async (c) => {
|
|
207
|
+
const id = c.req.param("id") ?? "";
|
|
208
|
+
const result = await opts.db
|
|
209
|
+
.delete(opts.table)
|
|
210
|
+
.where(eq(opts.table.id, parseId(id)));
|
|
211
|
+
const affected = extractRowCount(result);
|
|
212
|
+
if (affected > 0) {
|
|
213
|
+
// 204 No Content — body must be empty.
|
|
214
|
+
return c.body(null, 204);
|
|
215
|
+
}
|
|
216
|
+
return c.json({ error: "not_found" }, 404);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function extractRowCount(result: unknown): number {
|
|
221
|
+
if (typeof result === "number") return result;
|
|
222
|
+
if (Array.isArray(result)) return result.length;
|
|
223
|
+
if (result && typeof result === "object") {
|
|
224
|
+
const obj = result as { rowsAffected?: number | bigint; rowCount?: number };
|
|
225
|
+
if (typeof obj.rowsAffected === "number") return obj.rowsAffected;
|
|
226
|
+
if (typeof obj.rowsAffected === "bigint") return Number(obj.rowsAffected);
|
|
227
|
+
if (typeof obj.rowCount === "number") return obj.rowCount;
|
|
228
|
+
}
|
|
229
|
+
return 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
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
|
+
export { mountReadOnlyCrudRoutes, type MountReadOnlyOptions } from "./mount-read-only.js";
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// Hono read-only mount — projection (view-backed) entities. GET list +
|
|
2
|
+
// GET :id only. POST/PATCH/DELETE return 405. Mirrors the drizzle-fastify
|
|
3
|
+
// equivalent so the cross-port API contract holds for projection endpoints.
|
|
4
|
+
|
|
5
|
+
import type { Hono } from "hono";
|
|
6
|
+
import { sql, eq, and, count } from "drizzle-orm";
|
|
7
|
+
import qs from "qs";
|
|
8
|
+
import { parseFilterParams, FilterParseError } from "../drizzle-fastify/filter-parser.js";
|
|
9
|
+
import type {
|
|
10
|
+
FilterAllowlist,
|
|
11
|
+
SortAllowlist,
|
|
12
|
+
} from "../drizzle-fastify/filter-allowlist.js";
|
|
13
|
+
import { isTruthyFlag } from "../drizzle-fastify/util.js";
|
|
14
|
+
|
|
15
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic dispatch over user-supplied views
|
|
16
|
+
type AnyView = any;
|
|
17
|
+
// biome-ignore lint/suspicious/noExplicitAny: generic Hono app
|
|
18
|
+
type AnyHono = Hono<any, any, any>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Drizzle v0.45 stores view config under this well-known Symbol. Mirrors
|
|
22
|
+
* the same workaround the Fastify mount uses — accessing `view._` on a
|
|
23
|
+
* proxy-wrapped view throws.
|
|
24
|
+
*/
|
|
25
|
+
const VIEW_BASE_CONFIG = Symbol.for("drizzle:ViewBaseConfig");
|
|
26
|
+
|
|
27
|
+
export interface MountReadOnlyOptions {
|
|
28
|
+
readonly app: AnyHono;
|
|
29
|
+
readonly path: string;
|
|
30
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic Drizzle client
|
|
31
|
+
readonly db: any;
|
|
32
|
+
readonly view: AnyView;
|
|
33
|
+
readonly filterAllowlist: FilterAllowlist;
|
|
34
|
+
readonly sortAllowlist: SortAllowlist;
|
|
35
|
+
readonly dialect: "postgres" | "sqlite";
|
|
36
|
+
/** Override default ID column name (defaults to "id"). */
|
|
37
|
+
readonly idColumn?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getViewConfig(view: AnyView): Record<string, unknown> | undefined {
|
|
41
|
+
try {
|
|
42
|
+
const cfg = (view as Record<symbol, unknown>)[VIEW_BASE_CONFIG];
|
|
43
|
+
if (cfg && typeof cfg === "object") return cfg as Record<string, unknown>;
|
|
44
|
+
} catch {
|
|
45
|
+
// ignore — proxy handler may throw on unexpected shapes
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveViewName(view: AnyView): string | undefined {
|
|
51
|
+
const cfg = getViewConfig(view);
|
|
52
|
+
if (cfg) {
|
|
53
|
+
if (typeof cfg["name"] === "string") return cfg["name"] as string;
|
|
54
|
+
}
|
|
55
|
+
const v = view as Record<string, unknown>;
|
|
56
|
+
return (
|
|
57
|
+
(typeof v["__tableName"] === "string" ? v["__tableName"] as string : undefined) ??
|
|
58
|
+
(typeof v["_name"] === "string" ? v["_name"] as string : undefined)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isEmptyColumnView(view: AnyView): boolean {
|
|
63
|
+
const cfg = getViewConfig(view);
|
|
64
|
+
if (cfg) {
|
|
65
|
+
const fields = cfg["selectedFields"] as Record<string, unknown> | undefined;
|
|
66
|
+
return fields !== undefined && Object.keys(fields).length === 0;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function snakeToCamel(s: string): string {
|
|
72
|
+
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function camelizeRow(row: Record<string, unknown>): Record<string, unknown> {
|
|
76
|
+
const out: Record<string, unknown> = {};
|
|
77
|
+
for (const [k, v] of Object.entries(row)) {
|
|
78
|
+
out[snakeToCamel(k)] = v;
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function mountReadOnlyCrudRoutes(opts: MountReadOnlyOptions): void {
|
|
84
|
+
const { app, path, db, view, filterAllowlist, sortAllowlist, dialect } = opts;
|
|
85
|
+
const idCol = opts.idColumn ?? "id";
|
|
86
|
+
|
|
87
|
+
const viewName = resolveViewName(view);
|
|
88
|
+
const useRawSql = isEmptyColumnView(view) && !!viewName;
|
|
89
|
+
|
|
90
|
+
// ── List ──────────────────────────────────────────────────────────────────
|
|
91
|
+
app.get(path, async (c) => {
|
|
92
|
+
try {
|
|
93
|
+
if (useRawSql) {
|
|
94
|
+
const url = c.req.url;
|
|
95
|
+
const qIdx = url.indexOf("?");
|
|
96
|
+
const queryString = qIdx >= 0 ? url.slice(qIdx + 1) : "";
|
|
97
|
+
const parsed = qs.parse(queryString) as Record<string, unknown>;
|
|
98
|
+
const limitVal = Math.min(
|
|
99
|
+
1000,
|
|
100
|
+
Math.max(1, Number(typeof parsed["limit"] === "string" ? parsed["limit"] : 1000)),
|
|
101
|
+
);
|
|
102
|
+
const offsetVal = Math.max(
|
|
103
|
+
0,
|
|
104
|
+
Number(typeof parsed["offset"] === "string" ? parsed["offset"] : 0),
|
|
105
|
+
);
|
|
106
|
+
const withCount = isTruthyFlag(parsed["withCount"]);
|
|
107
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic raw result
|
|
108
|
+
const rows = (await db.all(sql.raw(`SELECT * FROM "${viewName}" LIMIT ${limitVal} OFFSET ${offsetVal}`))) as any[];
|
|
109
|
+
const camelRows = rows.map((r: Record<string, unknown>) => camelizeRow(r));
|
|
110
|
+
if (!withCount) return c.json(camelRows);
|
|
111
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic raw result
|
|
112
|
+
const countRows = (await db.all(sql.raw(`SELECT COUNT(*) AS c FROM "${viewName}"`))) as any[];
|
|
113
|
+
const total: number = Number(countRows[0]?.c ?? 0);
|
|
114
|
+
return c.json({ rows: camelRows, total });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const url = c.req.url;
|
|
118
|
+
const qIdx = url.indexOf("?");
|
|
119
|
+
const queryString = qIdx >= 0 ? url.slice(qIdx + 1) : "";
|
|
120
|
+
const parsed = qs.parse(queryString) as Record<string, unknown>;
|
|
121
|
+
const withCount = isTruthyFlag(parsed["withCount"]);
|
|
122
|
+
const result = parseFilterParams({
|
|
123
|
+
query: parsed,
|
|
124
|
+
table: view,
|
|
125
|
+
allowlist: filterAllowlist,
|
|
126
|
+
sortAllowlist,
|
|
127
|
+
dialect,
|
|
128
|
+
});
|
|
129
|
+
const combinedWhere = result.where && result.searchWhere
|
|
130
|
+
? and(result.where, result.searchWhere)
|
|
131
|
+
: (result.where ?? result.searchWhere);
|
|
132
|
+
let q = db.select().from(view);
|
|
133
|
+
if (combinedWhere) q = q.where(combinedWhere);
|
|
134
|
+
if (result.orderBy) q = q.orderBy(...result.orderBy);
|
|
135
|
+
if (result.limit !== undefined) q = q.limit(result.limit);
|
|
136
|
+
if (result.offset !== undefined) q = q.offset(result.offset);
|
|
137
|
+
const rows = await q.all();
|
|
138
|
+
|
|
139
|
+
if (!withCount) return c.json(rows);
|
|
140
|
+
|
|
141
|
+
let cq = db.select({ c: count() }).from(view);
|
|
142
|
+
if (combinedWhere) cq = cq.where(combinedWhere);
|
|
143
|
+
const countRow = (await cq.all())[0] as { c: number } | undefined;
|
|
144
|
+
const total = countRow?.c ?? 0;
|
|
145
|
+
return c.json({ rows, total });
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (err instanceof FilterParseError) {
|
|
148
|
+
return c.json({ error: err.code, message: err.message }, 400);
|
|
149
|
+
}
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ── Get by ID ─────────────────────────────────────────────────────────────
|
|
155
|
+
app.get(`${path}/:id`, async (c) => {
|
|
156
|
+
const id = c.req.param("id") ?? "";
|
|
157
|
+
if (useRawSql) {
|
|
158
|
+
const numericId = Number(id);
|
|
159
|
+
if (!Number.isFinite(numericId)) {
|
|
160
|
+
return c.json({ error: "invalid_id" }, 400);
|
|
161
|
+
}
|
|
162
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic raw result
|
|
163
|
+
const rows = (await db.all(sql.raw(`SELECT * FROM "${viewName}" WHERE "${idCol}" = ${numericId} LIMIT 1`))) as any[];
|
|
164
|
+
const row = rows[0] ? camelizeRow(rows[0]) : undefined;
|
|
165
|
+
return row ? c.json(row) : c.json({ error: "not_found" }, 404);
|
|
166
|
+
}
|
|
167
|
+
// biome-ignore lint/suspicious/noExplicitAny: Drizzle view column ref
|
|
168
|
+
const colRef = (view as any)[idCol];
|
|
169
|
+
const row = await db
|
|
170
|
+
.select()
|
|
171
|
+
.from(view)
|
|
172
|
+
.where(colRef !== undefined ? eq(colRef, Number(id)) : undefined)
|
|
173
|
+
.get();
|
|
174
|
+
return row ? c.json(row) : c.json({ error: "not_found" }, 404);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ── Mutations explicitly rejected (405) ───────────────────────────────────
|
|
178
|
+
const reject = (c: { req: { method: string }; json: (body: unknown, status: number) => unknown }) =>
|
|
179
|
+
c.json(
|
|
180
|
+
{ error: "method_not_allowed", message: `${c.req.method} is not supported on a projection (read-only).` },
|
|
181
|
+
405,
|
|
182
|
+
);
|
|
183
|
+
// biome-ignore lint/suspicious/noExplicitAny: cross-version Hono typing
|
|
184
|
+
app.post(path, reject as any);
|
|
185
|
+
// biome-ignore lint/suspicious/noExplicitAny: cross-version Hono typing
|
|
186
|
+
app.patch(`${path}/:id`, reject as any);
|
|
187
|
+
// biome-ignore lint/suspicious/noExplicitAny: cross-version Hono typing
|
|
188
|
+
app.delete(`${path}/:id`, reject as any);
|
|
189
|
+
}
|