crudora 0.2.1 → 0.4.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/README.md +57 -5
- package/dist/index.cjs +410 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +154 -2
- package/dist/index.d.ts +154 -2
- package/dist/index.js +416 -4
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
package/dist/index.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
1
8
|
// src/index.ts
|
|
2
9
|
import "reflect-metadata";
|
|
3
10
|
|
|
@@ -832,6 +839,299 @@ function getColumnImport(type, dialect) {
|
|
|
832
839
|
}
|
|
833
840
|
}
|
|
834
841
|
|
|
842
|
+
// src/core/openApiGenerator.ts
|
|
843
|
+
var SYSTEM_FIELDS2 = /* @__PURE__ */ new Set(["createdAt", "updatedAt", "deletedAt"]);
|
|
844
|
+
function fieldToSchema(opts) {
|
|
845
|
+
switch (opts.type) {
|
|
846
|
+
case "uuid":
|
|
847
|
+
return { type: "string", format: "uuid" };
|
|
848
|
+
case "text":
|
|
849
|
+
return { type: "string" };
|
|
850
|
+
case "integer":
|
|
851
|
+
return { type: "integer" };
|
|
852
|
+
case "number":
|
|
853
|
+
return { type: "number", format: "double" };
|
|
854
|
+
case "boolean":
|
|
855
|
+
return { type: "boolean" };
|
|
856
|
+
case "date":
|
|
857
|
+
return { type: "string", format: "date-time" };
|
|
858
|
+
case "decimal":
|
|
859
|
+
return { type: "number", format: "decimal" };
|
|
860
|
+
case "json":
|
|
861
|
+
return { type: "object" };
|
|
862
|
+
case "enum":
|
|
863
|
+
return { type: "string", enum: opts.enumValues ?? [] };
|
|
864
|
+
case "bigint":
|
|
865
|
+
return { type: "integer", format: "int64" };
|
|
866
|
+
case "serial":
|
|
867
|
+
return { type: "integer" };
|
|
868
|
+
case "array":
|
|
869
|
+
return { type: "array", items: { type: "string" } };
|
|
870
|
+
default: {
|
|
871
|
+
const s = { type: "string" };
|
|
872
|
+
if (opts.length) s.maxLength = opts.length;
|
|
873
|
+
return s;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
function buildOutputSchema(modelClass) {
|
|
878
|
+
const fields = getFieldMetadata(modelClass);
|
|
879
|
+
const hidden = new Set(modelClass.hidden ?? []);
|
|
880
|
+
const properties = {};
|
|
881
|
+
for (const [name, opts] of Object.entries(fields)) {
|
|
882
|
+
if (hidden.has(name)) continue;
|
|
883
|
+
const schema = fieldToSchema(opts);
|
|
884
|
+
if (opts.nullable) schema.nullable = true;
|
|
885
|
+
properties[name] = schema;
|
|
886
|
+
}
|
|
887
|
+
if (modelClass.timestamps !== false) {
|
|
888
|
+
properties.createdAt = { type: "string", format: "date-time" };
|
|
889
|
+
properties.updatedAt = { type: "string", format: "date-time" };
|
|
890
|
+
}
|
|
891
|
+
if (modelClass.softDelete) {
|
|
892
|
+
properties.deletedAt = { type: "string", format: "date-time", nullable: true };
|
|
893
|
+
}
|
|
894
|
+
return { type: "object", properties };
|
|
895
|
+
}
|
|
896
|
+
function buildInputSchema(modelClass) {
|
|
897
|
+
const fields = getFieldMetadata(modelClass);
|
|
898
|
+
const fillable = modelClass.fillable;
|
|
899
|
+
const properties = {};
|
|
900
|
+
const required = [];
|
|
901
|
+
let entries = Object.entries(fields).filter(
|
|
902
|
+
([name, opts]) => !opts.primary && !SYSTEM_FIELDS2.has(name)
|
|
903
|
+
);
|
|
904
|
+
if (fillable?.length) {
|
|
905
|
+
entries = entries.filter(([name]) => fillable.includes(name));
|
|
906
|
+
}
|
|
907
|
+
for (const [name, opts] of entries) {
|
|
908
|
+
const schema2 = fieldToSchema(opts);
|
|
909
|
+
if (opts.nullable) schema2.nullable = true;
|
|
910
|
+
properties[name] = schema2;
|
|
911
|
+
if (opts.required && !opts.nullable) required.push(name);
|
|
912
|
+
}
|
|
913
|
+
const schema = { type: "object", properties };
|
|
914
|
+
if (required.length > 0) schema.required = required;
|
|
915
|
+
return schema;
|
|
916
|
+
}
|
|
917
|
+
function capital(s) {
|
|
918
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
919
|
+
}
|
|
920
|
+
function itemLabel(tableName) {
|
|
921
|
+
return tableName.endsWith("s") ? tableName.slice(0, -1) : tableName;
|
|
922
|
+
}
|
|
923
|
+
var listResponse = (ref) => ({
|
|
924
|
+
description: "OK",
|
|
925
|
+
content: {
|
|
926
|
+
"application/json": {
|
|
927
|
+
schema: {
|
|
928
|
+
type: "object",
|
|
929
|
+
properties: {
|
|
930
|
+
success: { type: "boolean", example: true },
|
|
931
|
+
data: { type: "array", items: { $ref: ref } },
|
|
932
|
+
meta: {
|
|
933
|
+
type: "object",
|
|
934
|
+
properties: {
|
|
935
|
+
pagination: {
|
|
936
|
+
type: "object",
|
|
937
|
+
properties: {
|
|
938
|
+
page: { type: "integer" },
|
|
939
|
+
limit: { type: "integer" },
|
|
940
|
+
total: { type: "integer" },
|
|
941
|
+
pages: { type: "integer" }
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
var itemResponse = (ref, status = "OK") => ({
|
|
952
|
+
description: status,
|
|
953
|
+
content: {
|
|
954
|
+
"application/json": {
|
|
955
|
+
schema: {
|
|
956
|
+
type: "object",
|
|
957
|
+
properties: {
|
|
958
|
+
success: { type: "boolean", example: true },
|
|
959
|
+
data: { $ref: ref }
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
var errorResponse = (description, code) => ({
|
|
966
|
+
description,
|
|
967
|
+
content: {
|
|
968
|
+
"application/json": {
|
|
969
|
+
schema: {
|
|
970
|
+
type: "object",
|
|
971
|
+
properties: {
|
|
972
|
+
success: { type: "boolean", example: false },
|
|
973
|
+
error: {
|
|
974
|
+
type: "object",
|
|
975
|
+
properties: {
|
|
976
|
+
code: { type: "string", example: code },
|
|
977
|
+
message: { type: "string" }
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
var validationError = {
|
|
986
|
+
description: "Validation Error",
|
|
987
|
+
content: {
|
|
988
|
+
"application/json": {
|
|
989
|
+
schema: {
|
|
990
|
+
type: "object",
|
|
991
|
+
properties: {
|
|
992
|
+
success: { type: "boolean", example: false },
|
|
993
|
+
error: {
|
|
994
|
+
type: "object",
|
|
995
|
+
properties: {
|
|
996
|
+
code: { type: "string", example: "VALIDATION_ERROR" },
|
|
997
|
+
message: { type: "string" },
|
|
998
|
+
details: {
|
|
999
|
+
type: "array",
|
|
1000
|
+
items: {
|
|
1001
|
+
type: "object",
|
|
1002
|
+
properties: {
|
|
1003
|
+
field: { type: "string" },
|
|
1004
|
+
message: { type: "string" }
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
var idParam = { in: "path", name: "id", required: true, schema: { type: "string" } };
|
|
1016
|
+
var OpenApiGenerator = class {
|
|
1017
|
+
static generate(models, customRoutes, basePath, info) {
|
|
1018
|
+
const schemas = {};
|
|
1019
|
+
const paths = {};
|
|
1020
|
+
for (const [, modelClass] of models) {
|
|
1021
|
+
const tableName = modelClass.getTableName();
|
|
1022
|
+
const tag = capital(tableName);
|
|
1023
|
+
const item = itemLabel(tableName);
|
|
1024
|
+
const ref = `#/components/schemas/${tag}`;
|
|
1025
|
+
const inputRef = `#/components/schemas/${tag}Input`;
|
|
1026
|
+
const col = `${basePath}/${tableName}`;
|
|
1027
|
+
const byId = `${basePath}/${tableName}/{id}`;
|
|
1028
|
+
schemas[tag] = buildOutputSchema(modelClass);
|
|
1029
|
+
schemas[`${tag}Input`] = buildInputSchema(modelClass);
|
|
1030
|
+
paths[col] = {
|
|
1031
|
+
get: {
|
|
1032
|
+
summary: `List ${tableName}`,
|
|
1033
|
+
operationId: `list${tag}`,
|
|
1034
|
+
tags: [tag],
|
|
1035
|
+
parameters: [
|
|
1036
|
+
{ in: "query", name: "page", schema: { type: "integer", default: 1 } },
|
|
1037
|
+
{ in: "query", name: "limit", schema: { type: "integer", default: 10 } },
|
|
1038
|
+
{ in: "query", name: "cursor", schema: { type: "string" } },
|
|
1039
|
+
{ in: "query", name: "sortBy", schema: { type: "string" } },
|
|
1040
|
+
{ in: "query", name: "sortOrder", schema: { type: "string", enum: ["asc", "desc"] } },
|
|
1041
|
+
{ in: "query", name: "withDeleted", schema: { type: "boolean" } }
|
|
1042
|
+
],
|
|
1043
|
+
responses: { "200": listResponse(ref) }
|
|
1044
|
+
},
|
|
1045
|
+
post: {
|
|
1046
|
+
summary: `Create ${item}`,
|
|
1047
|
+
operationId: `create${tag}`,
|
|
1048
|
+
tags: [tag],
|
|
1049
|
+
requestBody: {
|
|
1050
|
+
required: true,
|
|
1051
|
+
content: { "application/json": { schema: { $ref: inputRef } } }
|
|
1052
|
+
},
|
|
1053
|
+
responses: {
|
|
1054
|
+
"201": itemResponse(ref, "Created"),
|
|
1055
|
+
"422": validationError
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
paths[byId] = {
|
|
1060
|
+
get: {
|
|
1061
|
+
summary: `Get ${item} by ID`,
|
|
1062
|
+
operationId: `get${tag}ById`,
|
|
1063
|
+
tags: [tag],
|
|
1064
|
+
parameters: [idParam],
|
|
1065
|
+
responses: {
|
|
1066
|
+
"200": itemResponse(ref),
|
|
1067
|
+
"404": errorResponse("Not Found", "NOT_FOUND")
|
|
1068
|
+
}
|
|
1069
|
+
},
|
|
1070
|
+
put: {
|
|
1071
|
+
summary: `Replace ${item}`,
|
|
1072
|
+
operationId: `replace${tag}`,
|
|
1073
|
+
tags: [tag],
|
|
1074
|
+
parameters: [idParam],
|
|
1075
|
+
requestBody: {
|
|
1076
|
+
required: true,
|
|
1077
|
+
content: { "application/json": { schema: { $ref: inputRef } } }
|
|
1078
|
+
},
|
|
1079
|
+
responses: {
|
|
1080
|
+
"200": itemResponse(ref),
|
|
1081
|
+
"404": errorResponse("Not Found", "NOT_FOUND"),
|
|
1082
|
+
"422": validationError
|
|
1083
|
+
}
|
|
1084
|
+
},
|
|
1085
|
+
patch: {
|
|
1086
|
+
summary: `Update ${item}`,
|
|
1087
|
+
operationId: `update${tag}`,
|
|
1088
|
+
tags: [tag],
|
|
1089
|
+
parameters: [idParam],
|
|
1090
|
+
requestBody: {
|
|
1091
|
+
required: true,
|
|
1092
|
+
content: { "application/json": { schema: { $ref: inputRef } } }
|
|
1093
|
+
},
|
|
1094
|
+
responses: {
|
|
1095
|
+
"200": itemResponse(ref),
|
|
1096
|
+
"404": errorResponse("Not Found", "NOT_FOUND"),
|
|
1097
|
+
"422": validationError
|
|
1098
|
+
}
|
|
1099
|
+
},
|
|
1100
|
+
delete: {
|
|
1101
|
+
summary: `Delete ${item}`,
|
|
1102
|
+
operationId: `delete${tag}`,
|
|
1103
|
+
tags: [tag],
|
|
1104
|
+
parameters: [idParam],
|
|
1105
|
+
responses: {
|
|
1106
|
+
"200": itemResponse(ref),
|
|
1107
|
+
"404": errorResponse("Not Found", "NOT_FOUND")
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
for (const route of customRoutes) {
|
|
1113
|
+
const fullPath = `${basePath}${route.path}`.replace(/:([a-zA-Z_]+)/g, "{$1}");
|
|
1114
|
+
if (!paths[fullPath]) paths[fullPath] = {};
|
|
1115
|
+
paths[fullPath][route.method.toLowerCase()] = {
|
|
1116
|
+
summary: `${route.method} ${route.path}`,
|
|
1117
|
+
tags: ["Custom"],
|
|
1118
|
+
responses: { "200": { description: "OK" } }
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
const spec = {
|
|
1122
|
+
openapi: "3.0.0",
|
|
1123
|
+
info: {
|
|
1124
|
+
title: info?.title ?? "Crudora API",
|
|
1125
|
+
version: info?.version ?? "1.0.0"
|
|
1126
|
+
},
|
|
1127
|
+
paths,
|
|
1128
|
+
components: { schemas }
|
|
1129
|
+
};
|
|
1130
|
+
if (info?.description) spec.info.description = info.description;
|
|
1131
|
+
return spec;
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
|
|
835
1135
|
// src/utils/validation.ts
|
|
836
1136
|
import { z } from "zod";
|
|
837
1137
|
function zodTypeFor(opts, forStrict) {
|
|
@@ -884,14 +1184,14 @@ function zodTypeFor(opts, forStrict) {
|
|
|
884
1184
|
}
|
|
885
1185
|
return base.optional();
|
|
886
1186
|
}
|
|
887
|
-
var
|
|
1187
|
+
var SYSTEM_FIELDS3 = /* @__PURE__ */ new Set(["createdAt", "updatedAt", "deletedAt"]);
|
|
888
1188
|
function resolveFields(modelClass) {
|
|
889
1189
|
const fieldMeta = getFieldMetadata(modelClass);
|
|
890
1190
|
const hasMeta = Object.keys(fieldMeta).length > 0;
|
|
891
1191
|
const fillable = modelClass.fillable;
|
|
892
1192
|
if (hasMeta) {
|
|
893
1193
|
let entries = Object.entries(fieldMeta).filter(
|
|
894
|
-
([name, opts]) => !opts.primary && !
|
|
1194
|
+
([name, opts]) => !opts.primary && !SYSTEM_FIELDS3.has(name)
|
|
895
1195
|
);
|
|
896
1196
|
if (fillable?.length) {
|
|
897
1197
|
entries = entries.filter(([name]) => fillable.includes(name));
|
|
@@ -1197,6 +1497,14 @@ var Crudora = class {
|
|
|
1197
1497
|
const modelClasses = Array.from(this.models.values());
|
|
1198
1498
|
return SchemaGenerator.generateDrizzleSchema(modelClasses, this.dialect);
|
|
1199
1499
|
}
|
|
1500
|
+
generateOpenApiSpec(basePath = "/api", info) {
|
|
1501
|
+
return OpenApiGenerator.generate(
|
|
1502
|
+
this.models,
|
|
1503
|
+
this.customRoutes.map(({ method, path }) => ({ method, path })),
|
|
1504
|
+
basePath,
|
|
1505
|
+
info
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1200
1508
|
getValidationSchema(modelClass) {
|
|
1201
1509
|
return ValidationGenerator.generateZodSchema(modelClass);
|
|
1202
1510
|
}
|
|
@@ -1446,6 +1754,7 @@ var Crudora = class {
|
|
|
1446
1754
|
};
|
|
1447
1755
|
|
|
1448
1756
|
// src/core/crudoraServer.ts
|
|
1757
|
+
import http from "http";
|
|
1449
1758
|
import express from "express";
|
|
1450
1759
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1451
1760
|
function createRateLimiter(config) {
|
|
@@ -1490,8 +1799,43 @@ function createDefaultLogger() {
|
|
|
1490
1799
|
debug: (msg, ctx) => console.debug(fmt("debug", msg, ctx))
|
|
1491
1800
|
};
|
|
1492
1801
|
}
|
|
1802
|
+
function resolveDocs(docs) {
|
|
1803
|
+
if (!docs) return false;
|
|
1804
|
+
if (docs === true) return { path: "/docs" };
|
|
1805
|
+
if (typeof docs === "string") return { path: docs };
|
|
1806
|
+
return { path: docs.path ?? "/docs", ...docs };
|
|
1807
|
+
}
|
|
1808
|
+
function tryRequireScalar() {
|
|
1809
|
+
try {
|
|
1810
|
+
return __require("@scalar/express-api-reference");
|
|
1811
|
+
} catch {
|
|
1812
|
+
return null;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
function buildInstallPromptHtml(specUrl) {
|
|
1816
|
+
return `<!DOCTYPE html>
|
|
1817
|
+
<html>
|
|
1818
|
+
<head>
|
|
1819
|
+
<title>API Docs</title>
|
|
1820
|
+
<meta charset="utf-8" />
|
|
1821
|
+
<style>
|
|
1822
|
+
body { font-family: system-ui, sans-serif; max-width: 600px; margin: 80px auto; padding: 0 24px; color: #333; }
|
|
1823
|
+
code { background: #f4f4f4; padding: 2px 6px; border-radius: 4px; }
|
|
1824
|
+
pre { background: #f4f4f4; padding: 16px; border-radius: 8px; overflow: auto; }
|
|
1825
|
+
a { color: #0070f3; }
|
|
1826
|
+
</style>
|
|
1827
|
+
</head>
|
|
1828
|
+
<body>
|
|
1829
|
+
<h2>API Docs</h2>
|
|
1830
|
+
<p>Install <code>@scalar/express-api-reference</code> to enable the interactive UI:</p>
|
|
1831
|
+
<pre>npm install @scalar/express-api-reference</pre>
|
|
1832
|
+
<p>OpenAPI spec: <a href="${specUrl}">${specUrl}</a></p>
|
|
1833
|
+
</body>
|
|
1834
|
+
</html>`;
|
|
1835
|
+
}
|
|
1493
1836
|
var CrudoraServer = class {
|
|
1494
1837
|
constructor(config) {
|
|
1838
|
+
this.httpServer = null;
|
|
1495
1839
|
const resolvedLogger = config.logger === void 0 ? createDefaultLogger() : config.logger;
|
|
1496
1840
|
const resolvedRateLimit = config.rateLimit === false ? false : {
|
|
1497
1841
|
windowMs: config.rateLimit?.windowMs ?? 6e4,
|
|
@@ -1505,9 +1849,12 @@ var CrudoraServer = class {
|
|
|
1505
1849
|
bodyParser: true,
|
|
1506
1850
|
bodyParserLimit: "100kb",
|
|
1507
1851
|
basePath: "/api",
|
|
1852
|
+
timeout: 0,
|
|
1853
|
+
healthCheck: true,
|
|
1508
1854
|
...config,
|
|
1509
1855
|
logger: resolvedLogger,
|
|
1510
|
-
rateLimit: resolvedRateLimit
|
|
1856
|
+
rateLimit: resolvedRateLimit,
|
|
1857
|
+
docs: resolveDocs(config.docs)
|
|
1511
1858
|
};
|
|
1512
1859
|
this.app = express();
|
|
1513
1860
|
this.crudora = new Crudora(
|
|
@@ -1528,6 +1875,19 @@ var CrudoraServer = class {
|
|
|
1528
1875
|
req.correlationId = randomUUID2();
|
|
1529
1876
|
next();
|
|
1530
1877
|
});
|
|
1878
|
+
if (this.config.timeout > 0) {
|
|
1879
|
+
const timeoutMs = this.config.timeout;
|
|
1880
|
+
this.app.use((_req, res, next) => {
|
|
1881
|
+
const timer = setTimeout(() => {
|
|
1882
|
+
if (!res.headersSent) {
|
|
1883
|
+
res.status(503).json({ success: false, error: { code: "TIMEOUT", message: "Request timed out" } });
|
|
1884
|
+
}
|
|
1885
|
+
}, timeoutMs);
|
|
1886
|
+
res.on("finish", () => clearTimeout(timer));
|
|
1887
|
+
res.on("close", () => clearTimeout(timer));
|
|
1888
|
+
next();
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1531
1891
|
if (this.config.rateLimit !== false) {
|
|
1532
1892
|
this.app.use(createRateLimiter(this.config.rateLimit));
|
|
1533
1893
|
}
|
|
@@ -1569,6 +1929,37 @@ var CrudoraServer = class {
|
|
|
1569
1929
|
return this;
|
|
1570
1930
|
}
|
|
1571
1931
|
generateRoutes() {
|
|
1932
|
+
if (this.config.healthCheck !== false) {
|
|
1933
|
+
const healthPath = typeof this.config.healthCheck === "string" ? this.config.healthCheck : "/health";
|
|
1934
|
+
this.app.get(healthPath, (_req, res) => {
|
|
1935
|
+
res.json({ success: true, data: { status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() } });
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
if (this.config.docs !== false) {
|
|
1939
|
+
const { path: docsPath, title, version, description, scalar: scalarOpts } = this.config.docs;
|
|
1940
|
+
const specPath = `${docsPath}/openapi.json`;
|
|
1941
|
+
const spec = this.crudora.generateOpenApiSpec(this.config.basePath, { title, version, description });
|
|
1942
|
+
this.app.get(specPath, (_req, res) => {
|
|
1943
|
+
res.json(spec);
|
|
1944
|
+
});
|
|
1945
|
+
const scalarPkg = tryRequireScalar();
|
|
1946
|
+
if (scalarPkg) {
|
|
1947
|
+
this.app.use(docsPath, scalarPkg.apiReference({
|
|
1948
|
+
spec: { url: specPath },
|
|
1949
|
+
...scalarOpts
|
|
1950
|
+
}));
|
|
1951
|
+
} else {
|
|
1952
|
+
if (this.config.logger !== false) {
|
|
1953
|
+
this.config.logger.warn(
|
|
1954
|
+
'docs is enabled but @scalar/express-api-reference is not installed. Run "npm install @scalar/express-api-reference" to enable the interactive UI.'
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
this.app.get(docsPath, (_req, res) => {
|
|
1958
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
1959
|
+
res.send(buildInstallPromptHtml(specPath));
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1572
1963
|
this.crudora.generateRoutes(this.app, this.config.basePath);
|
|
1573
1964
|
return this;
|
|
1574
1965
|
}
|
|
@@ -1576,12 +1967,32 @@ var CrudoraServer = class {
|
|
|
1576
1967
|
this.app.use(middleware);
|
|
1577
1968
|
return this;
|
|
1578
1969
|
}
|
|
1970
|
+
/**
|
|
1971
|
+
* Starts the HTTP server and returns the underlying `http.Server` instance.
|
|
1972
|
+
* Use the returned server for graceful shutdown:
|
|
1973
|
+
*
|
|
1974
|
+
* @example
|
|
1975
|
+
* const httpServer = server.listen();
|
|
1976
|
+
* process.on('SIGTERM', () => httpServer.close(() => process.exit(0)));
|
|
1977
|
+
*/
|
|
1579
1978
|
listen(callback) {
|
|
1580
|
-
this.
|
|
1979
|
+
this.httpServer = http.createServer(this.app);
|
|
1980
|
+
this.httpServer.listen(this.config.port, () => {
|
|
1581
1981
|
console.log(`\u{1F680} Crudora server running on port ${this.config.port}`);
|
|
1582
1982
|
console.log(`\u{1F4DA} API available at http://localhost:${this.config.port}${this.config.basePath}`);
|
|
1983
|
+
if (this.config.docs !== false) {
|
|
1984
|
+
console.log(`\u{1F4D6} API docs at http://localhost:${this.config.port}${this.config.docs.path}`);
|
|
1985
|
+
}
|
|
1583
1986
|
if (callback) callback();
|
|
1584
1987
|
});
|
|
1988
|
+
return this.httpServer;
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Returns the `http.Server` instance after `listen()` has been called, or `null` before.
|
|
1992
|
+
* Useful when you need the server reference without calling listen again.
|
|
1993
|
+
*/
|
|
1994
|
+
getHttpServer() {
|
|
1995
|
+
return this.httpServer;
|
|
1585
1996
|
}
|
|
1586
1997
|
getApp() {
|
|
1587
1998
|
return this.app;
|
|
@@ -1677,6 +2088,7 @@ export {
|
|
|
1677
2088
|
HasOne,
|
|
1678
2089
|
Model,
|
|
1679
2090
|
NotFoundError,
|
|
2091
|
+
OpenApiGenerator,
|
|
1680
2092
|
Repository,
|
|
1681
2093
|
SchemaGenerator,
|
|
1682
2094
|
ValidationGenerator,
|