rads-db 3.2.21 → 3.2.22

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.
@@ -0,0 +1,357 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.getOpenApiSpec = getOpenApiSpec;
7
+ function primitiveToOas(type) {
8
+ switch (type) {
9
+ case "string":
10
+ return {
11
+ type: "string"
12
+ };
13
+ case "number":
14
+ return {
15
+ type: "number"
16
+ };
17
+ case "boolean":
18
+ return {
19
+ type: "boolean"
20
+ };
21
+ case "Record<string, string>":
22
+ return {
23
+ type: "object",
24
+ additionalProperties: {
25
+ type: "string"
26
+ }
27
+ };
28
+ case "Record<string, any>":
29
+ return {
30
+ type: "object",
31
+ additionalProperties: true
32
+ };
33
+ default:
34
+ return {};
35
+ }
36
+ }
37
+ function fieldToOas(field, schema) {
38
+ const entry = schema[field.type];
39
+ let itemSchema;
40
+ if (entry) {
41
+ itemSchema = entry.enumValues ? {
42
+ type: "string",
43
+ enum: Object.keys(entry.enumValues)
44
+ } : {
45
+ $ref: `#/components/schemas/${field.type}`
46
+ };
47
+ } else {
48
+ itemSchema = primitiveToOas(field.type);
49
+ }
50
+ return field.isArray ? {
51
+ type: "array",
52
+ items: itemSchema
53
+ } : itemSchema;
54
+ }
55
+ function buildObjectSchema(entity, schema, allOptional = false) {
56
+ const properties = {};
57
+ const required = [];
58
+ for (const [name, field] of Object.entries(entity.fields ?? {})) {
59
+ if (field.isInverseRelation) continue;
60
+ properties[name] = {
61
+ ...fieldToOas(field, schema),
62
+ ...(field.comment ? {
63
+ description: field.comment
64
+ } : {})
65
+ };
66
+ if (!allOptional && field.isRequired) required.push(name);
67
+ }
68
+ return {
69
+ type: "object",
70
+ ...(entity.comment ? {
71
+ description: entity.comment
72
+ } : {}),
73
+ properties,
74
+ ...(required.length > 0 ? {
75
+ required
76
+ } : {})
77
+ };
78
+ }
79
+ const whereSchema = {
80
+ type: "object",
81
+ additionalProperties: true
82
+ };
83
+ const includeSchema = {
84
+ type: "object",
85
+ additionalProperties: true
86
+ };
87
+ const paginationProps = {
88
+ cursor: {
89
+ type: "string"
90
+ },
91
+ maxItemCount: {
92
+ type: "integer"
93
+ },
94
+ orderBy: {
95
+ type: "string",
96
+ description: 'Single order-by expression, e.g. "name_asc"'
97
+ },
98
+ orderByArray: {
99
+ type: "array",
100
+ items: {
101
+ type: "string"
102
+ },
103
+ description: "Multiple order-by expressions"
104
+ }
105
+ };
106
+ const getRequestSchema = {
107
+ type: "object",
108
+ properties: {
109
+ where: whereSchema,
110
+ include: includeSchema
111
+ }
112
+ };
113
+ const getManyRequestSchema = {
114
+ type: "object",
115
+ properties: {
116
+ where: whereSchema,
117
+ include: includeSchema,
118
+ ...paginationProps
119
+ }
120
+ };
121
+ const getAggRequestSchema = {
122
+ type: "object",
123
+ required: ["agg"],
124
+ properties: {
125
+ where: whereSchema,
126
+ agg: {
127
+ type: "array",
128
+ items: {
129
+ type: "string"
130
+ },
131
+ description: 'Aggregation operations: "_count", "field_min", "field_max", "field_sum"'
132
+ },
133
+ ...paginationProps
134
+ }
135
+ };
136
+ function jsonBody(s) {
137
+ return {
138
+ required: true,
139
+ content: {
140
+ "application/json": {
141
+ schema: s
142
+ }
143
+ }
144
+ };
145
+ }
146
+ function jsonOk(s, description = "Success") {
147
+ return {
148
+ "200": {
149
+ description,
150
+ content: {
151
+ "application/json": {
152
+ schema: s
153
+ }
154
+ }
155
+ }
156
+ };
157
+ }
158
+ function getOpenApiSpec(options, openApiOptions) {
159
+ const {
160
+ db,
161
+ prefix = "/api/"
162
+ } = options;
163
+ const schema = db._schema;
164
+ const base = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
165
+ const paths = {};
166
+ const schemas = {};
167
+ paths[`${base}/me`] = {
168
+ get: {
169
+ summary: "Get current user",
170
+ tags: ["Auth"],
171
+ responses: jsonOk({
172
+ type: "object",
173
+ properties: {
174
+ userId: {
175
+ type: "string",
176
+ nullable: true
177
+ }
178
+ }
179
+ })
180
+ }
181
+ };
182
+ paths[`${base}/uploadFile`] = {
183
+ post: {
184
+ summary: "Upload a file",
185
+ tags: ["Files"],
186
+ requestBody: jsonBody({
187
+ type: "object",
188
+ required: ["blobDataUrl"],
189
+ properties: {
190
+ blobDataUrl: {
191
+ type: "string",
192
+ description: "Base64 data URL: data:<mime>;base64,<data>"
193
+ },
194
+ filename: {
195
+ type: "string"
196
+ },
197
+ containerName: {
198
+ type: "string"
199
+ }
200
+ }
201
+ }),
202
+ responses: jsonOk({
203
+ type: "object",
204
+ required: ["url"],
205
+ properties: {
206
+ url: {
207
+ type: "string"
208
+ }
209
+ }
210
+ })
211
+ }
212
+ };
213
+ paths[`${base}/radsTunnel`] = {
214
+ post: {
215
+ summary: "Direct database method access",
216
+ tags: ["Tunnel"],
217
+ requestBody: jsonBody({
218
+ type: "object",
219
+ properties: {
220
+ method: {
221
+ type: "string",
222
+ description: "Method to call (getMany, put, _schema, uploadFile, \u2026)"
223
+ },
224
+ entity: {
225
+ type: "string",
226
+ description: "Entity class name, e.g. TcUser"
227
+ },
228
+ args: {
229
+ description: "Arguments forwarded to the method"
230
+ }
231
+ }
232
+ }),
233
+ responses: jsonOk({})
234
+ }
235
+ };
236
+ for (const key in schema) {
237
+ const entity = schema[key];
238
+ if (!entity.decorators?.entity) continue;
239
+ const {
240
+ name,
241
+ handle,
242
+ handlePlural
243
+ } = entity;
244
+ const tag = name;
245
+ schemas[name] = buildObjectSchema(entity, schema);
246
+ schemas[`${name}Put`] = buildObjectSchema(entity, schema, true);
247
+ schemas[`${name}Put`].required = ["id"];
248
+ const ref = n => ({
249
+ $ref: `#/components/schemas/${n}`
250
+ });
251
+ paths[`${base}/${handle}`] = {
252
+ post: {
253
+ summary: `Get ${name}`,
254
+ tags: [tag],
255
+ requestBody: jsonBody(getRequestSchema),
256
+ responses: jsonOk(ref(name), `${name} object`)
257
+ },
258
+ put: {
259
+ summary: `Create or update ${name}`,
260
+ tags: [tag],
261
+ requestBody: jsonBody({
262
+ type: "object",
263
+ required: ["data"],
264
+ properties: {
265
+ data: ref(`${name}Put`)
266
+ }
267
+ }),
268
+ responses: jsonOk(ref(name), `Updated ${name}`)
269
+ }
270
+ };
271
+ paths[`${base}/${handlePlural}`] = {
272
+ post: {
273
+ summary: `Get many ${name} objects`,
274
+ tags: [tag],
275
+ requestBody: jsonBody(getManyRequestSchema),
276
+ responses: jsonOk({
277
+ type: "object",
278
+ required: ["nodes"],
279
+ properties: {
280
+ nodes: {
281
+ type: "array",
282
+ items: ref(name)
283
+ },
284
+ cursor: {
285
+ type: "string",
286
+ nullable: true
287
+ }
288
+ }
289
+ }, `Paginated list of ${name}`)
290
+ },
291
+ put: {
292
+ summary: `Create or update many ${name} objects`,
293
+ tags: [tag],
294
+ requestBody: jsonBody({
295
+ type: "object",
296
+ required: ["data"],
297
+ properties: {
298
+ data: {
299
+ type: "array",
300
+ items: ref(`${name}Put`)
301
+ }
302
+ }
303
+ }),
304
+ responses: jsonOk({
305
+ type: "array",
306
+ items: ref(name)
307
+ }, `Updated ${name} list`)
308
+ }
309
+ };
310
+ paths[`${base}/${handle}/agg`] = {
311
+ post: {
312
+ summary: `Aggregate ${name} data`,
313
+ tags: [tag],
314
+ requestBody: jsonBody(getAggRequestSchema),
315
+ responses: jsonOk({
316
+ type: "object",
317
+ additionalProperties: {
318
+ type: "number",
319
+ nullable: true
320
+ }
321
+ }, `${name} aggregation result`)
322
+ }
323
+ };
324
+ }
325
+ for (const key in schema) {
326
+ const entry = schema[key];
327
+ if (entry.decorators?.entity || schemas[key]) continue;
328
+ if (entry.enumValues) {
329
+ schemas[key] = {
330
+ type: "string",
331
+ enum: Object.keys(entry.enumValues),
332
+ ...(entry.comment ? {
333
+ description: entry.comment
334
+ } : {})
335
+ };
336
+ } else if (entry.fields) {
337
+ schemas[key] = buildObjectSchema(entry, schema);
338
+ }
339
+ }
340
+ return {
341
+ openapi: "3.0.3",
342
+ info: {
343
+ title: openApiOptions?.title ?? "Rads DB API",
344
+ version: openApiOptions?.version ?? "1.0.0",
345
+ ...(openApiOptions?.description ? {
346
+ description: openApiOptions.description
347
+ } : {})
348
+ },
349
+ ...(openApiOptions?.servers ? {
350
+ servers: openApiOptions.servers
351
+ } : {}),
352
+ paths,
353
+ components: {
354
+ schemas
355
+ }
356
+ };
357
+ }
@@ -0,0 +1,11 @@
1
+ import type { GetRestRoutesOptions } from '../types';
2
+ export interface OpenApiSpecOptions {
3
+ title?: string;
4
+ version?: string;
5
+ description?: string;
6
+ servers?: {
7
+ url: string;
8
+ description?: string;
9
+ }[];
10
+ }
11
+ export declare function getOpenApiSpec(options: GetRestRoutesOptions, openApiOptions?: OpenApiSpecOptions): object;
@@ -0,0 +1,222 @@
1
+ function primitiveToOas(type) {
2
+ switch (type) {
3
+ case "string":
4
+ return { type: "string" };
5
+ case "number":
6
+ return { type: "number" };
7
+ case "boolean":
8
+ return { type: "boolean" };
9
+ case "Record<string, string>":
10
+ return { type: "object", additionalProperties: { type: "string" } };
11
+ case "Record<string, any>":
12
+ return { type: "object", additionalProperties: true };
13
+ default:
14
+ return {};
15
+ }
16
+ }
17
+ function fieldToOas(field, schema) {
18
+ const entry = schema[field.type];
19
+ let itemSchema;
20
+ if (entry) {
21
+ itemSchema = entry.enumValues ? { type: "string", enum: Object.keys(entry.enumValues) } : { $ref: `#/components/schemas/${field.type}` };
22
+ } else {
23
+ itemSchema = primitiveToOas(field.type);
24
+ }
25
+ return field.isArray ? { type: "array", items: itemSchema } : itemSchema;
26
+ }
27
+ function buildObjectSchema(entity, schema, allOptional = false) {
28
+ const properties = {};
29
+ const required = [];
30
+ for (const [name, field] of Object.entries(entity.fields ?? {})) {
31
+ if (field.isInverseRelation) continue;
32
+ properties[name] = {
33
+ ...fieldToOas(field, schema),
34
+ ...field.comment ? { description: field.comment } : {}
35
+ };
36
+ if (!allOptional && field.isRequired) required.push(name);
37
+ }
38
+ return {
39
+ type: "object",
40
+ ...entity.comment ? { description: entity.comment } : {},
41
+ properties,
42
+ ...required.length > 0 ? { required } : {}
43
+ };
44
+ }
45
+ const whereSchema = { type: "object", additionalProperties: true };
46
+ const includeSchema = { type: "object", additionalProperties: true };
47
+ const paginationProps = {
48
+ cursor: { type: "string" },
49
+ maxItemCount: { type: "integer" },
50
+ orderBy: { type: "string", description: 'Single order-by expression, e.g. "name_asc"' },
51
+ orderByArray: { type: "array", items: { type: "string" }, description: "Multiple order-by expressions" }
52
+ };
53
+ const getRequestSchema = {
54
+ type: "object",
55
+ properties: { where: whereSchema, include: includeSchema }
56
+ };
57
+ const getManyRequestSchema = {
58
+ type: "object",
59
+ properties: { where: whereSchema, include: includeSchema, ...paginationProps }
60
+ };
61
+ const getAggRequestSchema = {
62
+ type: "object",
63
+ required: ["agg"],
64
+ properties: {
65
+ where: whereSchema,
66
+ agg: {
67
+ type: "array",
68
+ items: { type: "string" },
69
+ description: 'Aggregation operations: "_count", "field_min", "field_max", "field_sum"'
70
+ },
71
+ ...paginationProps
72
+ }
73
+ };
74
+ function jsonBody(s) {
75
+ return { required: true, content: { "application/json": { schema: s } } };
76
+ }
77
+ function jsonOk(s, description = "Success") {
78
+ return { "200": { description, content: { "application/json": { schema: s } } } };
79
+ }
80
+ export function getOpenApiSpec(options, openApiOptions) {
81
+ const { db, prefix = "/api/" } = options;
82
+ const schema = db._schema;
83
+ const base = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
84
+ const paths = {};
85
+ const schemas = {};
86
+ paths[`${base}/me`] = {
87
+ get: {
88
+ summary: "Get current user",
89
+ tags: ["Auth"],
90
+ responses: jsonOk({
91
+ type: "object",
92
+ properties: { userId: { type: "string", nullable: true } }
93
+ })
94
+ }
95
+ };
96
+ paths[`${base}/uploadFile`] = {
97
+ post: {
98
+ summary: "Upload a file",
99
+ tags: ["Files"],
100
+ requestBody: jsonBody({
101
+ type: "object",
102
+ required: ["blobDataUrl"],
103
+ properties: {
104
+ blobDataUrl: { type: "string", description: "Base64 data URL: data:<mime>;base64,<data>" },
105
+ filename: { type: "string" },
106
+ containerName: { type: "string" }
107
+ }
108
+ }),
109
+ responses: jsonOk({
110
+ type: "object",
111
+ required: ["url"],
112
+ properties: { url: { type: "string" } }
113
+ })
114
+ }
115
+ };
116
+ paths[`${base}/radsTunnel`] = {
117
+ post: {
118
+ summary: "Direct database method access",
119
+ tags: ["Tunnel"],
120
+ requestBody: jsonBody({
121
+ type: "object",
122
+ properties: {
123
+ method: { type: "string", description: "Method to call (getMany, put, _schema, uploadFile, \u2026)" },
124
+ entity: { type: "string", description: "Entity class name, e.g. TcUser" },
125
+ args: { description: "Arguments forwarded to the method" }
126
+ }
127
+ }),
128
+ responses: jsonOk({})
129
+ }
130
+ };
131
+ for (const key in schema) {
132
+ const entity = schema[key];
133
+ if (!entity.decorators?.entity) continue;
134
+ const { name, handle, handlePlural } = entity;
135
+ const tag = name;
136
+ schemas[name] = buildObjectSchema(entity, schema);
137
+ schemas[`${name}Put`] = buildObjectSchema(entity, schema, true);
138
+ schemas[`${name}Put`].required = ["id"];
139
+ const ref = (n) => ({ $ref: `#/components/schemas/${n}` });
140
+ paths[`${base}/${handle}`] = {
141
+ post: {
142
+ summary: `Get ${name}`,
143
+ tags: [tag],
144
+ requestBody: jsonBody(getRequestSchema),
145
+ responses: jsonOk(ref(name), `${name} object`)
146
+ },
147
+ put: {
148
+ summary: `Create or update ${name}`,
149
+ tags: [tag],
150
+ requestBody: jsonBody({
151
+ type: "object",
152
+ required: ["data"],
153
+ properties: { data: ref(`${name}Put`) }
154
+ }),
155
+ responses: jsonOk(ref(name), `Updated ${name}`)
156
+ }
157
+ };
158
+ paths[`${base}/${handlePlural}`] = {
159
+ post: {
160
+ summary: `Get many ${name} objects`,
161
+ tags: [tag],
162
+ requestBody: jsonBody(getManyRequestSchema),
163
+ responses: jsonOk(
164
+ {
165
+ type: "object",
166
+ required: ["nodes"],
167
+ properties: {
168
+ nodes: { type: "array", items: ref(name) },
169
+ cursor: { type: "string", nullable: true }
170
+ }
171
+ },
172
+ `Paginated list of ${name}`
173
+ )
174
+ },
175
+ put: {
176
+ summary: `Create or update many ${name} objects`,
177
+ tags: [tag],
178
+ requestBody: jsonBody({
179
+ type: "object",
180
+ required: ["data"],
181
+ properties: { data: { type: "array", items: ref(`${name}Put`) } }
182
+ }),
183
+ responses: jsonOk({ type: "array", items: ref(name) }, `Updated ${name} list`)
184
+ }
185
+ };
186
+ paths[`${base}/${handle}/agg`] = {
187
+ post: {
188
+ summary: `Aggregate ${name} data`,
189
+ tags: [tag],
190
+ requestBody: jsonBody(getAggRequestSchema),
191
+ responses: jsonOk(
192
+ { type: "object", additionalProperties: { type: "number", nullable: true } },
193
+ `${name} aggregation result`
194
+ )
195
+ }
196
+ };
197
+ }
198
+ for (const key in schema) {
199
+ const entry = schema[key];
200
+ if (entry.decorators?.entity || schemas[key]) continue;
201
+ if (entry.enumValues) {
202
+ schemas[key] = {
203
+ type: "string",
204
+ enum: Object.keys(entry.enumValues),
205
+ ...entry.comment ? { description: entry.comment } : {}
206
+ };
207
+ } else if (entry.fields) {
208
+ schemas[key] = buildObjectSchema(entry, schema);
209
+ }
210
+ }
211
+ return {
212
+ openapi: "3.0.3",
213
+ info: {
214
+ title: openApiOptions?.title ?? "Rads DB API",
215
+ version: openApiOptions?.version ?? "1.0.0",
216
+ ...openApiOptions?.description ? { description: openApiOptions.description } : {}
217
+ },
218
+ ...openApiOptions?.servers ? { servers: openApiOptions.servers } : {},
219
+ paths,
220
+ components: { schemas }
221
+ };
222
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rads-db",
3
- "version": "3.2.21",
3
+ "version": "3.2.22",
4
4
  "description": "Say goodbye to boilerplate code and hello to efficient and elegant syntax.",
5
5
  "author": "",
6
6
  "license": "ISC",