crudora 0.3.0 → 0.4.1
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 +57 -5
- package/dist/index.cjs +369 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +119 -1
- package/dist/index.d.ts +119 -1
- package/dist/index.js +375 -3
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -22,6 +22,10 @@ Automatic CRUD API generator for TypeScript with Drizzle ORM — build REST APIs
|
|
|
22
22
|
- **Lifecycle Hooks** — `beforeCreate`, `afterCreate`, `afterCreateMany`, `beforeUpdate`, `afterUpdate`, `beforeDelete`, `afterDelete`, `beforeFind`, `afterFind`
|
|
23
23
|
- **Structured Logging** — pluggable `CrudoraLogger` with correlation IDs per request; compatible with pino, winston
|
|
24
24
|
- **Field Security** — `hidden` fields stripped at query time via `getTableColumns()`
|
|
25
|
+
- **Request Timeout** — built-in socket-level timeout middleware; returns `503` when a handler exceeds the configured limit
|
|
26
|
+
- **Health Check** — built-in `GET /health` endpoint; configurable path or disable entirely
|
|
27
|
+
- **Graceful Shutdown** — `listen()` returns the underlying `http.Server` for clean SIGTERM handling
|
|
28
|
+
- **Built-in API Docs** — Scalar interactive UI at `/docs`; fully configurable (theme, layout, info); enable with `docs: true` after installing `@scalar/express-api-reference`
|
|
25
29
|
- **Standardized Responses** — all endpoints return `{ success, data, meta?, error? }` OpenAPI-style envelope
|
|
26
30
|
- **Schema Generator** — auto-generate Drizzle TypeScript schema files from models
|
|
27
31
|
- **TypeScript First** — full type safety, ESM and CJS dual build
|
|
@@ -92,12 +96,22 @@ class User extends Model {
|
|
|
92
96
|
}
|
|
93
97
|
}
|
|
94
98
|
|
|
95
|
-
const server = new CrudoraServer({
|
|
99
|
+
const server = new CrudoraServer({
|
|
100
|
+
db,
|
|
101
|
+
dialect: 'postgresql',
|
|
102
|
+
port: 3000,
|
|
103
|
+
timeout: 30_000, // 503 after 30 s with no response
|
|
104
|
+
healthCheck: true, // GET /health → { status: 'ok' }
|
|
105
|
+
// docs: true, // GET /docs → Scalar UI (requires @scalar/express-api-reference)
|
|
106
|
+
});
|
|
96
107
|
|
|
97
|
-
server
|
|
108
|
+
const httpServer = server
|
|
98
109
|
.registerModel(User)
|
|
99
110
|
.generateRoutes()
|
|
100
111
|
.listen();
|
|
112
|
+
|
|
113
|
+
// Graceful shutdown
|
|
114
|
+
process.on('SIGTERM', () => httpServer.close(() => process.exit(0)));
|
|
101
115
|
```
|
|
102
116
|
|
|
103
117
|
## Generated API Endpoints
|
|
@@ -473,9 +487,6 @@ npx crudora generate-schema --entry src/server.ts --output src/db/schema.ts
|
|
|
473
487
|
|
|
474
488
|
```typescript
|
|
475
489
|
server
|
|
476
|
-
.get('/health', (_req, res) => {
|
|
477
|
-
res.json({ success: true, data: { status: 'ok', timestamp: new Date() } });
|
|
478
|
-
})
|
|
479
490
|
.post('/auth/login', async (req, res) => {
|
|
480
491
|
const { email, password } = req.body;
|
|
481
492
|
const userRepo = server.getCrudora().getRepository(User);
|
|
@@ -521,6 +532,46 @@ server.post('/auth/login', async (req, res) => {
|
|
|
521
532
|
|
|
522
533
|
See the [Authentication Guide](./docs/authentication.md) for register, per-route middleware, role guards, and a full JWT example.
|
|
523
534
|
|
|
535
|
+
## API Documentation (Scalar)
|
|
536
|
+
|
|
537
|
+
Crudora can serve an interactive API reference powered by [Scalar](https://scalar.com) — auto-generated from your registered models.
|
|
538
|
+
|
|
539
|
+
**1. Install the peer dependency:**
|
|
540
|
+
|
|
541
|
+
```bash
|
|
542
|
+
npm install @scalar/express-api-reference
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
**2. Enable `docs` in your server config:**
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
const server = new CrudoraServer({
|
|
549
|
+
db,
|
|
550
|
+
dialect: 'postgresql',
|
|
551
|
+
docs: true, // → GET /docs (UI) + GET /docs/openapi.json (spec)
|
|
552
|
+
});
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
**3. Customize as needed:**
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
docs: {
|
|
559
|
+
path: '/docs', // custom mount path, e.g. '/api-docs'
|
|
560
|
+
title: 'My API', // shown in Scalar UI header
|
|
561
|
+
version: '1.0.0',
|
|
562
|
+
description: 'Full description of what this API does.',
|
|
563
|
+
scalar: { // any @scalar/express-api-reference option
|
|
564
|
+
theme: 'purple', // 'default' | 'alternate' | 'moon' | 'purple' | ...
|
|
565
|
+
darkMode: true,
|
|
566
|
+
layout: 'classic', // 'modern' (default) | 'classic'
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
The raw OpenAPI 3.0 spec is always available at `{path}/openapi.json` — useful for importing into Postman, Insomnia, or other tooling even without the UI package installed.
|
|
572
|
+
|
|
573
|
+
> If `@scalar/express-api-reference` is not installed and `docs` is enabled, Crudora logs a warning and serves a plain install-prompt page. The spec endpoint is unaffected.
|
|
574
|
+
|
|
524
575
|
## Project Setup
|
|
525
576
|
|
|
526
577
|
```bash
|
|
@@ -544,6 +595,7 @@ npx ts-node src/server.ts
|
|
|
544
595
|
- [Custom Routes](./docs/custom-routes.md)
|
|
545
596
|
- [Authentication Guide](./docs/authentication.md)
|
|
546
597
|
- [Deployment Guide](./docs/deployment.md)
|
|
598
|
+
- [API Documentation (Scalar)](#api-documentation-scalar)
|
|
547
599
|
|
|
548
600
|
## Contributing
|
|
549
601
|
|
package/dist/index.cjs
CHANGED
|
@@ -40,6 +40,7 @@ __export(src_exports, {
|
|
|
40
40
|
HasOne: () => HasOne,
|
|
41
41
|
Model: () => Model,
|
|
42
42
|
NotFoundError: () => NotFoundError,
|
|
43
|
+
OpenApiGenerator: () => OpenApiGenerator,
|
|
43
44
|
Repository: () => Repository,
|
|
44
45
|
SchemaGenerator: () => SchemaGenerator,
|
|
45
46
|
ValidationGenerator: () => ValidationGenerator,
|
|
@@ -864,6 +865,299 @@ function getColumnImport(type, dialect) {
|
|
|
864
865
|
}
|
|
865
866
|
}
|
|
866
867
|
|
|
868
|
+
// src/core/openApiGenerator.ts
|
|
869
|
+
var SYSTEM_FIELDS2 = /* @__PURE__ */ new Set(["createdAt", "updatedAt", "deletedAt"]);
|
|
870
|
+
function fieldToSchema(opts) {
|
|
871
|
+
switch (opts.type) {
|
|
872
|
+
case "uuid":
|
|
873
|
+
return { type: "string", format: "uuid" };
|
|
874
|
+
case "text":
|
|
875
|
+
return { type: "string" };
|
|
876
|
+
case "integer":
|
|
877
|
+
return { type: "integer" };
|
|
878
|
+
case "number":
|
|
879
|
+
return { type: "number", format: "double" };
|
|
880
|
+
case "boolean":
|
|
881
|
+
return { type: "boolean" };
|
|
882
|
+
case "date":
|
|
883
|
+
return { type: "string", format: "date-time" };
|
|
884
|
+
case "decimal":
|
|
885
|
+
return { type: "number", format: "decimal" };
|
|
886
|
+
case "json":
|
|
887
|
+
return { type: "object" };
|
|
888
|
+
case "enum":
|
|
889
|
+
return { type: "string", enum: opts.enumValues ?? [] };
|
|
890
|
+
case "bigint":
|
|
891
|
+
return { type: "integer", format: "int64" };
|
|
892
|
+
case "serial":
|
|
893
|
+
return { type: "integer" };
|
|
894
|
+
case "array":
|
|
895
|
+
return { type: "array", items: { type: "string" } };
|
|
896
|
+
default: {
|
|
897
|
+
const s = { type: "string" };
|
|
898
|
+
if (opts.length) s.maxLength = opts.length;
|
|
899
|
+
return s;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
function buildOutputSchema(modelClass) {
|
|
904
|
+
const fields = getFieldMetadata(modelClass);
|
|
905
|
+
const hidden = new Set(modelClass.hidden ?? []);
|
|
906
|
+
const properties = {};
|
|
907
|
+
for (const [name, opts] of Object.entries(fields)) {
|
|
908
|
+
if (hidden.has(name)) continue;
|
|
909
|
+
const schema = fieldToSchema(opts);
|
|
910
|
+
if (opts.nullable) schema.nullable = true;
|
|
911
|
+
properties[name] = schema;
|
|
912
|
+
}
|
|
913
|
+
if (modelClass.timestamps !== false) {
|
|
914
|
+
properties.createdAt = { type: "string", format: "date-time" };
|
|
915
|
+
properties.updatedAt = { type: "string", format: "date-time" };
|
|
916
|
+
}
|
|
917
|
+
if (modelClass.softDelete) {
|
|
918
|
+
properties.deletedAt = { type: "string", format: "date-time", nullable: true };
|
|
919
|
+
}
|
|
920
|
+
return { type: "object", properties };
|
|
921
|
+
}
|
|
922
|
+
function buildInputSchema(modelClass) {
|
|
923
|
+
const fields = getFieldMetadata(modelClass);
|
|
924
|
+
const fillable = modelClass.fillable;
|
|
925
|
+
const properties = {};
|
|
926
|
+
const required = [];
|
|
927
|
+
let entries = Object.entries(fields).filter(
|
|
928
|
+
([name, opts]) => !opts.primary && !SYSTEM_FIELDS2.has(name)
|
|
929
|
+
);
|
|
930
|
+
if (fillable?.length) {
|
|
931
|
+
entries = entries.filter(([name]) => fillable.includes(name));
|
|
932
|
+
}
|
|
933
|
+
for (const [name, opts] of entries) {
|
|
934
|
+
const schema2 = fieldToSchema(opts);
|
|
935
|
+
if (opts.nullable) schema2.nullable = true;
|
|
936
|
+
properties[name] = schema2;
|
|
937
|
+
if (opts.required && !opts.nullable) required.push(name);
|
|
938
|
+
}
|
|
939
|
+
const schema = { type: "object", properties };
|
|
940
|
+
if (required.length > 0) schema.required = required;
|
|
941
|
+
return schema;
|
|
942
|
+
}
|
|
943
|
+
function capital(s) {
|
|
944
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
945
|
+
}
|
|
946
|
+
function itemLabel(tableName) {
|
|
947
|
+
return tableName.endsWith("s") ? tableName.slice(0, -1) : tableName;
|
|
948
|
+
}
|
|
949
|
+
var listResponse = (ref) => ({
|
|
950
|
+
description: "OK",
|
|
951
|
+
content: {
|
|
952
|
+
"application/json": {
|
|
953
|
+
schema: {
|
|
954
|
+
type: "object",
|
|
955
|
+
properties: {
|
|
956
|
+
success: { type: "boolean", example: true },
|
|
957
|
+
data: { type: "array", items: { $ref: ref } },
|
|
958
|
+
meta: {
|
|
959
|
+
type: "object",
|
|
960
|
+
properties: {
|
|
961
|
+
pagination: {
|
|
962
|
+
type: "object",
|
|
963
|
+
properties: {
|
|
964
|
+
page: { type: "integer" },
|
|
965
|
+
limit: { type: "integer" },
|
|
966
|
+
total: { type: "integer" },
|
|
967
|
+
pages: { type: "integer" }
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
var itemResponse = (ref, status = "OK") => ({
|
|
978
|
+
description: status,
|
|
979
|
+
content: {
|
|
980
|
+
"application/json": {
|
|
981
|
+
schema: {
|
|
982
|
+
type: "object",
|
|
983
|
+
properties: {
|
|
984
|
+
success: { type: "boolean", example: true },
|
|
985
|
+
data: { $ref: ref }
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
var errorResponse = (description, code) => ({
|
|
992
|
+
description,
|
|
993
|
+
content: {
|
|
994
|
+
"application/json": {
|
|
995
|
+
schema: {
|
|
996
|
+
type: "object",
|
|
997
|
+
properties: {
|
|
998
|
+
success: { type: "boolean", example: false },
|
|
999
|
+
error: {
|
|
1000
|
+
type: "object",
|
|
1001
|
+
properties: {
|
|
1002
|
+
code: { type: "string", example: code },
|
|
1003
|
+
message: { type: "string" }
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
var validationError = {
|
|
1012
|
+
description: "Validation Error",
|
|
1013
|
+
content: {
|
|
1014
|
+
"application/json": {
|
|
1015
|
+
schema: {
|
|
1016
|
+
type: "object",
|
|
1017
|
+
properties: {
|
|
1018
|
+
success: { type: "boolean", example: false },
|
|
1019
|
+
error: {
|
|
1020
|
+
type: "object",
|
|
1021
|
+
properties: {
|
|
1022
|
+
code: { type: "string", example: "VALIDATION_ERROR" },
|
|
1023
|
+
message: { type: "string" },
|
|
1024
|
+
details: {
|
|
1025
|
+
type: "array",
|
|
1026
|
+
items: {
|
|
1027
|
+
type: "object",
|
|
1028
|
+
properties: {
|
|
1029
|
+
field: { type: "string" },
|
|
1030
|
+
message: { type: "string" }
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
var idParam = { in: "path", name: "id", required: true, schema: { type: "string" } };
|
|
1042
|
+
var OpenApiGenerator = class {
|
|
1043
|
+
static generate(models, customRoutes, basePath, info) {
|
|
1044
|
+
const schemas = {};
|
|
1045
|
+
const paths = {};
|
|
1046
|
+
for (const [, modelClass] of models) {
|
|
1047
|
+
const tableName = modelClass.getTableName();
|
|
1048
|
+
const tag = capital(tableName);
|
|
1049
|
+
const item = itemLabel(tableName);
|
|
1050
|
+
const ref = `#/components/schemas/${tag}`;
|
|
1051
|
+
const inputRef = `#/components/schemas/${tag}Input`;
|
|
1052
|
+
const col = `${basePath}/${tableName}`;
|
|
1053
|
+
const byId = `${basePath}/${tableName}/{id}`;
|
|
1054
|
+
schemas[tag] = buildOutputSchema(modelClass);
|
|
1055
|
+
schemas[`${tag}Input`] = buildInputSchema(modelClass);
|
|
1056
|
+
paths[col] = {
|
|
1057
|
+
get: {
|
|
1058
|
+
summary: `List ${tableName}`,
|
|
1059
|
+
operationId: `list${tag}`,
|
|
1060
|
+
tags: [tag],
|
|
1061
|
+
parameters: [
|
|
1062
|
+
{ in: "query", name: "page", schema: { type: "integer", default: 1 } },
|
|
1063
|
+
{ in: "query", name: "limit", schema: { type: "integer", default: 10 } },
|
|
1064
|
+
{ in: "query", name: "cursor", schema: { type: "string" } },
|
|
1065
|
+
{ in: "query", name: "sortBy", schema: { type: "string" } },
|
|
1066
|
+
{ in: "query", name: "sortOrder", schema: { type: "string", enum: ["asc", "desc"] } },
|
|
1067
|
+
{ in: "query", name: "withDeleted", schema: { type: "boolean" } }
|
|
1068
|
+
],
|
|
1069
|
+
responses: { "200": listResponse(ref) }
|
|
1070
|
+
},
|
|
1071
|
+
post: {
|
|
1072
|
+
summary: `Create ${item}`,
|
|
1073
|
+
operationId: `create${tag}`,
|
|
1074
|
+
tags: [tag],
|
|
1075
|
+
requestBody: {
|
|
1076
|
+
required: true,
|
|
1077
|
+
content: { "application/json": { schema: { $ref: inputRef } } }
|
|
1078
|
+
},
|
|
1079
|
+
responses: {
|
|
1080
|
+
"201": itemResponse(ref, "Created"),
|
|
1081
|
+
"422": validationError
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
paths[byId] = {
|
|
1086
|
+
get: {
|
|
1087
|
+
summary: `Get ${item} by ID`,
|
|
1088
|
+
operationId: `get${tag}ById`,
|
|
1089
|
+
tags: [tag],
|
|
1090
|
+
parameters: [idParam],
|
|
1091
|
+
responses: {
|
|
1092
|
+
"200": itemResponse(ref),
|
|
1093
|
+
"404": errorResponse("Not Found", "NOT_FOUND")
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
put: {
|
|
1097
|
+
summary: `Replace ${item}`,
|
|
1098
|
+
operationId: `replace${tag}`,
|
|
1099
|
+
tags: [tag],
|
|
1100
|
+
parameters: [idParam],
|
|
1101
|
+
requestBody: {
|
|
1102
|
+
required: true,
|
|
1103
|
+
content: { "application/json": { schema: { $ref: inputRef } } }
|
|
1104
|
+
},
|
|
1105
|
+
responses: {
|
|
1106
|
+
"200": itemResponse(ref),
|
|
1107
|
+
"404": errorResponse("Not Found", "NOT_FOUND"),
|
|
1108
|
+
"422": validationError
|
|
1109
|
+
}
|
|
1110
|
+
},
|
|
1111
|
+
patch: {
|
|
1112
|
+
summary: `Update ${item}`,
|
|
1113
|
+
operationId: `update${tag}`,
|
|
1114
|
+
tags: [tag],
|
|
1115
|
+
parameters: [idParam],
|
|
1116
|
+
requestBody: {
|
|
1117
|
+
required: true,
|
|
1118
|
+
content: { "application/json": { schema: { $ref: inputRef } } }
|
|
1119
|
+
},
|
|
1120
|
+
responses: {
|
|
1121
|
+
"200": itemResponse(ref),
|
|
1122
|
+
"404": errorResponse("Not Found", "NOT_FOUND"),
|
|
1123
|
+
"422": validationError
|
|
1124
|
+
}
|
|
1125
|
+
},
|
|
1126
|
+
delete: {
|
|
1127
|
+
summary: `Delete ${item}`,
|
|
1128
|
+
operationId: `delete${tag}`,
|
|
1129
|
+
tags: [tag],
|
|
1130
|
+
parameters: [idParam],
|
|
1131
|
+
responses: {
|
|
1132
|
+
"200": itemResponse(ref),
|
|
1133
|
+
"404": errorResponse("Not Found", "NOT_FOUND")
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
for (const route of customRoutes) {
|
|
1139
|
+
const fullPath = `${basePath}${route.path}`.replace(/:([a-zA-Z_]+)/g, "{$1}");
|
|
1140
|
+
if (!paths[fullPath]) paths[fullPath] = {};
|
|
1141
|
+
paths[fullPath][route.method.toLowerCase()] = {
|
|
1142
|
+
summary: `${route.method} ${route.path}`,
|
|
1143
|
+
tags: ["Custom"],
|
|
1144
|
+
responses: { "200": { description: "OK" } }
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
const spec = {
|
|
1148
|
+
openapi: "3.0.0",
|
|
1149
|
+
info: {
|
|
1150
|
+
title: info?.title ?? "Crudora API",
|
|
1151
|
+
version: info?.version ?? "1.0.0"
|
|
1152
|
+
},
|
|
1153
|
+
paths,
|
|
1154
|
+
components: { schemas }
|
|
1155
|
+
};
|
|
1156
|
+
if (info?.description) spec.info.description = info.description;
|
|
1157
|
+
return spec;
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
|
|
867
1161
|
// src/utils/validation.ts
|
|
868
1162
|
var import_zod = require("zod");
|
|
869
1163
|
function zodTypeFor(opts, forStrict) {
|
|
@@ -916,14 +1210,14 @@ function zodTypeFor(opts, forStrict) {
|
|
|
916
1210
|
}
|
|
917
1211
|
return base.optional();
|
|
918
1212
|
}
|
|
919
|
-
var
|
|
1213
|
+
var SYSTEM_FIELDS3 = /* @__PURE__ */ new Set(["createdAt", "updatedAt", "deletedAt"]);
|
|
920
1214
|
function resolveFields(modelClass) {
|
|
921
1215
|
const fieldMeta = getFieldMetadata(modelClass);
|
|
922
1216
|
const hasMeta = Object.keys(fieldMeta).length > 0;
|
|
923
1217
|
const fillable = modelClass.fillable;
|
|
924
1218
|
if (hasMeta) {
|
|
925
1219
|
let entries = Object.entries(fieldMeta).filter(
|
|
926
|
-
([name, opts]) => !opts.primary && !
|
|
1220
|
+
([name, opts]) => !opts.primary && !SYSTEM_FIELDS3.has(name)
|
|
927
1221
|
);
|
|
928
1222
|
if (fillable?.length) {
|
|
929
1223
|
entries = entries.filter(([name]) => fillable.includes(name));
|
|
@@ -1202,6 +1496,14 @@ var Crudora = class {
|
|
|
1202
1496
|
const modelClasses = Array.from(this.models.values());
|
|
1203
1497
|
return SchemaGenerator.generateDrizzleSchema(modelClasses, this.dialect);
|
|
1204
1498
|
}
|
|
1499
|
+
generateOpenApiSpec(basePath = "/api", info) {
|
|
1500
|
+
return OpenApiGenerator.generate(
|
|
1501
|
+
this.models,
|
|
1502
|
+
this.customRoutes.map(({ method, path }) => ({ method, path })),
|
|
1503
|
+
basePath,
|
|
1504
|
+
info
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1205
1507
|
getValidationSchema(modelClass) {
|
|
1206
1508
|
return ValidationGenerator.generateZodSchema(modelClass);
|
|
1207
1509
|
}
|
|
@@ -1496,6 +1798,40 @@ function createDefaultLogger() {
|
|
|
1496
1798
|
debug: (msg, ctx) => console.debug(fmt("debug", msg, ctx))
|
|
1497
1799
|
};
|
|
1498
1800
|
}
|
|
1801
|
+
function resolveDocs(docs) {
|
|
1802
|
+
if (!docs) return false;
|
|
1803
|
+
if (docs === true) return { path: "/docs" };
|
|
1804
|
+
if (typeof docs === "string") return { path: docs };
|
|
1805
|
+
return { path: docs.path ?? "/docs", ...docs };
|
|
1806
|
+
}
|
|
1807
|
+
function tryRequireScalar() {
|
|
1808
|
+
try {
|
|
1809
|
+
return require("@scalar/express-api-reference");
|
|
1810
|
+
} catch {
|
|
1811
|
+
return null;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
function buildInstallPromptHtml(specUrl) {
|
|
1815
|
+
return `<!DOCTYPE html>
|
|
1816
|
+
<html>
|
|
1817
|
+
<head>
|
|
1818
|
+
<title>API Docs</title>
|
|
1819
|
+
<meta charset="utf-8" />
|
|
1820
|
+
<style>
|
|
1821
|
+
body { font-family: system-ui, sans-serif; max-width: 600px; margin: 80px auto; padding: 0 24px; color: #333; }
|
|
1822
|
+
code { background: #f4f4f4; padding: 2px 6px; border-radius: 4px; }
|
|
1823
|
+
pre { background: #f4f4f4; padding: 16px; border-radius: 8px; overflow: auto; }
|
|
1824
|
+
a { color: #0070f3; }
|
|
1825
|
+
</style>
|
|
1826
|
+
</head>
|
|
1827
|
+
<body>
|
|
1828
|
+
<h2>API Docs</h2>
|
|
1829
|
+
<p>Install <code>@scalar/express-api-reference</code> to enable the interactive UI:</p>
|
|
1830
|
+
<pre>npm install @scalar/express-api-reference</pre>
|
|
1831
|
+
<p>OpenAPI spec: <a href="${specUrl}">${specUrl}</a></p>
|
|
1832
|
+
</body>
|
|
1833
|
+
</html>`;
|
|
1834
|
+
}
|
|
1499
1835
|
var CrudoraServer = class {
|
|
1500
1836
|
constructor(config) {
|
|
1501
1837
|
this.httpServer = null;
|
|
@@ -1516,7 +1852,8 @@ var CrudoraServer = class {
|
|
|
1516
1852
|
healthCheck: true,
|
|
1517
1853
|
...config,
|
|
1518
1854
|
logger: resolvedLogger,
|
|
1519
|
-
rateLimit: resolvedRateLimit
|
|
1855
|
+
rateLimit: resolvedRateLimit,
|
|
1856
|
+
docs: resolveDocs(config.docs)
|
|
1520
1857
|
};
|
|
1521
1858
|
this.app = (0, import_express.default)();
|
|
1522
1859
|
this.crudora = new Crudora(
|
|
@@ -1597,6 +1934,31 @@ var CrudoraServer = class {
|
|
|
1597
1934
|
res.json({ success: true, data: { status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() } });
|
|
1598
1935
|
});
|
|
1599
1936
|
}
|
|
1937
|
+
if (this.config.docs !== false) {
|
|
1938
|
+
const { path: docsPath, title, version, description, scalar: scalarOpts } = this.config.docs;
|
|
1939
|
+
const specPath = `${docsPath}/openapi.json`;
|
|
1940
|
+
const spec = this.crudora.generateOpenApiSpec(this.config.basePath, { title, version, description });
|
|
1941
|
+
this.app.get(specPath, (_req, res) => {
|
|
1942
|
+
res.json(spec);
|
|
1943
|
+
});
|
|
1944
|
+
const scalarPkg = tryRequireScalar();
|
|
1945
|
+
if (scalarPkg) {
|
|
1946
|
+
this.app.use(docsPath, scalarPkg.apiReference({
|
|
1947
|
+
spec: { url: specPath },
|
|
1948
|
+
...scalarOpts
|
|
1949
|
+
}));
|
|
1950
|
+
} else {
|
|
1951
|
+
if (this.config.logger !== false) {
|
|
1952
|
+
this.config.logger.warn(
|
|
1953
|
+
'docs is enabled but @scalar/express-api-reference is not installed. Run "npm install @scalar/express-api-reference" to enable the interactive UI.'
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
this.app.get(docsPath, (_req, res) => {
|
|
1957
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
1958
|
+
res.send(buildInstallPromptHtml(specPath));
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1600
1962
|
this.crudora.generateRoutes(this.app, this.config.basePath);
|
|
1601
1963
|
return this;
|
|
1602
1964
|
}
|
|
@@ -1617,6 +1979,9 @@ var CrudoraServer = class {
|
|
|
1617
1979
|
this.httpServer.listen(this.config.port, () => {
|
|
1618
1980
|
console.log(`\u{1F680} Crudora server running on port ${this.config.port}`);
|
|
1619
1981
|
console.log(`\u{1F4DA} API available at http://localhost:${this.config.port}${this.config.basePath}`);
|
|
1982
|
+
if (this.config.docs !== false) {
|
|
1983
|
+
console.log(`\u{1F4D6} API docs at http://localhost:${this.config.port}${this.config.docs.path}`);
|
|
1984
|
+
}
|
|
1620
1985
|
if (callback) callback();
|
|
1621
1986
|
});
|
|
1622
1987
|
return this.httpServer;
|
|
@@ -1723,6 +2088,7 @@ Model.softDelete = false;
|
|
|
1723
2088
|
HasOne,
|
|
1724
2089
|
Model,
|
|
1725
2090
|
NotFoundError,
|
|
2091
|
+
OpenApiGenerator,
|
|
1726
2092
|
Repository,
|
|
1727
2093
|
SchemaGenerator,
|
|
1728
2094
|
ValidationGenerator,
|