@rebasepro/server 0.17.3 → 0.18.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/LICENSE +0 -1
- package/README.md +5 -1
- package/bin/rebase-server.js +16 -1
- package/dist/{GCSStorageController-Bl7nmhdv.js → GCSStorageController-ZXPoNqW3.js} +21 -10
- package/dist/GCSStorageController-ZXPoNqW3.js.map +1 -0
- package/dist/{S3StorageController-CWvvrRpF.js → S3StorageController-5pAXyv31.js} +24 -10
- package/dist/S3StorageController-5pAXyv31.js.map +1 -0
- package/dist/admin-roles-vYdp_Pil.js +36 -0
- package/dist/admin-roles-vYdp_Pil.js.map +1 -0
- package/dist/{admin_block-BX6YULvJ.js → admin_block-0Xu0r6eZ.js} +4 -1
- package/dist/admin_block-0Xu0r6eZ.js.map +1 -0
- package/dist/api/errors.d.ts +37 -0
- package/dist/api/live-schema-routes.d.ts +14 -0
- package/dist/api/logs-routes.d.ts +8 -0
- package/dist/api/rest/api-generator.d.ts +33 -0
- package/dist/api/root-error-handler.d.ts +62 -0
- package/dist/api/schema-drift.d.ts +39 -0
- package/dist/api/types.d.ts +55 -0
- package/dist/{ast-schema-editor-CvkYOvri.js → ast-schema-editor-CNgFJ3NF.js} +23 -7
- package/dist/ast-schema-editor-CNgFJ3NF.js.map +1 -0
- package/dist/auth/api-keys/api-key-routes.d.ts +12 -0
- package/dist/auth/auth-hooks.d.ts +13 -2
- package/dist/auth/exclude-auth-secrets.d.ts +14 -0
- package/dist/auth/jwt.d.ts +22 -1
- package/dist/auth/mfa-routes.d.ts +10 -1
- package/dist/auth/middleware.d.ts +22 -0
- package/dist/auth/rate-limiter.d.ts +50 -0
- package/dist/auth/registration-policy.d.ts +27 -0
- package/dist/auth/rls-scope.d.ts +9 -1
- package/dist/auth/routes.d.ts +8 -1
- package/dist/auth/seed-admin.d.ts +73 -0
- package/dist/auth/session-routes.d.ts +10 -0
- package/dist/{auth-DkbzMZUS.js → auth-BRiOuyq8.js} +283 -198
- package/dist/auth-BRiOuyq8.js.map +1 -0
- package/dist/{backup-CRdZkA6c.js → backup-C8P6Cl3G.js} +2 -2
- package/dist/{backup-CRdZkA6c.js.map → backup-C8P6Cl3G.js.map} +1 -1
- package/dist/boot/boot.d.ts +18 -0
- package/dist/boot/bundle.d.ts +29 -6
- package/dist/boot/ddl-bootstrap.d.ts +2 -0
- package/dist/boot/env.d.ts +30 -1
- package/dist/boot/fetch-bundle.d.ts +24 -1
- package/dist/boot/options.d.ts +0 -8
- package/dist/boot/provision.d.ts +58 -18
- package/dist/boot/resource-adapters.d.ts +11 -1
- package/dist/boot/resource-resolvers.d.ts +96 -0
- package/dist/boot/sources.d.ts +27 -20
- package/dist/collections/validate-config.d.ts +29 -0
- package/dist/{contract-routes-CHPVKTZW.js → contract-routes-CusnEB5h.js} +24 -9
- package/dist/{contract-routes-CHPVKTZW.js.map → contract-routes-CusnEB5h.js.map} +1 -1
- package/dist/cron/cron-loader.d.ts +7 -1
- package/dist/cron/cron-scheduler.d.ts +48 -2
- package/dist/{cron-loader-d9WMFENB.js → cron-loader-DnmIePn_.js} +23 -4
- package/dist/cron-loader-DnmIePn_.js.map +1 -0
- package/dist/{cron-routes-DSC-kZXI.js → cron-routes-D3x2ydMa.js} +9 -6
- package/dist/cron-routes-D3x2ydMa.js.map +1 -0
- package/dist/{cron-scheduler-BB82dWuU.js → cron-scheduler-BpPKpl4i.js} +148 -28
- package/dist/cron-scheduler-BpPKpl4i.js.map +1 -0
- package/dist/{cron-store-KeN8rN63.js → cron-store-CCQXwgVL.js} +5 -5
- package/dist/{cron-store-KeN8rN63.js.map → cron-store-CCQXwgVL.js.map} +1 -1
- package/dist/{ddl-bootstrap-5YZCZ8qk.js → ddl-bootstrap-CfNvxMuK.js} +26 -2
- package/dist/{ddl-bootstrap-5YZCZ8qk.js.map → ddl-bootstrap-CfNvxMuK.js.map} +1 -1
- package/dist/email/index.d.ts +1 -0
- package/dist/email/unconfigured.d.ts +20 -0
- package/dist/env.d.ts +25 -0
- package/dist/errors-HjfaPlvY.js +385 -0
- package/dist/errors-HjfaPlvY.js.map +1 -0
- package/dist/{function-loader-LLdmBFoL.js → function-loader-xnbDAPfa.js} +9 -3
- package/dist/{function-loader-LLdmBFoL.js.map → function-loader-xnbDAPfa.js.map} +1 -1
- package/dist/function-routes-gQ0EShVG.js +267 -0
- package/dist/function-routes-gQ0EShVG.js.map +1 -0
- package/dist/functions/define-function.d.ts +6 -3
- package/dist/functions/function-routes.d.ts +8 -4
- package/dist/functions/index.js +128 -23
- package/dist/functions/index.js.map +1 -1
- package/dist/{history-recorder-B2pL3X8b.js → history-recorder-B1FwXx9J.js} +3 -3
- package/dist/{history-recorder-B2pL3X8b.js.map → history-recorder-B1FwXx9J.js.map} +1 -1
- package/dist/{history-store-BUJ-37Sm.js → history-store-LHXaQywp.js} +2 -2
- package/dist/{history-store-BUJ-37Sm.js.map → history-store-LHXaQywp.js.map} +1 -1
- package/dist/index.d.ts +16 -4
- package/dist/index.es.js +16229 -7676
- package/dist/index.es.js.map +1 -1
- package/dist/init.d.ts +33 -13
- package/dist/{jobs-XXXcZrmj.js → jobs-DkkD9mPV.js} +5 -5
- package/dist/{jobs-XXXcZrmj.js.map → jobs-DkkD9mPV.js.map} +1 -1
- package/dist/{jwt-DNeOn39r.js → jwt-DkhXwMzR.js} +9 -314
- package/dist/jwt-DkhXwMzR.js.map +1 -0
- package/dist/keys-g8lbVC_o.js +230 -0
- package/dist/keys-g8lbVC_o.js.map +1 -0
- package/dist/{logger-DS03e908.js → logger-DO2PZc4i.js} +197 -18
- package/dist/logger-DO2PZc4i.js.map +1 -0
- package/dist/{logs-routes-DB72iQSr.js → logs-routes-CbsTpozn.js} +74 -6
- package/dist/logs-routes-CbsTpozn.js.map +1 -0
- package/dist/{openapi-generator-CpZp2Fai.js → openapi-generator-BIBbO1Tq.js} +221 -38
- package/dist/openapi-generator-BIBbO1Tq.js.map +1 -0
- package/dist/{proxy-CMymhnwG.js → proxy-Czngl3p9.js} +2 -2
- package/dist/{proxy-CMymhnwG.js.map → proxy-Czngl3p9.js.map} +1 -1
- package/dist/{query-parser-DqXNaBIH.js → query-parser-C68Q9EX4.js} +35 -6
- package/dist/{query-parser-DqXNaBIH.js.map → query-parser-C68Q9EX4.js.map} +1 -1
- package/dist/queues/runtime.d.ts +56 -0
- package/dist/{request-timeout-C8gkc-j7.js → request-timeout-DESvlfrS.js} +4 -6
- package/dist/request-timeout-DESvlfrS.js.map +1 -0
- package/dist/schema-edit/project-root.d.ts +4 -4
- package/dist/{schema-editor-routes-DNV_nBhW.js → schema-editor-routes-CcZKh50q.js} +4 -4
- package/dist/{schema-editor-routes-DNV_nBhW.js.map → schema-editor-routes-CcZKh50q.js.map} +1 -1
- package/dist/{schemas-DBxgjM9A.js → schemas-C3234HWE.js} +2746 -72
- package/dist/schemas-C3234HWE.js.map +1 -0
- package/dist/singleton.d.ts +6 -4
- package/dist/src-DHK4fHkw.js +1002 -0
- package/dist/src-DHK4fHkw.js.map +1 -0
- package/dist/{src-C9gKj23J.js → src-Dq-I3Ybx.js} +507 -54
- package/dist/src-Dq-I3Ybx.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +5 -0
- package/dist/storage/LocalStorageController.d.ts +14 -0
- package/dist/storage/S3StorageController.d.ts +8 -0
- package/dist/storage/image-transform.d.ts +35 -0
- package/dist/storage/keys.d.ts +23 -0
- package/dist/storage/policies.d.ts +22 -0
- package/dist/storage/routes.d.ts +9 -0
- package/dist/storage/tus-handler.d.ts +43 -1
- package/dist/storage/types.d.ts +28 -0
- package/dist/types-BfKcm9do.js.map +1 -1
- package/dist/utils/dev-port.d.ts +18 -0
- package/dist/utils/logger.d.ts +60 -0
- package/dist/utils/request-logger.d.ts +2 -1
- package/package.json +30 -24
- package/dist/GCSStorageController-Bl7nmhdv.js.map +0 -1
- package/dist/S3StorageController-CWvvrRpF.js.map +0 -1
- package/dist/admin_block-BX6YULvJ.js.map +0 -1
- package/dist/ast-schema-editor-CvkYOvri.js.map +0 -1
- package/dist/auth-DkbzMZUS.js.map +0 -1
- package/dist/cron-loader-d9WMFENB.js.map +0 -1
- package/dist/cron-routes-DSC-kZXI.js.map +0 -1
- package/dist/cron-scheduler-BB82dWuU.js.map +0 -1
- package/dist/errors-DBwpj9N8.js +0 -231
- package/dist/errors-DBwpj9N8.js.map +0 -1
- package/dist/function-routes-ClT6UQpD.js +0 -41
- package/dist/function-routes-ClT6UQpD.js.map +0 -1
- package/dist/jwt-DNeOn39r.js.map +0 -1
- package/dist/logger-DS03e908.js.map +0 -1
- package/dist/logs-routes-DB72iQSr.js.map +0 -1
- package/dist/openapi-generator-CpZp2Fai.js.map +0 -1
- package/dist/request-timeout-C8gkc-j7.js.map +0 -1
- package/dist/schemas-DBxgjM9A.js.map +0 -1
- package/dist/src-C9gKj23J.js.map +0 -1
- package/dist/src-CcOl05Oq.js +0 -552
- package/dist/src-CcOl05Oq.js.map +0 -1
- package/dist/utils/logging.d.ts +0 -5
|
@@ -3,7 +3,8 @@ import __rebaseProcess from "process";
|
|
|
3
3
|
globalThis.process ??= __rebaseProcess;
|
|
4
4
|
__rebaseCreateRequire(import.meta.url);
|
|
5
5
|
import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
|
|
6
|
-
import {
|
|
6
|
+
import { t as addLogSink } from "./logger-DO2PZc4i.js";
|
|
7
|
+
import { n as errorHandler, t as ApiError } from "./errors-HjfaPlvY.js";
|
|
7
8
|
import { Hono } from "hono";
|
|
8
9
|
import { streamSSE } from "hono/streaming";
|
|
9
10
|
//#region src/api/logs-routes.ts
|
|
@@ -12,7 +13,9 @@ var logs_routes_exports = /* @__PURE__ */ __exportAll({
|
|
|
12
13
|
createLogsRoutes: () => createLogsRoutes,
|
|
13
14
|
default: () => logs_routes_default,
|
|
14
15
|
logBuffer: () => logBuffer,
|
|
15
|
-
logMiddleware: () => logMiddleware
|
|
16
|
+
logMiddleware: () => logMiddleware,
|
|
17
|
+
sourceForMessage: () => sourceForMessage,
|
|
18
|
+
teeLoggerIntoLogBuffer: () => teeLoggerIntoLogBuffer
|
|
16
19
|
});
|
|
17
20
|
function normalizeFilter(options) {
|
|
18
21
|
return {
|
|
@@ -103,16 +106,80 @@ function logMiddleware(options = {}) {
|
|
|
103
106
|
if (ignored.has(c.req.path)) return;
|
|
104
107
|
const duration = Date.now() - start;
|
|
105
108
|
const reqId = c.get("requestId");
|
|
106
|
-
|
|
109
|
+
const status = c.res.status;
|
|
110
|
+
const level = status >= 500 ? "error" : status >= 400 ? "warn" : "info";
|
|
111
|
+
const failure = c.get("errorSummary");
|
|
112
|
+
addLog(level, "api", `${c.req.method} ${c.req.path} ${status} ${duration}ms` + (failure ? ` — ${failure.code}: ${failure.message}` : ""), {
|
|
107
113
|
method: c.req.method,
|
|
108
114
|
path: c.req.path,
|
|
109
|
-
status
|
|
115
|
+
status,
|
|
110
116
|
duration,
|
|
111
|
-
...reqId && { requestId: reqId }
|
|
117
|
+
...reqId && { requestId: reqId },
|
|
118
|
+
...c.get("collection") && { collection: c.get("collection") },
|
|
119
|
+
...failure && {
|
|
120
|
+
errorCode: failure.code,
|
|
121
|
+
errorMessage: failure.message
|
|
122
|
+
}
|
|
112
123
|
});
|
|
113
124
|
};
|
|
114
125
|
}
|
|
115
126
|
/**
|
|
127
|
+
* Prefixes the server writes at the head of a log message, and the `source`
|
|
128
|
+
* each one belongs to.
|
|
129
|
+
*
|
|
130
|
+
* The ring's `source` is a closed set the Studio filters on, and the messages
|
|
131
|
+
* carry their origin as a bracketed prefix — `[API]`, `[Auth]`, `[functions]`,
|
|
132
|
+
* `[schema]`. Matching them is what makes a teed line filterable beside the
|
|
133
|
+
* request entries rather than a heap under "system".
|
|
134
|
+
*/
|
|
135
|
+
var SOURCE_BY_PREFIX = [
|
|
136
|
+
[/^\[?(api|rest)\b/i, "api"],
|
|
137
|
+
[/^\[?auth\b/i, "auth"],
|
|
138
|
+
[/^\[?(storage|s3|gcs)\b/i, "storage"],
|
|
139
|
+
[/^\[?(realtime|ws|websocket|cdc)\b/i, "realtime"]
|
|
140
|
+
];
|
|
141
|
+
/**
|
|
142
|
+
* The `source` for a teed line, from whatever prefix it carries.
|
|
143
|
+
*
|
|
144
|
+
* Exported for its test: the mapping is a string match on wording somebody else
|
|
145
|
+
* writes, which is the shape of check that stops working without failing.
|
|
146
|
+
*/
|
|
147
|
+
function sourceForMessage(message) {
|
|
148
|
+
const candidate = message.match(/\[([^\]]{1,32})\]/)?.[1] ?? message;
|
|
149
|
+
for (const [pattern, source] of SOURCE_BY_PREFIX) if (pattern.test(candidate)) return source;
|
|
150
|
+
return "system";
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Feed the Logs Explorer everything the server says at warn and above.
|
|
154
|
+
*
|
|
155
|
+
* The ring used to be filled by `logMiddleware` alone, so the panel showed a
|
|
156
|
+
* wall of `GET /api/data/posts 200 4ms` and not one of the errors, warnings or
|
|
157
|
+
* boot diagnoses being written to stdout at the same moment. A function that
|
|
158
|
+
* threw was the sharpest case: the request entry said `500` and the reason
|
|
159
|
+
* existed only in a terminal the person looking at the panel does not have.
|
|
160
|
+
*
|
|
161
|
+
* Warn and above, deliberately. `info` is where the steady-state chatter lives,
|
|
162
|
+
* and a 10,000-entry ring filled with it evicts the lines somebody opened the
|
|
163
|
+
* panel to find.
|
|
164
|
+
*
|
|
165
|
+
* Idempotent: called from `createLogsRoutes`, which a split deployment may
|
|
166
|
+
* reach more than once.
|
|
167
|
+
*/
|
|
168
|
+
var detachLoggerTee;
|
|
169
|
+
function teeLoggerIntoLogBuffer() {
|
|
170
|
+
if (detachLoggerTee) return detachLoggerTee;
|
|
171
|
+
const detach = addLogSink((level, message, data) => {
|
|
172
|
+
if (level !== "warn" && level !== "error") return;
|
|
173
|
+
if (message === "request") return;
|
|
174
|
+
addLog(level, sourceForMessage(message), message, Object.keys(data).length > 0 ? data : void 0);
|
|
175
|
+
});
|
|
176
|
+
detachLoggerTee = () => {
|
|
177
|
+
detach();
|
|
178
|
+
detachLoggerTee = void 0;
|
|
179
|
+
};
|
|
180
|
+
return detachLoggerTee;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
116
183
|
* How long entries accumulate before a batch goes out.
|
|
117
184
|
*
|
|
118
185
|
* Not zero, and that is the point. A busy server logs faster than a browser can
|
|
@@ -158,6 +225,7 @@ var STREAM_MAX_PENDING = 2e3;
|
|
|
158
225
|
*/
|
|
159
226
|
var LOG_WINDOW_MAX = 1e4;
|
|
160
227
|
function createLogsRoutes(timing = {}) {
|
|
228
|
+
teeLoggerIntoLogBuffer();
|
|
161
229
|
const flushMs = timing.flushMs ?? STREAM_FLUSH_MS;
|
|
162
230
|
const heartbeatMs = timing.heartbeatMs ?? STREAM_HEARTBEAT_MS;
|
|
163
231
|
const maxPending = timing.maxPending ?? STREAM_MAX_PENDING;
|
|
@@ -269,4 +337,4 @@ var logs_routes_default = createLogsRoutes();
|
|
|
269
337
|
//#endregion
|
|
270
338
|
export { logs_routes_exports as n, logMiddleware as t };
|
|
271
339
|
|
|
272
|
-
//# sourceMappingURL=logs-routes-
|
|
340
|
+
//# sourceMappingURL=logs-routes-CbsTpozn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logs-routes-CbsTpozn.js","names":[],"sources":["../src/api/logs-routes.ts"],"sourcesContent":["import { Hono } from \"hono\";\nimport type { MiddlewareHandler } from \"hono\";\nimport { streamSSE } from \"hono/streaming\";\nimport type { HonoEnv } from \"./types\";\nimport { ApiError, errorHandler } from \"./errors\";\nimport { addLogSink } from \"../utils/logger\";\n\nexport interface LogEntry {\n id: string;\n timestamp: string;\n level: \"debug\" | \"info\" | \"warn\" | \"error\";\n source: \"api\" | \"auth\" | \"storage\" | \"realtime\" | \"system\";\n message: string;\n metadata?: Record<string, unknown>;\n}\n\n/** What a caller can narrow the log by, in either direction (query or stream). */\nexport interface LogFilterOptions {\n level?: string;\n source?: string;\n search?: string;\n since?: string;\n}\n\n/**\n * A filter with the search term already lowercased.\n *\n * The distinction matters on the stream path: `query()` lowercases once and then\n * scans, but a subscriber tests one entry at a time and would otherwise redo the\n * same `toLowerCase()` on every request the server handles.\n */\ntype NormalizedFilter = LogFilterOptions;\n\nfunction normalizeFilter(options: LogFilterOptions): NormalizedFilter {\n return { ...options,\n search: options.search?.toLowerCase() };\n}\n\n/**\n * Whether one entry belongs in a filtered view.\n *\n * Shared by the query and the stream on purpose: two copies of this would drift,\n * and the failure that produces is invisible — a tail that quietly shows a\n * different set of lines than the snapshot it started from.\n */\nfunction matchesFilter(entry: LogEntry, filter: NormalizedFilter): boolean {\n if (filter.level && entry.level !== filter.level) return false;\n if (filter.source && entry.source !== filter.source) return false;\n if (filter.search && !entry.message.toLowerCase().includes(filter.search)) return false;\n if (filter.since && entry.timestamp < filter.since) return false;\n return true;\n}\n\n/** Notified for every entry pushed, in push order. */\nexport type LogListener = (entry: LogEntry) => void;\n\nclass LogRingBuffer {\n private buffer: LogEntry[] = [];\n private maxSize: number;\n private idCounter = 0;\n private listeners = new Set<LogListener>();\n\n constructor(maxSize = 10000) {\n this.maxSize = maxSize;\n }\n\n push(entry: Omit<LogEntry, \"id\">): void {\n const id = `log_${++this.idCounter}`;\n const stored: LogEntry = { ...entry,\n id };\n this.buffer.push(stored);\n if (this.buffer.length > this.maxSize) {\n this.buffer.shift();\n }\n // This runs on the request hot path, so a listener must never be able to\n // take the request down with it: a tail that throws loses its own tail,\n // not the response the log line was describing.\n for (const listener of this.listeners) {\n try {\n listener(stored);\n } catch {\n /* a broken tail is not the request's problem */\n }\n }\n }\n\n /**\n * Follow the buffer. Returns the unsubscribe — call it, always: a listener\n * left behind holds its whole closure, and on this class that closure is a\n * pending-entry array.\n *\n * A listener must not log. It is called from inside `push`, so anything that\n * reaches `addLog` from here recurses until the stack gives out.\n */\n subscribe(listener: LogListener): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n query(options: LogFilterOptions & {\n limit?: number;\n offset?: number;\n }): { entries: LogEntry[]; total: number } {\n const filter = normalizeFilter(options);\n const filtered = this.buffer.filter(e => matchesFilter(e, filter));\n\n // Newest first\n const sorted = [...filtered].reverse();\n const total = sorted.length;\n const limit = options.limit || 100;\n const offset = options.offset || 0;\n\n return {\n entries: sorted.slice(offset, offset + limit),\n total\n };\n }\n\n getLatest(count = 50): LogEntry[] {\n return this.buffer.slice(-count).reverse();\n }\n}\n\n// Global singleton\nexport const logBuffer = new LogRingBuffer();\n\n/** Add a log entry */\nexport function addLog(\n level: LogEntry[\"level\"],\n source: LogEntry[\"source\"],\n message: string,\n metadata?: Record<string, unknown>\n): void {\n logBuffer.push({\n timestamp: new Date().toISOString(),\n level,\n source,\n message,\n metadata\n });\n}\n\nexport interface LogMiddlewareOptions {\n /**\n * Paths this sink ignores, matched exactly against `c.req.path`.\n *\n * For requests whose only reason to exist is to read the log. Recording\n * those makes the reader the loudest thing in its own output, and on a quiet\n * server it is also the thing evicting real entries out of the ring.\n */\n ignorePaths?: string[];\n}\n\n/** Hono middleware to log API requests */\nexport function logMiddleware(options: LogMiddlewareOptions = {}): MiddlewareHandler<HonoEnv> {\n const ignored = new Set(options.ignorePaths ?? []);\n return async (c, next) => {\n const start = Date.now();\n await next();\n if (ignored.has(c.req.path)) return;\n const duration = Date.now() - start;\n const reqId = c.get(\"requestId\");\n // Every request used to be recorded at `info`, whatever it answered, so\n // the Logs Explorer's level filter could not find a single failure: a\n // 500 sat at the same level as the 200 above it, in a wall of them.\n const status = c.res.status;\n const level: LogEntry[\"level\"] = status >= 500 ? \"error\" : status >= 400 ? \"warn\" : \"info\";\n // What the error handler answered, so the failure is on the entry\n // rather than in a stdout line the panel cannot see. See\n // `HonoEnv.Variables.errorSummary`.\n const failure = c.get(\"errorSummary\");\n addLog(\n level,\n \"api\",\n `${c.req.method} ${c.req.path} ${status} ${duration}ms`\n + (failure ? ` — ${failure.code}: ${failure.message}` : \"\"),\n {\n method: c.req.method,\n path: c.req.path,\n status,\n duration,\n ...(reqId && { requestId: reqId }),\n ...(c.get(\"collection\") && { collection: c.get(\"collection\") }),\n ...(failure && { errorCode: failure.code, errorMessage: failure.message })\n }\n );\n };\n}\n\n/**\n * Prefixes the server writes at the head of a log message, and the `source`\n * each one belongs to.\n *\n * The ring's `source` is a closed set the Studio filters on, and the messages\n * carry their origin as a bracketed prefix — `[API]`, `[Auth]`, `[functions]`,\n * `[schema]`. Matching them is what makes a teed line filterable beside the\n * request entries rather than a heap under \"system\".\n */\nconst SOURCE_BY_PREFIX: Array<[RegExp, LogEntry[\"source\"]]> = [\n [/^\\[?(api|rest)\\b/i, \"api\"],\n [/^\\[?auth\\b/i, \"auth\"],\n [/^\\[?(storage|s3|gcs)\\b/i, \"storage\"],\n [/^\\[?(realtime|ws|websocket|cdc)\\b/i, \"realtime\"]\n];\n\n/**\n * The `source` for a teed line, from whatever prefix it carries.\n *\n * Exported for its test: the mapping is a string match on wording somebody else\n * writes, which is the shape of check that stops working without failing.\n */\nexport function sourceForMessage(message: string): LogEntry[\"source\"] {\n // The level emoji comes first on several call sites (`⚠️ [API] …`), so the\n // prefix is whatever is inside the first bracket, wherever that is.\n const bracketed = message.match(/\\[([^\\]]{1,32})\\]/);\n const candidate = bracketed?.[1] ?? message;\n for (const [pattern, source] of SOURCE_BY_PREFIX) {\n if (pattern.test(candidate)) return source;\n }\n return \"system\";\n}\n\n/**\n * Feed the Logs Explorer everything the server says at warn and above.\n *\n * The ring used to be filled by `logMiddleware` alone, so the panel showed a\n * wall of `GET /api/data/posts 200 4ms` and not one of the errors, warnings or\n * boot diagnoses being written to stdout at the same moment. A function that\n * threw was the sharpest case: the request entry said `500` and the reason\n * existed only in a terminal the person looking at the panel does not have.\n *\n * Warn and above, deliberately. `info` is where the steady-state chatter lives,\n * and a 10,000-entry ring filled with it evicts the lines somebody opened the\n * panel to find.\n *\n * Idempotent: called from `createLogsRoutes`, which a split deployment may\n * reach more than once.\n */\nlet detachLoggerTee: (() => void) | undefined;\nexport function teeLoggerIntoLogBuffer(): () => void {\n if (detachLoggerTee) return detachLoggerTee;\n const detach = addLogSink((level, message, data) => {\n if (level !== \"warn\" && level !== \"error\") return;\n // `requestLogger` writes this one to stdout for every request, and\n // `logMiddleware` has already recorded the same request here with the\n // fields this panel renders. Teeing it too would double every failure.\n if (message === \"request\") return;\n addLog(level, sourceForMessage(message), message, Object.keys(data).length > 0 ? data : undefined);\n });\n detachLoggerTee = () => {\n detach();\n detachLoggerTee = undefined;\n };\n return detachLoggerTee;\n}\n\n/**\n * How long entries accumulate before a batch goes out.\n *\n * Not zero, and that is the point. A busy server logs faster than a browser can\n * render, and one SSE frame per line would hand the client a re-render per\n * request served — worse than the 3s poll this replaces, precisely when the logs\n * are worth watching. Coalescing keeps the frame rate bounded by the window\n * rather than by traffic, and 250ms still reads as \"live\" to a person.\n */\nconst STREAM_FLUSH_MS = 250;\n\n/**\n * Idle gap after which the stream sends a comment line.\n *\n * A silent SSE connection is indistinguishable from a dead one to everything in\n * between — proxies, load balancers and laptop NICs all reap idle sockets, and a\n * server with nothing to say is the normal state here.\n */\nconst STREAM_HEARTBEAT_MS = 25_000;\n\n/**\n * Entries a single connection will hold between flushes.\n *\n * This is a *rate* ceiling, not just a memory bound, and that is easy to get\n * wrong: nothing drains `pending` between flushes, so the most a connection can\n * carry losslessly is `maxPending` per `flushMs` — here 2000 per 250ms, or 8000\n * entries a second. Above that the oldest pending entries go and the client is\n * told how many, whatever speed it is reading at.\n *\n * It was 500, which put that ceiling at 2000/s. A healthy reader on a loopback\n * socket lost 85% of a 20k burst to it — the cap fired on the server's own\n * coalescing window rather than on any slowness at the client, which is a drop\n * notice that says nothing true about why. 8000/s is past what one Node process\n * serves, so reaching it now means genuinely more log than a person can be shown.\n *\n * The memory this bounds is the copy a *stalled* reader causes: roughly 2000\n * entries, a few MB, per stuck connection.\n */\nconst STREAM_MAX_PENDING = 2000;\n\n/**\n * The largest window any of these routes will hand back.\n *\n * The ring buffer holds 10,000 entries, so asking for more than all of it is a\n * mistake worth naming rather than silently clamping — and a clamped answer is\n * indistinguishable from \"that is all there is\".\n */\nconst LOG_WINDOW_MAX = 10_000;\n\n/**\n * The stream's timings, injectable only so they can be tested.\n *\n * The defaults above are the contract and nothing in production passes this. A\n * heartbeat is a 25-second wait to observe, and a suite that cannot observe it is\n * a suite where the keepalive can rot — which surfaces as \"the tail dies after a\n * few minutes behind the load balancer\", months later, on someone else's cluster.\n */\nexport interface LogStreamTiming {\n flushMs?: number;\n heartbeatMs?: number;\n maxPending?: number;\n}\n\nexport function createLogsRoutes(timing: LogStreamTiming = {}): Hono<HonoEnv> {\n // Here rather than at module load: the ring exists whether or not anything\n // reads it, but there is no reason to fill it on a process that serves no\n // logs surface. Idempotent, so the repeated calls a split deployment makes\n // do not stack up sinks.\n teeLoggerIntoLogBuffer();\n\n const flushMs = timing.flushMs ?? STREAM_FLUSH_MS;\n const heartbeatMs = timing.heartbeatMs ?? STREAM_HEARTBEAT_MS;\n const maxPending = timing.maxPending ?? STREAM_MAX_PENDING;\n\n const app = new Hono<HonoEnv>();\n // Its own, like every other router here: nothing registers one on the host\n // app — not `boot.ts`, not the scaffolded backend, not the eject template —\n // so a router that throws without this answers Hono's default 500 in plain\n // text, outside the `{ error: { code, message } }` envelope the rest of the\n // API keeps to.\n app.onError(errorHandler);\n\n /**\n * A window into the ring buffer, or a 400 saying why not.\n *\n * `parseInt` was the whole of it before, and every malformed value failed\n * differently and silently: `?limit=abc` fell through to the default,\n * `?limit=-5` sliced an empty window and answered 200 with no entries, and\n * `?count=abc` made `slice(-NaN)` return the *entire* buffer. Three ways to\n * be wrong, none of them visible to the caller. The data plane refuses the\n * same input with a 400 — see `resolveListLimitParam`.\n */\n const window = (raw: string | undefined, what: string, max: number): number | undefined => {\n if (raw === undefined || raw.trim() === \"\") return undefined;\n const parsed = Number(raw.trim());\n if (!Number.isInteger(parsed) || parsed < 1 || parsed > max) {\n throw new ApiError(\n 400,\n \"INVALID_PARAM\",\n `Invalid \\`${what}\\`: ${raw}. Expected a whole number between 1 and ${max}.`,\n undefined,\n true\n );\n }\n return parsed;\n };\n\n // GET /api/logs — Query logs\n app.get(\"/\", (c) => {\n const query = c.req.query();\n const result = logBuffer.query({\n level: query.level,\n source: query.source,\n search: query.search,\n limit: window(query.limit, \"limit\", LOG_WINDOW_MAX),\n offset: window(query.offset, \"offset\", Number.MAX_SAFE_INTEGER),\n since: query.since\n });\n return c.json(result);\n });\n\n // GET /api/logs/latest — Get latest logs (for real-time)\n app.get(\"/latest\", (c) => {\n const count = window(c.req.query(\"count\"), \"count\", LOG_WINDOW_MAX) ?? 50;\n return c.json({ entries: logBuffer.getLatest(count) });\n });\n\n // GET /api/logs/stream — tail the buffer over SSE.\n //\n // The Logs Explorer used to poll this router every 3 seconds, which cost a\n // request per client per 3s to say \"nothing happened\" and still showed each\n // line up to 3s late. Here the buffer pushes instead, so an idle server is an\n // idle socket.\n //\n // Events:\n // snapshot {entries, total} the filtered window, oldest-first, at open\n // append {entries, dropped} entries since the last frame, oldest-first\n // `: ping` comment, keepalive only\n //\n // Snapshot and appends come down the same connection deliberately. A client\n // that fetched its backlog separately would race the subscription — entries\n // logged between the two calls belong to neither — and closing that race from\n // the outside needs an id cursor and dedupe on every frame.\n app.get(\"/stream\", (c) => {\n const query = c.req.query();\n const filter = normalizeFilter({\n level: query.level,\n source: query.source,\n search: query.search\n });\n const limit = window(query.limit, \"limit\", LOG_WINDOW_MAX) ?? 200;\n\n // Reverse proxies buffer text responses by default, which turns a live\n // tail into nothing at all until the buffer fills. nginx (and the ingress\n // in front of the managed runtime) reads this header; everything else\n // ignores it. The rest of the SSE headers are set by `streamSSE`.\n c.header(\"X-Accel-Buffering\", \"no\");\n\n return streamSSE(c, async (stream) => {\n let pending: LogEntry[] = [];\n let dropped = 0;\n\n // Subscribe *before* reading the backlog, with nothing awaited\n // between the two. Both are synchronous, so the two halves meet\n // exactly: an entry logged after the query but before the\n // subscription would otherwise be in neither, and that gap is the one\n // thing this route exists to close.\n const unsubscribe = logBuffer.subscribe(entry => {\n if (!matchesFilter(entry, filter)) return;\n if (pending.length >= maxPending) {\n pending.shift();\n dropped++;\n }\n pending.push(entry);\n });\n const snapshot = logBuffer.query({ ...filter,\n limit });\n\n // A client that goes away has to end this handler, or the\n // subscription outlives the socket. `streamSSE` only wires the\n // request signal through on old Bun, so do it here and let\n // `stream.aborted` be the one condition the loop tests.\n //\n // The `aborted` check is not belt-and-braces. A listener added to an\n // already-aborted signal is never called, so a client that leaves\n // during the snapshot write — a fast navigation, or a reconnect storm\n // against a restarting server — would leave this handler with no way\n // to learn it had gone: a subscriber and a flush loop, per attempt,\n // for the life of the process.\n const abortOnDisconnect = () => {\n if (!stream.closed) stream.abort();\n };\n c.req.raw.signal.addEventListener(\"abort\", abortOnDisconnect, { once: true });\n if (c.req.raw.signal.aborted) abortOnDisconnect();\n\n try {\n await stream.writeSSE({\n event: \"snapshot\",\n // The view tails like a terminal; both frames are oldest-first\n // so the client only ever appends.\n data: JSON.stringify({\n entries: snapshot.entries.slice().reverse(),\n total: snapshot.total\n })\n });\n\n let idleMs = 0;\n while (!stream.aborted && !stream.closed) {\n await stream.sleep(flushMs);\n if (stream.aborted || stream.closed) break;\n\n if (pending.length === 0) {\n idleMs += flushMs;\n if (idleMs >= heartbeatMs) {\n await stream.write(\": ping\\n\\n\");\n idleMs = 0;\n }\n continue;\n }\n\n const entries = pending;\n const lost = dropped;\n pending = [];\n dropped = 0;\n idleMs = 0;\n await stream.writeSSE({\n event: \"append\",\n data: JSON.stringify(lost > 0 ? { entries,\n dropped: lost } : { entries })\n });\n }\n } finally {\n unsubscribe();\n c.req.raw.signal.removeEventListener(\"abort\", abortOnDisconnect);\n }\n });\n });\n\n return app;\n}\n\nexport default createLogsRoutes();\n"],"mappings":"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"}
|
|
@@ -2,8 +2,8 @@ import { createRequire as __rebaseCreateRequire } from "module";
|
|
|
2
2
|
import __rebaseProcess from "process";
|
|
3
3
|
globalThis.process ??= __rebaseProcess;
|
|
4
4
|
__rebaseCreateRequire(import.meta.url);
|
|
5
|
-
import {
|
|
6
|
-
import "./src-
|
|
5
|
+
import { S as resolveCollectionRelations, v as fieldKeyForColumn, x as isRelationRequired, y as findRelation } from "./src-Dq-I3Ybx.js";
|
|
6
|
+
import "./src-DHK4fHkw.js";
|
|
7
7
|
//#region ../types/src/types/relations.ts
|
|
8
8
|
/** @group Models */
|
|
9
9
|
function isToMany(relation) {
|
|
@@ -203,6 +203,7 @@ function generateOpenApiSpec(collections, options = {}) {
|
|
|
203
203
|
const schemas = spec.components.schemas;
|
|
204
204
|
const tags = spec.tags;
|
|
205
205
|
const reservedParameterNames = new Set(listQueryParameters().map((p) => p.name));
|
|
206
|
+
const registeredSchemas = new Set((collections || []).map(schemaNameFor));
|
|
206
207
|
for (const collection of collections || []) {
|
|
207
208
|
const schemaName = schemaNameFor(collection);
|
|
208
209
|
const slug = collection.slug;
|
|
@@ -210,7 +211,7 @@ function generateOpenApiSpec(collections, options = {}) {
|
|
|
210
211
|
name: collection.name,
|
|
211
212
|
description: collection.description || `CRUD operations for ${collection.name}`
|
|
212
213
|
});
|
|
213
|
-
schemas[schemaName] = buildCollectionSchema(collection);
|
|
214
|
+
schemas[schemaName] = buildCollectionSchema(collection, registeredSchemas);
|
|
214
215
|
schemas[`${schemaName}Input`] = buildCollectionInputSchema(collection);
|
|
215
216
|
schemas[`${schemaName}Update`] = buildCollectionUpdateSchema(collection);
|
|
216
217
|
const dataPath = `/data/${slug}`;
|
|
@@ -569,46 +570,186 @@ function generateOpenApiSpec(collections, options = {}) {
|
|
|
569
570
|
/**
|
|
570
571
|
* Is this property part of the shape the document may describe?
|
|
571
572
|
*
|
|
572
|
-
*
|
|
573
|
-
* server
|
|
573
|
+
* One exclusion, and it is the document telling the truth about what the server
|
|
574
|
+
* does: `excludeFromApi` is a server-side guarantee that the column "is
|
|
575
|
+
* stripped from every row the API serves, for every caller, including admins
|
|
576
|
+
* and service keys" — `stripExcluded` in the row pipeline enforces it. A schema
|
|
577
|
+
* that lists such a column describes a field that is never present, and it
|
|
578
|
+
* describes it to everyone: `/docs` is mounted on the app, not on the data
|
|
579
|
+
* router, so it carries none of the auth middleware `{basePath}/data` does.
|
|
580
|
+
* Every project scaffolded by `rebase init` published its `users` collection's
|
|
581
|
+
* `passwordHash` and `emailVerificationToken` this way.
|
|
574
582
|
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
* router, so it carries none of the auth middleware `{basePath}/data` does.
|
|
585
|
-
* Every project scaffolded by `rebase init` published its `users`
|
|
586
|
-
* collection's `passwordHash` and `emailVerificationToken` this way.
|
|
583
|
+
* A `relation` property used to be excluded here too, on the reasoning that the
|
|
584
|
+
* property is virtual. It is — but the row is not empty where it stands: the
|
|
585
|
+
* owning side carries a foreign key under a *wire* name (`authorId`), and a
|
|
586
|
+
* read that includes the relation carries the target's row under the relation's
|
|
587
|
+
* own name. Excluding both left `/api/docs` describing a strictly smaller
|
|
588
|
+
* collection than `generated/sdk/database.types.ts` did for the same config.
|
|
589
|
+
* Relations are now emitted by {@link emitRelationProperties}, under the same
|
|
590
|
+
* keys and in the same order as the SDK's `Row`; the direct-property loops skip
|
|
591
|
+
* them so the two passes cannot emit one key twice.
|
|
587
592
|
*
|
|
588
|
-
* Written as one predicate rather than
|
|
589
|
-
* fixed in one loop at a time: this is the same rule the SDK generator
|
|
590
|
-
* to its `Row` type (`packages/codegen/src/generate-types.ts`).
|
|
593
|
+
* Written as one predicate rather than a `continue` per loop because it kept
|
|
594
|
+
* being fixed in one loop at a time: this is the same rule the SDK generator
|
|
595
|
+
* applies to its `Row` type (`packages/codegen/src/generate-types.ts`).
|
|
591
596
|
*/
|
|
592
597
|
function isDocumentedProperty(property) {
|
|
593
|
-
return
|
|
598
|
+
return !property.excludeFromApi;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* The keys `stripExcluded` deletes: the property name *and* its column name.
|
|
602
|
+
*
|
|
603
|
+
* Seeding the emitted set with both is what stops a foreign key derived from a
|
|
604
|
+
* relation putting an `excludeFromApi` column back on the surface under its
|
|
605
|
+
* other name. Same pair, same reason, as `excludedApiKeys` in the SDK
|
|
606
|
+
* generator.
|
|
607
|
+
*/
|
|
608
|
+
function excludedApiKeys(collection) {
|
|
609
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
610
|
+
for (const [key, property] of Object.entries(collection.properties ?? {})) {
|
|
611
|
+
if (!property?.excludeFromApi) continue;
|
|
612
|
+
excluded.add(key);
|
|
613
|
+
const columnName = property.columnName;
|
|
614
|
+
if (typeof columnName === "string") excluded.add(columnName);
|
|
615
|
+
}
|
|
616
|
+
return excluded;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* The collection's primary key, as the key it is addressed by on the wire and
|
|
620
|
+
* the property that declares it.
|
|
621
|
+
*
|
|
622
|
+
* `id` was a literal in three places — seeded before the read loop, assigned
|
|
623
|
+
* after the input loop, inherited by the update schema from the input one — and
|
|
624
|
+
* the two spellings disagreed: a declared `id: { type: "number" }` overwrote
|
|
625
|
+
* the read seed and was overwritten by the input assignment, so the same field
|
|
626
|
+
* was `integer` in `Post` and `string` in `PostInput`. One helper now answers
|
|
627
|
+
* the question for all three.
|
|
628
|
+
*/
|
|
629
|
+
function idPropertyEntry(collection) {
|
|
630
|
+
for (const [key, property] of Object.entries(collection?.properties ?? {})) if (property?.isId) return [key, property];
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* The schema of a primary key or of a foreign key pointing at one: the declared
|
|
634
|
+
* property's own type, stripped of the field-level facts (description,
|
|
635
|
+
* validation bounds) that belong to the column and not to a reference to it.
|
|
636
|
+
*
|
|
637
|
+
* Falls back to `string` for a collection that declares no primary key, which
|
|
638
|
+
* is what every schema here assumed unconditionally before.
|
|
639
|
+
*/
|
|
640
|
+
function idSchemaFor(collection) {
|
|
641
|
+
const declared = idPropertyEntry(collection);
|
|
642
|
+
if (!declared) return { type: "string" };
|
|
643
|
+
const converted = convertPropertyToSchema(declared[1]);
|
|
644
|
+
const schema = { type: converted.type ?? "string" };
|
|
645
|
+
if (converted.format) schema.format = converted.format;
|
|
646
|
+
return schema;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Relations resolve or they do not; a document is still owed for a collection
|
|
650
|
+
* whose target thunk throws (a circular import, usually). The SDK generator
|
|
651
|
+
* warns and carries on with no relation fields, and so does this — the
|
|
652
|
+
* alternative is `/api/docs` 500ing for the whole project.
|
|
653
|
+
*/
|
|
654
|
+
function resolveRelationsForDocument(collection) {
|
|
655
|
+
try {
|
|
656
|
+
return resolveCollectionRelations(collection);
|
|
657
|
+
} catch {
|
|
658
|
+
return {};
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
/** Unwrap a target handed back as a module namespace — `() => import("./authors")`. */
|
|
662
|
+
function relationTarget(relation) {
|
|
663
|
+
try {
|
|
664
|
+
let target = relation.target();
|
|
665
|
+
if (target && (target.default || target.__esModule)) target = target.default ?? target;
|
|
666
|
+
return target;
|
|
667
|
+
} catch {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* The schema an *included* relation arrives as: the target's own row.
|
|
673
|
+
*
|
|
674
|
+
* `$ref` when the target is one of the collections this document describes, and
|
|
675
|
+
* an open object when it is not — a dangling pointer makes Swagger UI render an
|
|
676
|
+
* empty model and makes a strict generator abort, which is worse than a vague
|
|
677
|
+
* one.
|
|
678
|
+
*/
|
|
679
|
+
function includedRelationSchema(relation, registeredSchemas) {
|
|
680
|
+
const target = relationTarget(relation);
|
|
681
|
+
const targetSchema = target ? schemaNameFor(target) : void 0;
|
|
682
|
+
const item = targetSchema && registeredSchemas.has(targetSchema) ? { $ref: `#/components/schemas/${targetSchema}` } : { type: "object" };
|
|
683
|
+
return relation.cardinality === "many" ? {
|
|
684
|
+
type: "array",
|
|
685
|
+
items: item
|
|
686
|
+
} : item;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Emit a collection's relations onto a read schema: the foreign keys first,
|
|
690
|
+
* then the relations themselves.
|
|
691
|
+
*
|
|
692
|
+
* The order and the keys are the SDK `Row`'s, deliberately — the parity test
|
|
693
|
+
* next door compares the two key sets, and the only way that stays true is for
|
|
694
|
+
* both to be derived the same way rather than kept in step by hand.
|
|
695
|
+
*
|
|
696
|
+
* A `belongsTo` reaches the wire under the *field* name of its local column
|
|
697
|
+
* (`author_id` → `authorId`), which is what `fieldKeyForColumn` answers; a
|
|
698
|
+
* relation addressed by the same name as its own foreign key is served as
|
|
699
|
+
* either the scalar or the nested row depending on `include`, so it is
|
|
700
|
+
* documented as both.
|
|
701
|
+
*/
|
|
702
|
+
function emitRelationProperties(collection, properties, required, emitted, registeredSchemas) {
|
|
703
|
+
const resolved = resolveRelationsForDocument(collection);
|
|
704
|
+
for (const [relationKey, relation] of Object.entries(resolved)) {
|
|
705
|
+
if (relation.kind !== "belongsTo" || !relation.localKey) continue;
|
|
706
|
+
const fieldKey = fieldKeyForColumn(collection, relation.localKey);
|
|
707
|
+
if (emitted.has(fieldKey)) continue;
|
|
708
|
+
const foreignKey = idSchemaFor(relationTarget(relation));
|
|
709
|
+
const shadowedByInclude = relationKey === fieldKey;
|
|
710
|
+
properties[fieldKey] = shadowedByInclude ? { oneOf: [foreignKey, includedRelationSchema(relation, registeredSchemas)] } : {
|
|
711
|
+
...foreignKey,
|
|
712
|
+
description: `Foreign key into \`${relation.targetSlug}\``
|
|
713
|
+
};
|
|
714
|
+
emitted.add(fieldKey);
|
|
715
|
+
if (isRelationRequired(collection, relation) && !shadowedByInclude) required.push(fieldKey);
|
|
716
|
+
}
|
|
717
|
+
for (const [key, relation] of Object.entries(resolved)) {
|
|
718
|
+
if (emitted.has(key)) continue;
|
|
719
|
+
properties[key] = includedRelationSchema(relation, registeredSchemas);
|
|
720
|
+
emitted.add(key);
|
|
721
|
+
}
|
|
722
|
+
for (const [key, property] of Object.entries(collection.properties ?? {})) {
|
|
723
|
+
if (property?.type !== "relation") continue;
|
|
724
|
+
if (emitted.has(key)) continue;
|
|
725
|
+
properties[key] = { type: "object" };
|
|
726
|
+
emitted.add(key);
|
|
727
|
+
}
|
|
594
728
|
}
|
|
595
729
|
/**
|
|
596
730
|
* Build the component schema for a collection (output / read shape).
|
|
597
731
|
*
|
|
598
732
|
* Every declared property except the ones {@link isDocumentedProperty} rules
|
|
599
|
-
* out.
|
|
733
|
+
* out, plus the foreign keys and relations {@link emitRelationProperties} adds.
|
|
600
734
|
*/
|
|
601
|
-
function buildCollectionSchema(collection) {
|
|
602
|
-
const
|
|
603
|
-
|
|
735
|
+
function buildCollectionSchema(collection, registeredSchemas) {
|
|
736
|
+
const idKey = idPropertyEntry(collection)?.[0] ?? "id";
|
|
737
|
+
const properties = { [idKey]: {
|
|
738
|
+
...idSchemaFor(collection),
|
|
604
739
|
description: "Unique identifier"
|
|
605
740
|
} };
|
|
606
|
-
const required = [
|
|
741
|
+
const required = [idKey];
|
|
742
|
+
const excluded = excludedApiKeys(collection);
|
|
743
|
+
const emitted = new Set(excluded);
|
|
744
|
+
emitted.add(idKey);
|
|
607
745
|
for (const [key, property] of Object.entries(collection.properties)) {
|
|
746
|
+
if (property.type === "relation") continue;
|
|
608
747
|
if (!isDocumentedProperty(property)) continue;
|
|
609
748
|
properties[key] = convertPropertyToSchema(property);
|
|
610
|
-
|
|
749
|
+
emitted.add(key);
|
|
750
|
+
if (property.validation?.required && key !== idKey) required.push(key);
|
|
611
751
|
}
|
|
752
|
+
emitRelationProperties(collection, properties, required, emitted, registeredSchemas);
|
|
612
753
|
return {
|
|
613
754
|
type: "object",
|
|
614
755
|
required: required.length > 0 ? required : void 0,
|
|
@@ -670,26 +811,35 @@ function buildCollectionUpdateSchema(collection) {
|
|
|
670
811
|
/**
|
|
671
812
|
* Build an input schema (for POST/PUT) — excludes auto-generated fields.
|
|
672
813
|
*
|
|
673
|
-
* `excludeFromApi` columns are left out too,
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
814
|
+
* `excludeFromApi` columns are left out too, and the server now agrees: a write
|
|
815
|
+
* naming one is refused (`write-validation.ts`), which is what the flag's name
|
|
816
|
+
* and the generated SDK's own documentation always said. This comment used to
|
|
817
|
+
* record that such a write was "still accepted" — the document was right and
|
|
818
|
+
* the server was the thing that had not caught up.
|
|
678
819
|
*/
|
|
679
820
|
function buildCollectionInputSchema(collection) {
|
|
680
821
|
const properties = {};
|
|
681
822
|
const required = [];
|
|
823
|
+
const excluded = excludedApiKeys(collection);
|
|
824
|
+
const emitted = new Set(excluded);
|
|
825
|
+
const idKey = idPropertyEntry(collection)?.[0] ?? "id";
|
|
682
826
|
for (const [key, property] of Object.entries(collection.properties)) {
|
|
827
|
+
if (property.type === "relation") continue;
|
|
683
828
|
if (!isDocumentedProperty(property)) continue;
|
|
684
829
|
if (property.type === "date" && property.autoValue) continue;
|
|
685
830
|
if ("isId" in property && property.isId && property.isId !== "manual" && property.isId !== true) continue;
|
|
686
831
|
properties[key] = convertPropertyToSchema(property);
|
|
832
|
+
emitted.add(key);
|
|
687
833
|
if (property.validation?.required) required.push(key);
|
|
688
834
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
835
|
+
if (!emitted.has(idKey)) {
|
|
836
|
+
properties[idKey] = {
|
|
837
|
+
...idSchemaFor(collection),
|
|
838
|
+
description: "Optional: client-assigned ID. If omitted, the server generates one."
|
|
839
|
+
};
|
|
840
|
+
emitted.add(idKey);
|
|
841
|
+
}
|
|
842
|
+
emitWritableRelations(collection, properties, emitted);
|
|
693
843
|
return {
|
|
694
844
|
type: "object",
|
|
695
845
|
required: required.length > 0 ? required : void 0,
|
|
@@ -697,6 +847,38 @@ function buildCollectionInputSchema(collection) {
|
|
|
697
847
|
};
|
|
698
848
|
}
|
|
699
849
|
/**
|
|
850
|
+
* The writable half of a collection's relations: `belongsTo` only.
|
|
851
|
+
*
|
|
852
|
+
* A to-many is not writable through the body — the server links rows through
|
|
853
|
+
* the nested routes — so offering `tags: [...]` on a create would describe a
|
|
854
|
+
* write that does nothing. Same rule, same reason, as `emitWritableRelations`
|
|
855
|
+
* in the SDK generator, whose `Insert` type this mirrors key for key.
|
|
856
|
+
*
|
|
857
|
+
* Neither spelling is listed in `required`, even for a relation the collection
|
|
858
|
+
* declares `validation: { required: true }` on: the two keys are alternatives,
|
|
859
|
+
* and a schema naming both would tell a spec-validating gateway to reject a
|
|
860
|
+
* create that the server accepts. (The SDK's `Insert` marks both non-optional
|
|
861
|
+
* for the same relation, which is the same fact stated less carefully; a
|
|
862
|
+
* document is the half that a gateway enforces.)
|
|
863
|
+
*/
|
|
864
|
+
function emitWritableRelations(collection, properties, emitted) {
|
|
865
|
+
const resolved = resolveRelationsForDocument(collection);
|
|
866
|
+
const emit = (key, relation) => {
|
|
867
|
+
if (emitted.has(key)) return;
|
|
868
|
+
properties[key] = {
|
|
869
|
+
...idSchemaFor(relationTarget(relation)),
|
|
870
|
+
description: `The \`${relation.targetSlug}\` row this belongs to.`
|
|
871
|
+
};
|
|
872
|
+
emitted.add(key);
|
|
873
|
+
};
|
|
874
|
+
for (const relation of Object.values(resolved)) if (relation.kind === "belongsTo" && relation.localKey) emit(fieldKeyForColumn(collection, relation.localKey), relation);
|
|
875
|
+
for (const [key, property] of Object.entries(collection.properties ?? {})) {
|
|
876
|
+
if (property?.type !== "relation") continue;
|
|
877
|
+
const relation = findRelation(resolved, key);
|
|
878
|
+
if (relation?.kind === "belongsTo" && relation.localKey) emit(key, relation);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
700
882
|
* Convert a Rebase Property to an OpenAPI 3.0 schema object.
|
|
701
883
|
*/
|
|
702
884
|
function convertPropertyToSchema(property) {
|
|
@@ -726,7 +908,7 @@ function convertPropertyToSchema(property) {
|
|
|
726
908
|
}
|
|
727
909
|
case "number": {
|
|
728
910
|
const np = property;
|
|
729
|
-
base.type = np.validation?.integer || np.columnType === "integer" || np.columnType === "serial" || np.columnType === "bigserial" || np.columnType === "bigint" ? "integer" : "number";
|
|
911
|
+
base.type = np.validation?.integer || Boolean(np.isId) || np.columnType === "integer" || np.columnType === "serial" || np.columnType === "bigserial" || np.columnType === "bigint" ? "integer" : "number";
|
|
730
912
|
if (np.enum) {
|
|
731
913
|
const enumValues = resolveEnumValues(np.enum);
|
|
732
914
|
if (enumValues.length > 0) base.enum = enumValues;
|
|
@@ -851,6 +1033,7 @@ function buildFilterParameters(collection, reservedNames = /* @__PURE__ */ new S
|
|
|
851
1033
|
const params = [];
|
|
852
1034
|
for (const [key, property] of Object.entries(collection.properties)) {
|
|
853
1035
|
if (!isDocumentedProperty(property)) continue;
|
|
1036
|
+
if (property.type === "relation") continue;
|
|
854
1037
|
if (property.type === "map" || property.type === "array" || property.type === "geopoint") continue;
|
|
855
1038
|
if (reservedNames.has(key)) continue;
|
|
856
1039
|
params.push({
|
|
@@ -923,4 +1106,4 @@ function toPascalCase(str) {
|
|
|
923
1106
|
//#endregion
|
|
924
1107
|
export { generateOpenApiSpec };
|
|
925
1108
|
|
|
926
|
-
//# sourceMappingURL=openapi-generator-
|
|
1109
|
+
//# sourceMappingURL=openapi-generator-BIBbO1Tq.js.map
|