@metaobjectsdev/runtime-ts 0.7.0-rc.8 → 0.7.0-rc.9

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.
@@ -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-rc.8",
3
+ "version": "0.7.0-rc.9",
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-rc.8",
56
+ "@metaobjectsdev/metadata": "0.7.0-rc.9",
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
+ }