@polardbx/pxbaas 0.1.0-beta.2 → 0.1.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -9
- package/dist/app/preflight.d.ts +12 -1
- package/dist/app/preflight.d.ts.map +1 -1
- package/dist/app/preflight.js +28 -5
- package/dist/app/preflight.js.map +1 -1
- package/dist/app/spec-lint.d.ts.map +1 -1
- package/dist/app/spec-lint.js +16 -0
- package/dist/app/spec-lint.js.map +1 -1
- package/dist/commands/app/index.d.ts.map +1 -1
- package/dist/commands/app/index.js +34 -2
- package/dist/commands/app/index.js.map +1 -1
- package/dist/commands/db/index.d.ts.map +1 -1
- package/dist/commands/db/index.js +25 -12
- package/dist/commands/db/index.js.map +1 -1
- package/dist/commands/gen/index.d.ts.map +1 -1
- package/dist/commands/gen/index.js +13 -10
- package/dist/commands/gen/index.js.map +1 -1
- package/dist/db/introspect.d.ts +54 -0
- package/dist/db/introspect.d.ts.map +1 -0
- package/dist/db/introspect.js +140 -0
- package/dist/db/introspect.js.map +1 -0
- package/package.json +2 -2
- package/skills/pxbaas-app-spec/references/runtime-contract.md +1 -1
- package/skills/pxbaas-cli/references/data-postgrest.md +15 -0
- package/templates/webapp/react-vite-hono/backend/index.ts +21 -4
- package/templates/webapp/ssr-hono/backend/index.ts +50 -31
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema introspection shared by `db tables`, `db schema`, and `gen types`.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists (fixes defect C1): those three commands used the Meta API
|
|
5
|
+
* (`MetaDriver` -> `/pg/*`) unconditionally. In multi-tenant mode `/pg/*` only
|
|
6
|
+
* accepts the platform/global service_role key, so a per-tenant key is rejected
|
|
7
|
+
* with 401 (`Authentication failed`) — even though the same credentials work for
|
|
8
|
+
* `db query` (which already routes through tenant-ddl), `data *` (REST), and
|
|
9
|
+
* `storage`. See `db/apply-schema.ts` ("per-tenant keys are rejected 401 on
|
|
10
|
+
* /pg/query").
|
|
11
|
+
*
|
|
12
|
+
* The fix keeps single-tenant behavior EXACTLY as before (Meta API) and adds a
|
|
13
|
+
* multi-tenant path that runs the same portable `information_schema` queries
|
|
14
|
+
* through `TenantDdlDriver` (which the tenant key CAN call). The introspection
|
|
15
|
+
* SQL is backend-agnostic, so it is factored into pure functions that take an
|
|
16
|
+
* injectable `SqlRunner` — trivial to unit-test without a network.
|
|
17
|
+
*/
|
|
18
|
+
import { MetaDriver, TenantDdlDriver, } from '@polardbx/pxbaas-client';
|
|
19
|
+
/** Escape a single-quoted SQL string literal. */
|
|
20
|
+
function esc(value) {
|
|
21
|
+
return value.replace(/'/g, "''");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Schema predicate for information_schema filters. An explicit schema filters by
|
|
25
|
+
* name; otherwise scope to the connection's current database via DATABASE() —
|
|
26
|
+
* correct for multi-tenant, where the tenant-ddl connection is bound to the
|
|
27
|
+
* tenant's own database (tenant_<id>), not a Postgres-style `public`.
|
|
28
|
+
*/
|
|
29
|
+
function schemaPredicate(schema) {
|
|
30
|
+
return schema && schema !== ''
|
|
31
|
+
? `table_schema = '${esc(schema)}'`
|
|
32
|
+
: 'table_schema = DATABASE()';
|
|
33
|
+
}
|
|
34
|
+
function toBool(v) {
|
|
35
|
+
return v === 1 || v === true || v === '1';
|
|
36
|
+
}
|
|
37
|
+
/** List tables/views via information_schema (portable across both drivers). */
|
|
38
|
+
export async function listTablesViaSql(run, schema) {
|
|
39
|
+
const sql = `SELECT table_schema AS \`schema\`, table_name AS \`name\`, table_type AS \`type\`, ` +
|
|
40
|
+
`table_rows AS \`rowCount\`, table_comment AS \`comment\` ` +
|
|
41
|
+
`FROM information_schema.tables ` +
|
|
42
|
+
`WHERE ${schemaPredicate(schema)} AND table_type IN ('BASE TABLE','VIEW') ` +
|
|
43
|
+
`ORDER BY table_name`;
|
|
44
|
+
const rows = await run(sql);
|
|
45
|
+
return rows.map((t) => ({
|
|
46
|
+
schema: String(t.schema ?? ''),
|
|
47
|
+
name: String(t.name ?? ''),
|
|
48
|
+
type: String(t.type ?? '').toUpperCase() === 'VIEW' ? 'view' : 'table',
|
|
49
|
+
rowCount: t.rowCount != null ? Number(t.rowCount) : undefined,
|
|
50
|
+
comment: t.comment != null && t.comment !== '' ? String(t.comment) : undefined,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
/** List a table's columns via information_schema (mirrors MetaDriver SQL). */
|
|
54
|
+
export async function getTableColumnsViaSql(run, table, schema) {
|
|
55
|
+
const sql = `SELECT column_name AS \`name\`, data_type AS \`dataType\`, ` +
|
|
56
|
+
`CASE WHEN is_nullable='YES' THEN 1 ELSE 0 END AS \`isNullable\`, ` +
|
|
57
|
+
`column_default AS \`defaultValue\`, ` +
|
|
58
|
+
`CASE WHEN column_key='PRI' THEN 1 ELSE 0 END AS \`isPrimaryKey\`, ` +
|
|
59
|
+
`column_comment AS \`comment\` ` +
|
|
60
|
+
`FROM information_schema.columns ` +
|
|
61
|
+
`WHERE ${schemaPredicate(schema)} AND table_name='${esc(table)}' ` +
|
|
62
|
+
`ORDER BY ordinal_position`;
|
|
63
|
+
const rows = await run(sql);
|
|
64
|
+
return rows.map((row) => ({
|
|
65
|
+
name: String(row.name),
|
|
66
|
+
dataType: String(row.dataType),
|
|
67
|
+
isNullable: toBool(row.isNullable),
|
|
68
|
+
defaultValue: row.defaultValue == null ? null : String(row.defaultValue),
|
|
69
|
+
isPrimaryKey: toBool(row.isPrimaryKey),
|
|
70
|
+
comment: row.comment != null && row.comment !== '' ? String(row.comment) : undefined,
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
/** List a table's indexes via information_schema (mirrors MetaDriver SQL). */
|
|
74
|
+
export async function getTableIndexesViaSql(run, table, schema) {
|
|
75
|
+
const sql = `SELECT index_name AS \`name\`, ` +
|
|
76
|
+
`GROUP_CONCAT(column_name ORDER BY seq_in_index) AS \`cols\`, ` +
|
|
77
|
+
`CASE WHEN MIN(non_unique)=0 THEN 1 ELSE 0 END AS \`isUnique\`, ` +
|
|
78
|
+
`CASE WHEN index_name='PRIMARY' THEN 1 ELSE 0 END AS \`isPrimary\` ` +
|
|
79
|
+
`FROM information_schema.statistics ` +
|
|
80
|
+
`WHERE ${schemaPredicate(schema)} AND table_name='${esc(table)}' ` +
|
|
81
|
+
`GROUP BY index_name`;
|
|
82
|
+
const rows = await run(sql);
|
|
83
|
+
return rows.map((row) => ({
|
|
84
|
+
name: String(row.name),
|
|
85
|
+
columns: row.cols != null && row.cols !== '' ? String(row.cols).split(',') : [],
|
|
86
|
+
isUnique: toBool(row.isUnique),
|
|
87
|
+
isPrimary: toBool(row.isPrimary),
|
|
88
|
+
type: 'BTREE',
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Build the mode-appropriate introspector.
|
|
93
|
+
* - multi-tenant: TenantDdlDriver + information_schema scoped to DATABASE().
|
|
94
|
+
* - single-tenant: the existing MetaDriver behavior, unchanged.
|
|
95
|
+
*/
|
|
96
|
+
export function makeIntrospector(opts) {
|
|
97
|
+
if (opts.mode === 'multi-tenant') {
|
|
98
|
+
if (!opts.tenantId) {
|
|
99
|
+
throw new Error('Multi-tenant profile is missing tenantId; re-run "pxbaas login --tenant-id <id>" with per-tenant keys.');
|
|
100
|
+
}
|
|
101
|
+
const tenantDriver = new TenantDdlDriver({
|
|
102
|
+
endpoint: opts.endpoint,
|
|
103
|
+
serviceRoleKey: opts.serviceRoleKey,
|
|
104
|
+
});
|
|
105
|
+
const run = async (sql) => (await tenantDriver.query(sql));
|
|
106
|
+
let cachedSchema = null;
|
|
107
|
+
return {
|
|
108
|
+
async currentSchema() {
|
|
109
|
+
if (cachedSchema == null) {
|
|
110
|
+
const rows = await run('SELECT DATABASE() AS db');
|
|
111
|
+
cachedSchema = rows.length > 0 ? String(rows[0].db ?? '') : '';
|
|
112
|
+
}
|
|
113
|
+
return cachedSchema;
|
|
114
|
+
},
|
|
115
|
+
listTables: () => listTablesViaSql(run),
|
|
116
|
+
getColumns: (table) => getTableColumnsViaSql(run, table),
|
|
117
|
+
getIndexes: (table) => getTableIndexesViaSql(run, table),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const metaDriver = new MetaDriver({
|
|
121
|
+
endpoint: opts.endpoint,
|
|
122
|
+
anonKey: opts.anonKey,
|
|
123
|
+
serviceRoleKey: opts.serviceRoleKey,
|
|
124
|
+
tenantId: opts.tenantId,
|
|
125
|
+
});
|
|
126
|
+
const schema = opts.schema ?? 'public';
|
|
127
|
+
const useServiceRole = opts.useServiceRole ?? true;
|
|
128
|
+
return {
|
|
129
|
+
async currentSchema() {
|
|
130
|
+
return schema;
|
|
131
|
+
},
|
|
132
|
+
async listTables() {
|
|
133
|
+
const all = await metaDriver.listTables(useServiceRole);
|
|
134
|
+
return all.filter((t) => t.schema === schema);
|
|
135
|
+
},
|
|
136
|
+
getColumns: (table) => metaDriver.getTableColumns(table, schema, useServiceRole),
|
|
137
|
+
getIndexes: (table) => metaDriver.getTableIndexes(table, schema, useServiceRole),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=introspect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"introspect.js","sourceRoot":"","sources":["../../src/db/introspect.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polardbx/pxbaas",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.3",
|
|
4
4
|
"description": "PolarDB-X BaaS CLI application",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"polardbx",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"inquirer": "^9.2.0",
|
|
36
36
|
"jsonwebtoken": "^9.0.0",
|
|
37
37
|
"keytar": "^7.9.0",
|
|
38
|
-
"@polardbx/pxbaas-client": "0.1.0-beta.
|
|
38
|
+
"@polardbx/pxbaas-client": "0.1.0-beta.3"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/inquirer": "^9.0.0",
|
|
@@ -98,7 +98,7 @@ Apps may declare `frontend.fallback: "worker"` in app.json: static-miss non-`/ap
|
|
|
98
98
|
- **React SSR emits no event handlers**: `renderToString` drops every `on*` prop (lowercase `onclick` included). Wire interactivity with `data-*` attributes (passed through verbatim) plus one client-side delegated listener; verify by grepping the served HTML for the attribute, not by reading component code.
|
|
99
99
|
- **MUST** fetch data client-side after hydration (relative `./api/*` or the instance REST/Auth URLs with the user's JWT) for the default SSR-shell shape. For server-side data fetching (L3 full SSR / SEO first paint), **MUST** use the platform-injected `SUPABASE_INTERNAL_URL` as the base — it is the ONLY origin reachable from inside the sandbox (the public `SUPABASE_URL` is egress-filtered and cluster service names do not resolve). The platform derives `SUPABASE_INTERNAL_URL` from the public host via DX-12 (edge-runtime `app-env.ts`, MR #30497903); the worker just reads it and **MUST NOT** re-derive from `SUPABASE_URL` or fetch `SUPABASE_URL` directly. Use `SUPABASE_ANON_KEY` (or the forwarded user JWT) as the credential; server-side RLS still applies on the internal base. These three public keys reach App candidate workers only on sandbox images carrying MR #30497903 — degrade gracefully (render the shell without server data) when `SUPABASE_INTERNAL_URL` is empty.
|
|
100
100
|
- SSR responses are `private, no-cache` with the platform CSP (inline hydrate scripts allowed); do not rely on shared caching of rendered pages.
|
|
101
|
-
- **Path shapes (
|
|
101
|
+
- **Path shapes (current default, `APP_WORKER_PATH_REWRITE` OFF)**: the proxy forwards EVERY worker request as `/<candidateSlug>/<logicalPath>` — the candidate slug prefix is **not** stripped at runtime. So `/apps/{m}/api/data` reaches the worker as `/<candidateSlug>/data`, and a static-miss fallthrough `/apps/{m}/page` reaches it as `/<candidateSlug>/page` (the `/api` segment is consumed by the proxy; the slug is prepended for edge-runtime function routing). The stripped logical path is delivered ONLY via the `X-Pxbaas-Worker-Path` header when `APP_WORKER_PATH_REWRITE` is ON, which is **disabled by default** (the runtime `new Request(url, req)` rewrite crashes with a streamRid TypeError on current sandbox binaries — D18). Workers MUST therefore normalize the leading `/<candidateSlug>` prefix and match on the logical path, e.g. `const p = new URL(req.url).pathname.replace(/^\/app_[^/]+/, "")` then dispatch on `p` (the `ssr-hono`, `ssr-shop`, `ssr-blog`, `csr-bff` templates all do this). A literal `app.get("/data")` will NOT match; do not rely on the declared `pathMode: "strip-app-api-prefix"` being honored at runtime yet. Hydrate scripts calling `./api/*` from an SSR page are routed the same way (browser `/apps/{m}/api/data` → worker `/<candidateSlug>/data`).
|
|
102
102
|
- Start from the `ssr-hono` template (`pxbaas app new --template ssr-hono`); spec-lint enforces the rules above (`ssr-probe-declared`, `ssr-self-contained`, `ssr-marker-present`, `ssr-no-browser-globals`, `ssr-lib-not-allowed`).
|
|
103
103
|
|
|
104
104
|
## Lifecycle & cold start
|
|
@@ -83,3 +83,18 @@ pxbaas rest POST "/todos" --data '{"title":"x"}' --role anon --json
|
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
> REST endpoint details, embedding/RPC constraints: fetch `${endpoint}/docs` — see api-reference.md.
|
|
86
|
+
|
|
87
|
+
## Schema cache after DDL
|
|
88
|
+
|
|
89
|
+
The REST layer (`/rest/v1`) caches each table's column list. After a DDL change
|
|
90
|
+
(`ALTER TABLE ... ADD/DROP/MODIFY COLUMN`, or `CREATE`/`DROP TABLE`) the cache
|
|
91
|
+
re-introspects automatically: immediately where the platform wires DDL to a
|
|
92
|
+
cache reload, otherwise within one refresh interval (~30s). Until then:
|
|
93
|
+
|
|
94
|
+
- `select=*` (and `Prefer: return=representation` bodies) may still omit a
|
|
95
|
+
just-added column. This is a stale column list, not an error — the row is
|
|
96
|
+
returned, only the new field is missing.
|
|
97
|
+
- Workaround: enumerate columns explicitly (`?select=id,room_id,board`) instead
|
|
98
|
+
of `*`, or retry after a few seconds.
|
|
99
|
+
|
|
100
|
+
`pxbaas db add-column` / `db create-table` print a reminder about this.
|
|
@@ -10,10 +10,27 @@ app.use("*", async (context, next) => {
|
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
app.options("*", (context) => context.body(null, 204));
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
)
|
|
13
|
+
|
|
14
|
+
// Path contract (APP_WORKER_PATH_REWRITE off = current default): the proxy
|
|
15
|
+
// forwards /apps/{mount}/api/<x> to the worker as /<candidateSlug>/<x>, so a
|
|
16
|
+
// literal app.get("/health") never matches at runtime. Normalize the candidate
|
|
17
|
+
// slug prefix away and dispatch on the logical path (same convention as the
|
|
18
|
+
// ssr-shop / ssr-blog / csr-bff templates).
|
|
19
|
+
function norm(path: string): string {
|
|
20
|
+
return path.replace(/^\/app_[^/]+/, "") || path;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
app.get("*", (context) => {
|
|
24
|
+
const p = norm(new URL(context.req.url).pathname);
|
|
25
|
+
if (p === "/health" || p === "/api/health") {
|
|
26
|
+
return context.json({ ok: true, app: "{{SLUG}}" });
|
|
27
|
+
}
|
|
28
|
+
if (p === "/hello" || p === "/api/hello") {
|
|
29
|
+
return context.json({ message: "Hello from {{APP_NAME}}" });
|
|
30
|
+
}
|
|
31
|
+
return context.json({ code: "NOT_FOUND", message: "Route not found", path: p }, 404);
|
|
32
|
+
});
|
|
33
|
+
|
|
17
34
|
app.notFound((context) =>
|
|
18
35
|
context.json({ code: "NOT_FOUND", message: "Route not found" }, 404),
|
|
19
36
|
);
|
|
@@ -37,30 +37,45 @@ function esc(s: string): string {
|
|
|
37
37
|
.split(String.fromCharCode(34)).join(""");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
40
|
+
// Path contract (APP_WORKER_PATH_REWRITE off = current default): the proxy
|
|
41
|
+
// forwards /apps/{mount}/api/<x> to the worker as /<candidateSlug>/<x> — the
|
|
42
|
+
// candidate slug prefix is NOT stripped at runtime (the stripped path is only
|
|
43
|
+
// delivered via X-Pxbaas-Worker-Path when the rewrite kill-switch is ON, which
|
|
44
|
+
// is disabled by default due to the D18 streamRid regression). Normalize the
|
|
45
|
+
// slug prefix away and match on the logical path, so handlers resolve under ANY
|
|
46
|
+
// prefix shape (/{slug}/x, /api/x, or /x) and still work if the switch flips on.
|
|
47
|
+
// Same convention as the ssr-shop / ssr-blog / csr-bff templates.
|
|
48
|
+
function norm(path: string): string {
|
|
49
|
+
return path.replace(/^\/app_[^/]+/, "") || path;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
app.get("*", async (c) => {
|
|
53
|
+
const p = norm(new URL(c.req.url).pathname);
|
|
54
|
+
|
|
55
|
+
// L3 shape demo: server-side fetch of DB content via the derived internal base
|
|
56
|
+
// (anon key from env; RLS enforced server-side). Degrades gracefully so the
|
|
57
|
+
// shell still renders if the internal entry is ever unreachable.
|
|
58
|
+
if (p === "/ssr-data") {
|
|
59
|
+
let rows: Array<Record<string, unknown>> = [];
|
|
60
|
+
let note = "";
|
|
61
|
+
try {
|
|
62
|
+
const r = await fetch(internalBase() + "/rest/v1/?select=1", {
|
|
63
|
+
headers: { apikey: Deno.env.get("SUPABASE_ANON_KEY") || "" },
|
|
64
|
+
});
|
|
65
|
+
note = `internal base reachable: http-${r.status}`;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
note = `internal base unreachable: ${String(e).slice(0, 60)}`;
|
|
68
|
+
}
|
|
69
|
+
return c.html(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>L3 demo</title></head><body>
|
|
55
70
|
<div id="__ssr" data-ssr="v1"><h1>L3 full-SSR demo</h1><p>${esc(note)}</p><pre>${esc(JSON.stringify(rows))}</pre></div>
|
|
56
71
|
</body></html>`);
|
|
57
|
-
}
|
|
72
|
+
}
|
|
58
73
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
if (p === "/page") {
|
|
75
|
+
const theme = c.req.query("theme") === "dark" ? "dark" : "light";
|
|
76
|
+
const bg = theme === "dark" ? "#111" : "#fafafa";
|
|
77
|
+
const fg = theme === "dark" ? "#eee" : "#222";
|
|
78
|
+
const html = `<!DOCTYPE html><html><head><meta charset="utf-8">
|
|
64
79
|
<title>{{APP_NAME}} SSR</title>
|
|
65
80
|
<style>body{font-family:sans-serif;max-width:720px;margin:40px auto;background:${bg};color:${fg}}</style>
|
|
66
81
|
</head><body>
|
|
@@ -77,16 +92,20 @@ app.get("/page", (c) => {
|
|
|
77
92
|
})();
|
|
78
93
|
</script>
|
|
79
94
|
</body></html>`;
|
|
80
|
-
|
|
81
|
-
}
|
|
95
|
+
return c.html(html);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// JSON API. /apps/{m}/api/data and /api/health normalize to /data and /health
|
|
99
|
+
// after the slug strip; the /api-prefixed shapes are kept as a defensive
|
|
100
|
+
// fallback for deployments that deliver the logical path un-stripped.
|
|
101
|
+
if (p === "/data" || p === "/api/data") {
|
|
102
|
+
return c.json({ ok: true, engine: "hono", now: new Date().toISOString() });
|
|
103
|
+
}
|
|
104
|
+
if (p === "/health" || p === "/api/health") {
|
|
105
|
+
return c.json({ ok: true });
|
|
106
|
+
}
|
|
82
107
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// fallthrough delivers the FULL logical path (/page). Register both shapes so
|
|
86
|
-
// the worker behaves identically when the rewrite kill-switch is flipped back on.
|
|
87
|
-
app.get("/data", (c) => c.json({ ok: true, engine: "hono", now: new Date().toISOString() }));
|
|
88
|
-
app.get("/api/data", (c) => c.json({ ok: true, engine: "hono", now: new Date().toISOString() }));
|
|
89
|
-
app.get("/health", (c) => c.json({ ok: true }));
|
|
90
|
-
app.get("/api/health", (c) => c.json({ ok: true }));
|
|
108
|
+
return c.json({ ok: false, error: "not_found", path: p }, 404);
|
|
109
|
+
});
|
|
91
110
|
|
|
92
111
|
Deno.serve(app.fetch);
|