appos 0.3.2-0 → 0.3.3-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/auth-schema-CcqAJY9P.mjs +2 -0
- package/dist/bin/better-sqlite3-CuQ3hsWl.mjs +2 -0
- package/dist/bin/bun-sql-DGeo-s_M.mjs +2 -0
- package/dist/bin/cache-3oO07miM.mjs +2 -0
- package/dist/bin/chunk-l9p7A9gZ.mjs +2 -0
- package/dist/bin/cockroach-BaICwY7N.mjs +2 -0
- package/dist/bin/database-CaysWPpa.mjs +2 -0
- package/dist/bin/esm-BvsccvmM.mjs +2 -0
- package/dist/bin/esm-CGKzJ7Am.mjs +3 -0
- package/dist/bin/event-DnSe3eh0.mjs +8 -0
- package/dist/bin/extract-blob-metadata-iqwTl2ft.mjs +170 -0
- package/dist/bin/generate-image-variant-Lyx0vhM6.mjs +2 -0
- package/dist/bin/generate-preview-0MrKxslA.mjs +2 -0
- package/dist/bin/libsql-DQJrZsU9.mjs +2 -0
- package/dist/bin/logger-BAGZLUzj.mjs +2 -0
- package/dist/bin/main.mjs +1201 -190
- package/dist/bin/migrator-B7iNKM8N.mjs +2 -0
- package/dist/bin/migrator-BKE1cSQQ.mjs +2 -0
- package/dist/bin/migrator-BXcbc9zs.mjs +2 -0
- package/dist/bin/migrator-B_XhRWZC.mjs +8 -0
- package/dist/bin/migrator-Bz52Gtr8.mjs +2 -0
- package/dist/bin/migrator-C7W-cZHB.mjs +2 -0
- package/dist/bin/migrator-CEnKyGSW.mjs +2 -0
- package/dist/bin/migrator-CHzIIl5X.mjs +2 -0
- package/dist/bin/migrator-CR-rjZdM.mjs +2 -0
- package/dist/bin/migrator-CjIr1ZCx.mjs +8 -0
- package/dist/bin/migrator-Cuubh2dg.mjs +2 -0
- package/dist/bin/migrator-D8m-ORbr.mjs +8 -0
- package/dist/bin/migrator-DBFwrhZH.mjs +2 -0
- package/dist/bin/migrator-DLmhW9u_.mjs +2 -0
- package/dist/bin/migrator-DLoHx807.mjs +4 -0
- package/dist/bin/migrator-DtN_iS87.mjs +2 -0
- package/dist/bin/migrator-Yc57lb3w.mjs +2 -0
- package/dist/bin/migrator-cEVXH3xC.mjs +2 -0
- package/dist/bin/migrator-hWi-sYIq.mjs +2 -0
- package/dist/bin/mysql2-DufFWkj4.mjs +2 -0
- package/dist/bin/neon-serverless-5a4h2VFz.mjs +2 -0
- package/dist/bin/node-CiOp4xrR.mjs +22 -0
- package/dist/bin/node-mssql-DvZGaUkB.mjs +322 -0
- package/dist/bin/node-postgres-BqbJVBQY.mjs +2 -0
- package/dist/bin/node-postgres-DnhRTTO8.mjs +2 -0
- package/dist/bin/open-0ksnL0S8.mjs +2 -0
- package/dist/bin/pdf-sUYeFPr4.mjs +14 -0
- package/dist/bin/pg-CaH8ptj-.mjs +2 -0
- package/dist/bin/pg-core-BLTZt9AH.mjs +8 -0
- package/dist/bin/pg-core-CGzidKaA.mjs +2 -0
- package/dist/bin/pglite-BJB9z7Ju.mjs +2 -0
- package/dist/bin/planetscale-serverless-H3RfLlMK.mjs +13 -0
- package/dist/bin/postgres-js-DuOf1eWm.mjs +2 -0
- package/dist/bin/purge-attachment-DQXpTtTx.mjs +2 -0
- package/dist/bin/purge-audit-logs-BEt2J2gD.mjs +2 -0
- package/dist/bin/{purge-unattached-blobs-Duvv8Izd.mjs → purge-unattached-blobs-DOmk4ddJ.mjs} +1 -1
- package/dist/bin/query-builder-DSRrR6X_.mjs +8 -0
- package/dist/bin/query-builder-V8-LDhvA.mjs +3 -0
- package/dist/bin/session-CdB1A-LB.mjs +14 -0
- package/dist/bin/session-Cl2e-_i8.mjs +8 -0
- package/dist/bin/singlestore-COft6TlR.mjs +8 -0
- package/dist/bin/sql-D-eKV1Dn.mjs +2 -0
- package/dist/bin/sqlite-cloud-Co9jOn5G.mjs +2 -0
- package/dist/bin/sqlite-proxy-Cpu78gJF.mjs +2 -0
- package/dist/bin/src-C-oXmCzx.mjs +6 -0
- package/dist/bin/table-3zUpWkMg.mjs +2 -0
- package/dist/bin/track-db-changes-DWyY5jXm.mjs +2 -0
- package/dist/bin/utils-CyoeCJlf.mjs +2 -0
- package/dist/bin/utils-EoqYQKy1.mjs +2 -0
- package/dist/bin/utils-bsypyqPl.mjs +2 -0
- package/dist/bin/vercel-postgres-HWL6xtqi.mjs +2 -0
- package/dist/bin/workflow-zxHDyfLq.mjs +2 -0
- package/dist/bin/youch-handler-DrYdbUhe.mjs +2 -0
- package/dist/bin/zod-MJjkEkRY.mjs +24 -0
- package/dist/exports/api/_virtual/rolldown_runtime.mjs +36 -1
- package/dist/exports/api/app-context.mjs +24 -1
- package/dist/exports/api/auth-schema.mjs +373 -1
- package/dist/exports/api/auth.d.mts +4 -0
- package/dist/exports/api/auth.mjs +188 -1
- package/dist/exports/api/cache.d.mts +2 -2
- package/dist/exports/api/cache.mjs +28 -1
- package/dist/exports/api/config.mjs +72 -1
- package/dist/exports/api/constants.mjs +92 -1
- package/dist/exports/api/container.mjs +49 -1
- package/dist/exports/api/database.mjs +218 -1
- package/dist/exports/api/event.mjs +236 -1
- package/dist/exports/api/i18n.mjs +45 -1
- package/dist/exports/api/index.mjs +20 -1
- package/dist/exports/api/instrumentation.mjs +40 -1
- package/dist/exports/api/logger.mjs +26 -1
- package/dist/exports/api/mailer.mjs +37 -1
- package/dist/exports/api/middleware.mjs +73 -1
- package/dist/exports/api/openapi.mjs +507 -1
- package/dist/exports/api/orm.mjs +43 -1
- package/dist/exports/api/otel.mjs +56 -1
- package/dist/exports/api/redis.mjs +41 -1
- package/dist/exports/api/storage-schema.mjs +72 -1
- package/dist/exports/api/storage.mjs +833 -1
- package/dist/exports/api/web/auth.mjs +17 -1
- package/dist/exports/api/workflow.mjs +196 -1
- package/dist/exports/api/workflows/_virtual/rolldown_runtime.mjs +36 -1
- package/dist/exports/api/workflows/api/auth-schema.mjs +373 -1
- package/dist/exports/api/workflows/api/auth.d.mts +4 -0
- package/dist/exports/api/workflows/api/cache.d.mts +2 -2
- package/dist/exports/api/workflows/api/event.mjs +126 -1
- package/dist/exports/api/workflows/api/redis.mjs +3 -1
- package/dist/exports/api/workflows/api/workflow.mjs +135 -1
- package/dist/exports/api/workflows/constants.mjs +23 -1
- package/dist/exports/api/workflows/extract-blob-metadata.mjs +132 -1
- package/dist/exports/api/workflows/generate-image-variant.d.mts +2 -2
- package/dist/exports/api/workflows/generate-image-variant.mjs +118 -1
- package/dist/exports/api/workflows/generate-preview.mjs +160 -1
- package/dist/exports/api/workflows/index.mjs +3 -1
- package/dist/exports/api/workflows/purge-attachment.mjs +34 -1
- package/dist/exports/api/workflows/purge-audit-logs.mjs +47 -1
- package/dist/exports/api/workflows/purge-unattached-blobs.mjs +46 -1
- package/dist/exports/api/workflows/track-db-changes.mjs +110 -1
- package/dist/exports/cli/_virtual/rolldown_runtime.mjs +36 -1
- package/dist/exports/cli/api/auth-schema.mjs +373 -1
- package/dist/exports/cli/api/auth.d.mts +4 -0
- package/dist/exports/cli/api/cache.d.mts +2 -2
- package/dist/exports/cli/api/event.mjs +126 -1
- package/dist/exports/cli/api/redis.mjs +3 -1
- package/dist/exports/cli/api/workflow.mjs +135 -1
- package/dist/exports/cli/api/workflows/extract-blob-metadata.mjs +132 -1
- package/dist/exports/cli/api/workflows/generate-image-variant.mjs +118 -1
- package/dist/exports/cli/api/workflows/generate-preview.mjs +160 -1
- package/dist/exports/cli/api/workflows/purge-attachment.mjs +34 -1
- package/dist/exports/cli/api/workflows/purge-audit-logs.mjs +47 -1
- package/dist/exports/cli/api/workflows/purge-unattached-blobs.mjs +46 -1
- package/dist/exports/cli/api/workflows/track-db-changes.mjs +110 -1
- package/dist/exports/cli/command.d.mts +2 -0
- package/dist/exports/cli/command.mjs +43 -1
- package/dist/exports/cli/constants.mjs +23 -1
- package/dist/exports/cli/index.mjs +3 -1
- package/dist/exports/devtools/index.js +4 -1
- package/dist/exports/tests/api/auth.d.mts +4 -0
- package/dist/exports/tests/api/cache.d.mts +2 -2
- package/dist/exports/tests/api/middleware/i18n.mjs +1 -1
- package/dist/exports/tests/api/middleware/youch-handler.mjs +1 -1
- package/dist/exports/tests/api/openapi.mjs +1 -1
- package/dist/exports/tests/api/server.mjs +1 -1
- package/dist/exports/tests/constants.mjs +1 -1
- package/dist/exports/vendors/date.js +1 -1
- package/dist/exports/vendors/toolkit.js +1 -1
- package/dist/exports/vendors/zod.js +1 -1
- package/dist/exports/vitest/globals.mjs +1 -1
- package/dist/exports/web/auth.js +75 -1
- package/dist/exports/web/i18n.js +45 -1
- package/dist/exports/web/index.js +8 -1
- package/package.json +19 -17
- package/dist/bin/auth-schema-Va0CYicu.mjs +0 -2
- package/dist/bin/event-8JibGFH_.mjs +0 -2
- package/dist/bin/extract-blob-metadata-DjPfHtQ2.mjs +0 -2
- package/dist/bin/generate-image-variant-D5VDFyWj.mjs +0 -2
- package/dist/bin/generate-preview-Dssw7w5U.mjs +0 -2
- package/dist/bin/purge-attachment-BBPzIxwt.mjs +0 -2
- package/dist/bin/purge-audit-logs-BeZy3IFM.mjs +0 -2
- package/dist/bin/track-db-changes-CFykw_YO.mjs +0 -2
- package/dist/bin/workflow-BNUZrj4F.mjs +0 -2
- package/dist/bin/youch-handler-BadUgHb0.mjs +0 -2
|
@@ -1 +1,507 @@
|
|
|
1
|
-
import{defineAppContext
|
|
1
|
+
import { defineAppContext } from "./app-context.mjs";
|
|
2
|
+
import { APPOS_DIR, FILE_EXT, PUBLIC_DIR, ROUTES_DIR } from "./constants.mjs";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
6
|
+
import { remixRoutesOptionAdapter } from "@react-router/remix-routes-option-adapter";
|
|
7
|
+
import { isEmpty } from "es-toolkit/compat";
|
|
8
|
+
import { flatRoutes } from "remix-flat-routes";
|
|
9
|
+
import { createDocument } from "zod-openapi";
|
|
10
|
+
|
|
11
|
+
//#region src/api/openapi.ts
|
|
12
|
+
const outputDir = resolve(PUBLIC_DIR, "openapi");
|
|
13
|
+
/**
|
|
14
|
+
* Define typed response builders based on defined response schemas.
|
|
15
|
+
*/
|
|
16
|
+
function defineTypedResponses(responses) {
|
|
17
|
+
const builder = {};
|
|
18
|
+
for (const [statusCode, responseSpec] of Object.entries(responses)) {
|
|
19
|
+
const status = Number(statusCode);
|
|
20
|
+
const methodName = getMethodNameForStatus(status);
|
|
21
|
+
const schema = responseSpec.schema;
|
|
22
|
+
let strictSchema = schema;
|
|
23
|
+
if (schema instanceof z.ZodObject) strictSchema = schema.strict();
|
|
24
|
+
else if (schema instanceof z.ZodArray) {
|
|
25
|
+
const elementSchema = schema.element;
|
|
26
|
+
if (elementSchema instanceof z.ZodObject) strictSchema = z.array(elementSchema.strict());
|
|
27
|
+
}
|
|
28
|
+
if (methodName) builder[methodName] = (data) => {
|
|
29
|
+
if (schema instanceof z.ZodNull || data === null || data === void 0) return {
|
|
30
|
+
__response: true,
|
|
31
|
+
status,
|
|
32
|
+
data: data ?? null
|
|
33
|
+
};
|
|
34
|
+
try {
|
|
35
|
+
return {
|
|
36
|
+
__response: true,
|
|
37
|
+
status,
|
|
38
|
+
data: strictSchema.parse(data)
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error instanceof z.ZodError) throw new Error(`Response validation failed for status ${status}: ${JSON.stringify(error.issues, null, 2)}\n\nData provided: ${JSON.stringify(data, null, 2)}`);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const strictSchemaCache = /* @__PURE__ */ new Map();
|
|
47
|
+
for (const [statusCode, responseSpec] of Object.entries(responses)) {
|
|
48
|
+
const status = Number(statusCode);
|
|
49
|
+
const schema = responseSpec.schema;
|
|
50
|
+
let strictSchema = schema;
|
|
51
|
+
if (schema instanceof z.ZodObject) strictSchema = schema.strict();
|
|
52
|
+
else if (schema instanceof z.ZodArray) {
|
|
53
|
+
const elementSchema = schema.element;
|
|
54
|
+
if (elementSchema instanceof z.ZodObject) strictSchema = z.array(elementSchema.strict());
|
|
55
|
+
}
|
|
56
|
+
strictSchemaCache.set(status, strictSchema);
|
|
57
|
+
}
|
|
58
|
+
builder.status = (statusCode, data) => {
|
|
59
|
+
const status = Number(statusCode);
|
|
60
|
+
const responseSpec = responses[statusCode];
|
|
61
|
+
if (!responseSpec) return {
|
|
62
|
+
__response: true,
|
|
63
|
+
status,
|
|
64
|
+
data: data ?? null
|
|
65
|
+
};
|
|
66
|
+
const schema = responseSpec.schema;
|
|
67
|
+
if (!schema) return {
|
|
68
|
+
__response: true,
|
|
69
|
+
status,
|
|
70
|
+
data: data ?? null
|
|
71
|
+
};
|
|
72
|
+
if (schema instanceof z.ZodNull || data === null || data === void 0) return {
|
|
73
|
+
__response: true,
|
|
74
|
+
status,
|
|
75
|
+
data: data ?? null
|
|
76
|
+
};
|
|
77
|
+
const strictSchema = strictSchemaCache.get(status) || schema;
|
|
78
|
+
try {
|
|
79
|
+
return {
|
|
80
|
+
__response: true,
|
|
81
|
+
status,
|
|
82
|
+
data: strictSchema.parse(data)
|
|
83
|
+
};
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error instanceof z.ZodError) throw new Error(`Response validation failed for status ${status}: ${JSON.stringify(error.issues, null, 2)}\n\nData provided: ${JSON.stringify(data, null, 2)}`);
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
return builder;
|
|
90
|
+
}
|
|
91
|
+
function getMethodNameForStatus(status) {
|
|
92
|
+
return {
|
|
93
|
+
200: "ok",
|
|
94
|
+
201: "created",
|
|
95
|
+
202: "accepted",
|
|
96
|
+
204: "noContent",
|
|
97
|
+
400: "badRequest",
|
|
98
|
+
401: "unauthorized",
|
|
99
|
+
403: "forbidden",
|
|
100
|
+
404: "notFound",
|
|
101
|
+
409: "conflict",
|
|
102
|
+
422: "unprocessableEntity",
|
|
103
|
+
500: "internalServerError"
|
|
104
|
+
}[status] || null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Helper function to define an OpenAPI endpoint with full type safety.
|
|
108
|
+
*/
|
|
109
|
+
function defineOpenAPIEndpoint(spec) {
|
|
110
|
+
return spec;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Parse and validate request parameters with Zod schema
|
|
114
|
+
*/
|
|
115
|
+
function parseParams(schema, rawParams) {
|
|
116
|
+
if (!schema) return rawParams;
|
|
117
|
+
return schema.parse(rawParams);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Parse and validate query parameters with Zod schema
|
|
121
|
+
*/
|
|
122
|
+
function parseQuery(schema, url) {
|
|
123
|
+
const queryObj = {};
|
|
124
|
+
for (const [key, value] of url.searchParams.entries()) {
|
|
125
|
+
const existing = queryObj[key];
|
|
126
|
+
if (existing === void 0) queryObj[key] = value;
|
|
127
|
+
else if (Array.isArray(existing)) existing.push(value);
|
|
128
|
+
else queryObj[key] = [existing, value];
|
|
129
|
+
}
|
|
130
|
+
if (!schema) return queryObj;
|
|
131
|
+
return schema.parse(queryObj);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Parse and validate request body with Zod schema
|
|
135
|
+
* Note: Express body is already parsed by express.json() middleware
|
|
136
|
+
*/
|
|
137
|
+
function parseBody(schema, req) {
|
|
138
|
+
const bodyData = req.body;
|
|
139
|
+
if (!schema) return bodyData;
|
|
140
|
+
return schema.parse(bodyData);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Parse and validate request headers with Zod schema
|
|
144
|
+
* Note: Express req.headers is already a plain object
|
|
145
|
+
*/
|
|
146
|
+
function parseHeaders(schema, req) {
|
|
147
|
+
const headersObj = req.headers;
|
|
148
|
+
if (!schema) return headersObj;
|
|
149
|
+
return schema.parse(headersObj);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Get appropriate HTTP status code for validation error type
|
|
153
|
+
*/
|
|
154
|
+
function getValidationStatusCode(errorType) {
|
|
155
|
+
switch (errorType) {
|
|
156
|
+
case "params":
|
|
157
|
+
case "query":
|
|
158
|
+
case "headers": return 400;
|
|
159
|
+
case "body": return 422;
|
|
160
|
+
default: return 400;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Create RFC 9457 compliant validation error response
|
|
165
|
+
*/
|
|
166
|
+
function createValidationErrorResponse(error, errorType) {
|
|
167
|
+
const status = getValidationStatusCode(errorType);
|
|
168
|
+
return {
|
|
169
|
+
__response: true,
|
|
170
|
+
status,
|
|
171
|
+
data: {
|
|
172
|
+
error: "validation_error",
|
|
173
|
+
status,
|
|
174
|
+
detail: "Request validation failed",
|
|
175
|
+
errors: error.issues.map((issue) => ({
|
|
176
|
+
field: issue.path.join("."),
|
|
177
|
+
message: issue.message,
|
|
178
|
+
received: "received" in issue ? issue.received : issue.input
|
|
179
|
+
}))
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Define Express handler for a single HTTP method.
|
|
185
|
+
*
|
|
186
|
+
* Builds a unified AppContext from the Express request, including:
|
|
187
|
+
* - container and workflows from app.locals
|
|
188
|
+
* - request metadata (id, ipAddress, userAgent, headers)
|
|
189
|
+
* - auth state (user, session) from Better Auth
|
|
190
|
+
*/
|
|
191
|
+
function defineExpressHandler(spec, method) {
|
|
192
|
+
const responseBuilder = defineTypedResponses(spec.responses);
|
|
193
|
+
return async (req, res, next) => {
|
|
194
|
+
try {
|
|
195
|
+
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
196
|
+
const parsedParams = parseParams(spec.params, req.params);
|
|
197
|
+
const parsedQuery = parseQuery(spec.query, url);
|
|
198
|
+
const parsedBody = parseBody(spec.requestBody, req);
|
|
199
|
+
const parsedHeaders = parseHeaders(spec.headers, req);
|
|
200
|
+
const ctx = await defineAppContext({
|
|
201
|
+
apiKey: req.apiKey,
|
|
202
|
+
container: req.app.locals.container,
|
|
203
|
+
request: req,
|
|
204
|
+
fetchSession: false
|
|
205
|
+
});
|
|
206
|
+
const result = await spec.handler({
|
|
207
|
+
ctx,
|
|
208
|
+
body: parsedBody,
|
|
209
|
+
headers: parsedHeaders,
|
|
210
|
+
params: parsedParams,
|
|
211
|
+
query: parsedQuery,
|
|
212
|
+
request: req,
|
|
213
|
+
response: responseBuilder
|
|
214
|
+
});
|
|
215
|
+
if (result && typeof result === "object" && "__response" in result) {
|
|
216
|
+
const marker = result;
|
|
217
|
+
if (marker.data === null || marker.data === void 0) res.status(marker.status).end();
|
|
218
|
+
else res.status(marker.status).json(marker.data);
|
|
219
|
+
} else res.status(200).json(result);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (error instanceof z.ZodError) {
|
|
222
|
+
const errorResponse = createValidationErrorResponse(error, method === "GET" || method === "HEAD" ? "query" : "body");
|
|
223
|
+
res.status(errorResponse.status).json(errorResponse.data);
|
|
224
|
+
} else next(error);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Generate OpenAPI specification array for CLI registration.
|
|
230
|
+
*/
|
|
231
|
+
function generateOpenAPISpec(config) {
|
|
232
|
+
const specs = [];
|
|
233
|
+
for (const method of [
|
|
234
|
+
"GET",
|
|
235
|
+
"HEAD",
|
|
236
|
+
"POST",
|
|
237
|
+
"PUT",
|
|
238
|
+
"PATCH",
|
|
239
|
+
"DELETE"
|
|
240
|
+
]) {
|
|
241
|
+
const spec = config[method];
|
|
242
|
+
if (!spec) continue;
|
|
243
|
+
const request = {};
|
|
244
|
+
if (spec.params) request.params = spec.params;
|
|
245
|
+
if (spec.query) request.query = spec.query;
|
|
246
|
+
if (spec.requestBody) request.body = spec.requestBody;
|
|
247
|
+
if (spec.headers) request.headers = spec.headers;
|
|
248
|
+
const responses = {};
|
|
249
|
+
for (const [statusCode, responseSpec] of Object.entries(spec.responses)) responses[Number(statusCode)] = {
|
|
250
|
+
description: responseSpec.description,
|
|
251
|
+
content: { "application/json": { schema: responseSpec.schema } }
|
|
252
|
+
};
|
|
253
|
+
specs.push({
|
|
254
|
+
method: method.toLowerCase(),
|
|
255
|
+
summary: spec.summary,
|
|
256
|
+
description: spec.description,
|
|
257
|
+
request: Object.keys(request).length > 0 ? request : void 0,
|
|
258
|
+
responses
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return specs;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Define OpenAPI-based route handlers with full type safety.
|
|
265
|
+
*
|
|
266
|
+
* @param config Configuration object with HTTP method specifications
|
|
267
|
+
* @returns Object containing Express handlers and openAPISpec
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```typescript
|
|
271
|
+
* // Basic usage
|
|
272
|
+
* export default defineOpenAPI({
|
|
273
|
+
* GET: {
|
|
274
|
+
* summary: "Get user by ID",
|
|
275
|
+
* params: z.object({ id: z.uuid() }),
|
|
276
|
+
* responses: {
|
|
277
|
+
* 200: { description: "User data", schema: UserSchema }
|
|
278
|
+
* },
|
|
279
|
+
* handler: async ({ params, response }) => {
|
|
280
|
+
* const user = await getUser(params.id);
|
|
281
|
+
* return response.ok(user); // Fully typed!
|
|
282
|
+
* }
|
|
283
|
+
* },
|
|
284
|
+
* POST: {
|
|
285
|
+
* summary: "Create user",
|
|
286
|
+
* requestBody: CreateUserSchema,
|
|
287
|
+
* responses: {
|
|
288
|
+
* 201: { description: "User created", schema: UserSchema }
|
|
289
|
+
* },
|
|
290
|
+
* handler: async ({ body, response }) => {
|
|
291
|
+
* const user = await createUser(body);
|
|
292
|
+
* return response.created(user); // Fully typed!
|
|
293
|
+
* }
|
|
294
|
+
* }
|
|
295
|
+
* });
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
function defineOpenAPI(config) {
|
|
299
|
+
const handlers = {};
|
|
300
|
+
for (const method of [
|
|
301
|
+
"GET",
|
|
302
|
+
"HEAD",
|
|
303
|
+
"POST",
|
|
304
|
+
"PUT",
|
|
305
|
+
"PATCH",
|
|
306
|
+
"DELETE"
|
|
307
|
+
]) {
|
|
308
|
+
const spec = config[method];
|
|
309
|
+
if (spec) handlers[method] = defineExpressHandler(spec, method);
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
handlers,
|
|
313
|
+
openAPISpec: generateOpenAPISpec(config)
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Define OpenAPI document configuration with improved DX.
|
|
318
|
+
*
|
|
319
|
+
* Only allows specifying info and servers objects, ensuring a consistent
|
|
320
|
+
* OpenAPI v3.1.0 structure while preventing configuration of other fields.
|
|
321
|
+
*
|
|
322
|
+
* @param config Configuration object with info and optional servers
|
|
323
|
+
* @returns Complete OpenAPI configuration object
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
* ```typescript
|
|
327
|
+
* export const openAPIConfig = defineOpenAPIConfig({
|
|
328
|
+
* info: {
|
|
329
|
+
* title: "My API",
|
|
330
|
+
* version: "1.0.0",
|
|
331
|
+
* description: "API v1 - Authentication and core features",
|
|
332
|
+
* contact: {
|
|
333
|
+
* name: "API Support",
|
|
334
|
+
* email: "support@example.com",
|
|
335
|
+
* },
|
|
336
|
+
* },
|
|
337
|
+
* servers: [
|
|
338
|
+
* {
|
|
339
|
+
* url: process.env.API_URL || "http://localhost:8000",
|
|
340
|
+
* description: "API Server",
|
|
341
|
+
* },
|
|
342
|
+
* ],
|
|
343
|
+
* });
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
function defineOpenAPIConfig(config) {
|
|
347
|
+
return (container) => {
|
|
348
|
+
const input = config(container);
|
|
349
|
+
return {
|
|
350
|
+
info: input.info,
|
|
351
|
+
servers: input.servers,
|
|
352
|
+
openapi: "3.1.0"
|
|
353
|
+
};
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Extract API version from route path.
|
|
358
|
+
*/
|
|
359
|
+
function extractVersion(path) {
|
|
360
|
+
return path.match(/^\/(v\d+)/)?.[1];
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Convert React Router path params (:param) to OpenAPI format ({param}).
|
|
364
|
+
*/
|
|
365
|
+
function convertToOpenAPIPath(reactRouterPath) {
|
|
366
|
+
return reactRouterPath.replace(/:(\w+)/g, "{$1}");
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Build an OpenAPI path object from a route specification.
|
|
370
|
+
*/
|
|
371
|
+
function buildPathObject(path, spec) {
|
|
372
|
+
const openApiPath = convertToOpenAPIPath(path);
|
|
373
|
+
const methodConfig = {
|
|
374
|
+
summary: spec.summary,
|
|
375
|
+
description: spec.description
|
|
376
|
+
};
|
|
377
|
+
if (spec.request?.params || spec.request?.query || spec.request?.headers) {
|
|
378
|
+
methodConfig.requestParams = {};
|
|
379
|
+
if (spec.request.params) methodConfig.requestParams.path = spec.request.params;
|
|
380
|
+
if (spec.request.query) methodConfig.requestParams.query = spec.request.query;
|
|
381
|
+
if (spec.request.headers) methodConfig.requestParams.header = spec.request.headers;
|
|
382
|
+
}
|
|
383
|
+
if (spec.request?.body) methodConfig.requestBody = { content: { "application/json": { schema: spec.request.body } } };
|
|
384
|
+
methodConfig.responses = {};
|
|
385
|
+
for (const [statusCode, response] of Object.entries(spec.responses)) methodConfig.responses[statusCode] = {
|
|
386
|
+
description: response.description,
|
|
387
|
+
content: { "application/json": { schema: response.content["application/json"].schema } }
|
|
388
|
+
};
|
|
389
|
+
return {
|
|
390
|
+
path: openApiPath,
|
|
391
|
+
config: methodConfig
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Generate complete OpenAPI 3.1.0 document from routes using zod-openapi
|
|
396
|
+
*/
|
|
397
|
+
function generateOpenAPIDocument(routes, version, config) {
|
|
398
|
+
const paths = {};
|
|
399
|
+
for (const route of routes) {
|
|
400
|
+
if (route.version !== version) continue;
|
|
401
|
+
const apiPath = route.path.replace(`/${version}`, "") || "/";
|
|
402
|
+
for (const spec of route.openAPISpec) {
|
|
403
|
+
const { path: openApiPath, config: methodConfig } = buildPathObject(apiPath, spec);
|
|
404
|
+
if (!paths[openApiPath]) paths[openApiPath] = {};
|
|
405
|
+
paths[openApiPath][spec.method] = methodConfig;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return createDocument({
|
|
409
|
+
openapi: "3.1.0",
|
|
410
|
+
info: config?.info || {
|
|
411
|
+
title: `API ${version.toUpperCase()}`,
|
|
412
|
+
version: version.replace("v", ""),
|
|
413
|
+
description: `OpenAPI specification for ${version} API`
|
|
414
|
+
},
|
|
415
|
+
servers: config?.servers || [{
|
|
416
|
+
url: `http://localhost:8000/${version}`,
|
|
417
|
+
description: process.env.NODE_ENV === "production" ? "Production server" : "Development server"
|
|
418
|
+
}],
|
|
419
|
+
paths
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Scan routes directory and convert to Express route paths using remix-flat-routes.
|
|
424
|
+
*/
|
|
425
|
+
async function scanAPIRoutes(container) {
|
|
426
|
+
const fullPath = resolve(APPOS_DIR, ROUTES_DIR);
|
|
427
|
+
try {
|
|
428
|
+
await access(fullPath);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
container.logger.error({ error }, "OpenAPI routes directory not found");
|
|
431
|
+
return [];
|
|
432
|
+
}
|
|
433
|
+
const routeManifest = await remixRoutesOptionAdapter((defineRoutes) => {
|
|
434
|
+
return flatRoutes(ROUTES_DIR, defineRoutes, {
|
|
435
|
+
appDir: APPOS_DIR,
|
|
436
|
+
ignoredRouteFiles: [
|
|
437
|
+
"**/.*",
|
|
438
|
+
"**/*.{spec,test}.{ts,tsx}",
|
|
439
|
+
"**/*-????????.{js,ts}"
|
|
440
|
+
]
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
const routes = [];
|
|
444
|
+
for (const [routeId, route] of Object.entries(routeManifest)) {
|
|
445
|
+
const absolutePath = resolve(APPOS_DIR, route.file).replace(/\.ts$/, `.${FILE_EXT}`);
|
|
446
|
+
try {
|
|
447
|
+
const exported = (await import(absolutePath)).default;
|
|
448
|
+
if (!exported || isEmpty(exported) || /\/openapi.(j|t)s$/.test(absolutePath)) continue;
|
|
449
|
+
if (!exported?.handlers && !exported?.openAPISpec) {
|
|
450
|
+
container.logger.warn(`Missing default export with 'defineOpenAPI' for '${APPOS_DIR}/${route.file}'`);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const routePath = `/${route.path}`;
|
|
454
|
+
routes.push({
|
|
455
|
+
path: routePath,
|
|
456
|
+
filePath: absolutePath,
|
|
457
|
+
handlers: exported.handlers,
|
|
458
|
+
openAPISpec: exported.openAPISpec,
|
|
459
|
+
version: extractVersion(routePath)
|
|
460
|
+
});
|
|
461
|
+
} catch (error) {
|
|
462
|
+
container.logger.error(error, `Error loading route ${route.file}:`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return routes;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Register scanned routes with Express app.
|
|
469
|
+
*/
|
|
470
|
+
function registerRoutes(app, routes) {
|
|
471
|
+
for (const route of routes) for (const [method, handler] of Object.entries(route.handlers)) app[method.toLowerCase()](route.path, handler);
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Load version-specific OpenAPI config from openapi.ts file.
|
|
475
|
+
*/
|
|
476
|
+
async function loadVersionConfig(container, version) {
|
|
477
|
+
const configPath = resolve(APPOS_DIR, ROUTES_DIR, `${version}+/openapi.${FILE_EXT}`);
|
|
478
|
+
try {
|
|
479
|
+
const configFn = (await import(configPath)).default;
|
|
480
|
+
return configFn ? configFn(container) : void 0;
|
|
481
|
+
} catch {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Write OpenAPI specs to api/public/openapi directory.
|
|
487
|
+
*/
|
|
488
|
+
async function writeOpenAPISpecs(container, routes) {
|
|
489
|
+
const versions = [...new Set(routes.map((r) => r.version).filter(Boolean))];
|
|
490
|
+
if (versions.length > 0) await mkdir(outputDir, { recursive: true });
|
|
491
|
+
for (const version of versions) {
|
|
492
|
+
const doc = generateOpenAPIDocument(routes, version, await loadVersionConfig(container, version));
|
|
493
|
+
await writeFile(join(outputDir, `${version}.json`), JSON.stringify(doc, null, 2), "utf-8");
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Load and register API routes with Express (convenience function).
|
|
498
|
+
* Also auto-generates OpenAPI specs in development mode.
|
|
499
|
+
*/
|
|
500
|
+
async function loadAndRegisterAPIRoutes(app) {
|
|
501
|
+
const routes = await scanAPIRoutes(app.locals.container);
|
|
502
|
+
registerRoutes(app, routes);
|
|
503
|
+
if (process.env.NODE_ENV !== "production") await writeOpenAPISpecs(app.locals.container, routes);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
//#endregion
|
|
507
|
+
export { defineOpenAPI, defineOpenAPIConfig, defineOpenAPIEndpoint, defineTypedResponses, generateOpenAPIDocument, loadAndRegisterAPIRoutes, registerRoutes, scanAPIRoutes, writeOpenAPISpecs };
|
package/dist/exports/api/orm.mjs
CHANGED
|
@@ -1 +1,43 @@
|
|
|
1
|
-
import{__export
|
|
1
|
+
import { __export, __reExport } from "./_virtual/rolldown_runtime.mjs";
|
|
2
|
+
import { index, isPgEnum, isPgMaterializedView, isPgSchema, isPgSequence, isPgView, numeric, parsePgArray, parsePgNestedArray, pgEnum, pgMaterializedView, pgPolicy, pgRole, pgSchema, pgSequence, pgTable, pgTableCreator, pgView, serial, smallint, smallserial, sparsevec, unique, uniqueIndex, uniqueKeyName, withReplicas } from "drizzle-orm/pg-core";
|
|
3
|
+
|
|
4
|
+
export * from "drizzle-orm"
|
|
5
|
+
|
|
6
|
+
export * from "drizzle-seed"
|
|
7
|
+
|
|
8
|
+
//#region src/api/orm.ts
|
|
9
|
+
var orm_exports = /* @__PURE__ */ __export({
|
|
10
|
+
index: () => index,
|
|
11
|
+
isPgEnum: () => isPgEnum,
|
|
12
|
+
isPgMaterializedView: () => isPgMaterializedView,
|
|
13
|
+
isPgSchema: () => isPgSchema,
|
|
14
|
+
isPgSequence: () => isPgSequence,
|
|
15
|
+
isPgView: () => isPgView,
|
|
16
|
+
numeric: () => numeric,
|
|
17
|
+
parsePgArray: () => parsePgArray,
|
|
18
|
+
parsePgNestedArray: () => parsePgNestedArray,
|
|
19
|
+
pgEnum: () => pgEnum,
|
|
20
|
+
pgMaterializedView: () => pgMaterializedView,
|
|
21
|
+
pgPolicy: () => pgPolicy,
|
|
22
|
+
pgRole: () => pgRole,
|
|
23
|
+
pgSchema: () => pgSchema,
|
|
24
|
+
pgSequence: () => pgSequence,
|
|
25
|
+
pgTable: () => pgTable,
|
|
26
|
+
pgTableCreator: () => pgTableCreator,
|
|
27
|
+
pgView: () => pgView,
|
|
28
|
+
serial: () => serial,
|
|
29
|
+
smallint: () => smallint,
|
|
30
|
+
smallserial: () => smallserial,
|
|
31
|
+
sparsevec: () => sparsevec,
|
|
32
|
+
unique: () => unique,
|
|
33
|
+
uniqueIndex: () => uniqueIndex,
|
|
34
|
+
uniqueKeyName: () => uniqueKeyName,
|
|
35
|
+
withReplicas: () => withReplicas
|
|
36
|
+
});
|
|
37
|
+
import * as import_drizzle_orm from "drizzle-orm";
|
|
38
|
+
__reExport(orm_exports, import_drizzle_orm);
|
|
39
|
+
import * as import_drizzle_seed from "drizzle-seed";
|
|
40
|
+
__reExport(orm_exports, import_drizzle_seed);
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
export { index, isPgEnum, isPgMaterializedView, isPgSchema, isPgSequence, isPgView, numeric, orm_exports, parsePgArray, parsePgNestedArray, pgEnum, pgMaterializedView, pgPolicy, pgRole, pgSchema, pgSequence, pgTable, pgTableCreator, pgView, serial, smallint, smallserial, sparsevec, unique, uniqueIndex, uniqueKeyName, withReplicas };
|
|
@@ -1 +1,56 @@
|
|
|
1
|
-
import{instrumentation_exports
|
|
1
|
+
import { instrumentation_exports } from "./instrumentation.mjs";
|
|
2
|
+
import { getCallSites } from "node:util";
|
|
3
|
+
|
|
4
|
+
//#region src/api/otel.ts
|
|
5
|
+
/**
|
|
6
|
+
* Wraps an async function with an OpenTelemetry span for distributed tracing.
|
|
7
|
+
*
|
|
8
|
+
* Automatically handles span lifecycle (start/end), error recording, and context propagation.
|
|
9
|
+
* The span will appear as a child of the currently active span (if any).
|
|
10
|
+
*
|
|
11
|
+
* @param tracerName - Fully qualified module path (e.g., "api/routes/payouts", "packages/primitives/mailer")
|
|
12
|
+
* @param spanName - Operation name following {verb} {object} pattern (e.g., "fetch payouts", "send email")
|
|
13
|
+
* @param fn - The async function to execute within the span context. Receives span for custom attributes.
|
|
14
|
+
* @param attributes - Optional span attributes for high-cardinality data (IDs, counts, etc.).
|
|
15
|
+
* Use semantic conventions: https://opentelemetry.io/docs/specs/semconv/
|
|
16
|
+
* @returns Promise resolving to the function's return value.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Basic usage - simple business operation
|
|
20
|
+
* await withOtelSpan("api/routes/payouts", "fetch payouts", async () => {
|
|
21
|
+
* return await db.query.payouts.findMany();
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // With attributes and custom span data
|
|
26
|
+
* const payouts = await withOtelSpan(
|
|
27
|
+
* "api/routes/payouts",
|
|
28
|
+
* "fetch payouts",
|
|
29
|
+
* async (span) => {
|
|
30
|
+
* const results = await db.query.payouts.findMany();
|
|
31
|
+
* span.setAttribute("payouts.count", results.length);
|
|
32
|
+
* span.addEvent("cache.miss");
|
|
33
|
+
* return results;
|
|
34
|
+
* },
|
|
35
|
+
* { "payouts.limit": 100, "payouts.offset": 0 }
|
|
36
|
+
* );
|
|
37
|
+
*/
|
|
38
|
+
async function withOtelSpan(fn, attributes) {
|
|
39
|
+
const callSite = getCallSites()[1];
|
|
40
|
+
const tracerName = callSite.scriptName.replace(process.cwd(), "").replace("file:///", "");
|
|
41
|
+
const spanName = callSite.functionName;
|
|
42
|
+
const span = instrumentation_exports.trace.getTracer(tracerName.replace("build/", "")).startSpan(spanName, { attributes });
|
|
43
|
+
const ctx = instrumentation_exports.trace.setSpan(instrumentation_exports.context.active(), span);
|
|
44
|
+
try {
|
|
45
|
+
const result = await instrumentation_exports.context.with(ctx, () => fn(span));
|
|
46
|
+
span.end();
|
|
47
|
+
return result;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
span.recordException(error);
|
|
50
|
+
span.end();
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
56
|
+
export { withOtelSpan };
|
|
@@ -1 +1,41 @@
|
|
|
1
|
-
import{createClient
|
|
1
|
+
import { createClient, createCluster } from "redis";
|
|
2
|
+
|
|
3
|
+
//#region src/api/redis.ts
|
|
4
|
+
/**
|
|
5
|
+
* Defines Redis client based on URL format with lazy connection.
|
|
6
|
+
* Single URL → createClient(), comma-separated URLs → createCluster()
|
|
7
|
+
*
|
|
8
|
+
* Algorithm:
|
|
9
|
+
* 1. Split URL by comma to detect cluster vs single
|
|
10
|
+
* 2. Create appropriate client type
|
|
11
|
+
* 3. Attach error handler with label for debugging
|
|
12
|
+
* 4. Wrap in proxy that auto-connects on first use
|
|
13
|
+
*
|
|
14
|
+
* @param opts Options for defining the Redis client.
|
|
15
|
+
* @returns Redis client that auto-connects on first operation.
|
|
16
|
+
*/
|
|
17
|
+
function defineRedisClient(opts) {
|
|
18
|
+
const urls = opts.url.split(",").map((u) => u.trim()).filter(Boolean);
|
|
19
|
+
let client;
|
|
20
|
+
if (urls.length === 1) client = createClient({ url: urls[0] });
|
|
21
|
+
else client = createCluster({ rootNodes: urls.map((u) => ({ url: u })) });
|
|
22
|
+
client.on("error", (err) => opts.logger.error({ err }, "Redis error"));
|
|
23
|
+
let connectPromise = null;
|
|
24
|
+
const ensureConnected = async () => {
|
|
25
|
+
if (!client.isOpen) {
|
|
26
|
+
connectPromise ??= client.connect();
|
|
27
|
+
await connectPromise;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
return new Proxy(client, { get(target, prop, receiver) {
|
|
31
|
+
const value = Reflect.get(target, prop, receiver);
|
|
32
|
+
if (typeof value === "function" && prop !== "on" && prop !== "once") return async (...args) => {
|
|
33
|
+
await ensureConnected();
|
|
34
|
+
return value.apply(target, args);
|
|
35
|
+
};
|
|
36
|
+
return value;
|
|
37
|
+
} });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
export { defineRedisClient };
|