rads-db 3.2.21 → 3.2.23

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