rads-db 3.2.23 → 3.2.25

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.
@@ -94,14 +94,136 @@ function buildObjectSchema(entity, schema, allOptional = false) {
94
94
  } : {})
95
95
  };
96
96
  }
97
- const whereSchema = {
97
+ const WHERE_OPERATORS_DESCRIPTION = "Suffix operators: strings \u2014 _in, _ieq, _istartsWith, _startsWith, _icontains, _contains, _iendsWith, _endsWith, _gt, _gte, _lt, _lte; numbers \u2014 _in, _gt, _gte, _lt, _lte; optional fields \u2014 _isNull; arrays \u2014 _isEmpty; JSON \u2014 _jsonContains";
98
+ const relationWhereOas = () => ({
98
99
  type: "object",
99
- additionalProperties: true
100
- };
101
- const includeSchema = {
102
- type: "object",
103
- additionalProperties: true
104
- };
100
+ properties: {
101
+ id: {
102
+ type: "string"
103
+ },
104
+ id_in: {
105
+ type: "array",
106
+ items: {
107
+ type: "string"
108
+ }
109
+ }
110
+ }
111
+ });
112
+ function buildWhereProps(typeDef, schema) {
113
+ const props = {};
114
+ for (const [, field] of Object.entries(typeDef.fields ?? {})) {
115
+ if (field.isInverseRelation) continue;
116
+ const {
117
+ name,
118
+ type,
119
+ isRelation,
120
+ isRequired,
121
+ isArray
122
+ } = field;
123
+ const entry = schema[type];
124
+ const isEnum = !!entry?.enumValues;
125
+ const isObject = !!entry?.fields;
126
+ const optional = !isRequired;
127
+ const enumSchema = isEnum ? {
128
+ type: "string",
129
+ enum: Object.keys(entry.enumValues)
130
+ } : {};
131
+ if (isArray) {
132
+ if (isEnum) props[`${name}_arrayContains`] = enumSchema;else if (!entry) props[`${name}_arrayContains`] = primitiveToOas(type);else if (isRelation) props[`${name}_some`] = relationWhereOas();else if (isObject) props[`${name}_some`] = {
133
+ $ref: `#/components/schemas/${type}Where`
134
+ };
135
+ props[`${name}_isEmpty`] = {
136
+ type: "boolean"
137
+ };
138
+ if (optional) props[`${name}_isNull`] = {
139
+ type: "boolean"
140
+ };
141
+ continue;
142
+ }
143
+ if (isRelation) {
144
+ props[name] = relationWhereOas();
145
+ } else if (isObject) {
146
+ props[name] = {
147
+ $ref: `#/components/schemas/${type}Where`
148
+ };
149
+ } else if (isEnum) {
150
+ props[name] = enumSchema;
151
+ } else if (type === "Record<string, any>") {
152
+ props[`${name}_jsonContains`] = {
153
+ $ref: "#/components/schemas/WhereJsonContains"
154
+ };
155
+ } else {
156
+ props[name] = primitiveToOas(type);
157
+ }
158
+ if (optional) props[`${name}_isNull`] = {
159
+ type: "boolean"
160
+ };
161
+ }
162
+ return props;
163
+ }
164
+ function buildWhereSchema(typeName, typeDef, schema) {
165
+ const selfRef = `#/components/schemas/${typeName}Where`;
166
+ return {
167
+ type: "object",
168
+ description: WHERE_OPERATORS_DESCRIPTION,
169
+ additionalProperties: true,
170
+ properties: {
171
+ _not: {
172
+ $ref: selfRef
173
+ },
174
+ _and: {
175
+ type: "array",
176
+ items: {
177
+ $ref: selfRef
178
+ }
179
+ },
180
+ _or: {
181
+ type: "array",
182
+ items: {
183
+ $ref: selfRef
184
+ }
185
+ },
186
+ ...buildWhereProps(typeDef, schema)
187
+ }
188
+ };
189
+ }
190
+ function buildIncludeSchema(typeDef, schema) {
191
+ const primitiveFields = [];
192
+ const includeProps = {};
193
+ for (const [, field] of Object.entries(typeDef.fields ?? {})) {
194
+ if (field.isInverseRelation) continue;
195
+ const entry = schema[field.type];
196
+ const canSubInclude = field.isRelation || entry?.fields && !entry.enumValues;
197
+ if (canSubInclude) {
198
+ includeProps[field.name] = {
199
+ $ref: `#/components/schemas/${field.type}Include`
200
+ };
201
+ } else {
202
+ primitiveFields.push(field.name);
203
+ }
204
+ }
205
+ return {
206
+ type: "object",
207
+ properties: {
208
+ _pick: {
209
+ type: "array",
210
+ items: {
211
+ type: "string",
212
+ enum: primitiveFields
213
+ }
214
+ },
215
+ ...includeProps
216
+ }
217
+ };
218
+ }
219
+ function buildAggEnum(typeDef) {
220
+ const values = ["_count"];
221
+ for (const [, field] of Object.entries(typeDef.fields ?? {})) {
222
+ if (field.isInverseRelation || field.isArray || field.isRelation) continue;
223
+ if (field.type === "number") values.push(`${field.name}_min`, `${field.name}_max`, `${field.name}_sum`);
224
+ }
225
+ return values;
226
+ }
105
227
  const paginationProps = {
106
228
  cursor: {
107
229
  type: "string"
@@ -121,36 +243,6 @@ const paginationProps = {
121
243
  description: "Multiple order-by expressions"
122
244
  }
123
245
  };
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
246
  function jsonBody(s) {
155
247
  return {
156
248
  required: true,
@@ -182,6 +274,22 @@ function getOpenApiSpec(options, openApiOptions) {
182
274
  const base = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
183
275
  const paths = {};
184
276
  const schemas = {};
277
+ schemas.WhereJsonContains = {
278
+ type: "object",
279
+ required: ["path"],
280
+ properties: {
281
+ path: {
282
+ type: "string",
283
+ description: 'JSON path, e.g. "meta.countries[0].name"'
284
+ },
285
+ isNull: {
286
+ type: "boolean"
287
+ },
288
+ value: {
289
+ description: "Expected value at path (string, number, or boolean)"
290
+ }
291
+ }
292
+ };
185
293
  paths[`${base}/me`] = {
186
294
  get: {
187
295
  summary: "Get current user",
@@ -260,17 +368,52 @@ function getOpenApiSpec(options, openApiOptions) {
260
368
  handlePlural
261
369
  } = entity;
262
370
  const tag = name;
263
- schemas[name] = buildObjectSchema(entity, schema);
264
- schemas[`${name}Put`] = buildObjectSchema(entity, schema, true);
265
- schemas[`${name}Put`].required = ["id"];
266
371
  const ref = n => ({
267
372
  $ref: `#/components/schemas/${n}`
268
373
  });
374
+ schemas[name] = buildObjectSchema(entity, schema);
375
+ schemas[`${name}Put`] = buildObjectSchema(entity, schema, true);
376
+ schemas[`${name}Put`].required = ["id"];
377
+ schemas[`${name}Where`] = buildWhereSchema(name, entity, schema);
378
+ schemas[`${name}Include`] = buildIncludeSchema(entity, schema);
379
+ const aggValues = buildAggEnum(entity);
380
+ const aggSchema = {
381
+ type: "array",
382
+ items: {
383
+ type: "string",
384
+ enum: aggValues
385
+ },
386
+ description: "Aggregation operations to compute"
387
+ };
388
+ const getReqBody = {
389
+ type: "object",
390
+ properties: {
391
+ where: ref(`${name}Where`),
392
+ include: ref(`${name}Include`)
393
+ }
394
+ };
395
+ const getManyReqBody = {
396
+ type: "object",
397
+ properties: {
398
+ where: ref(`${name}Where`),
399
+ include: ref(`${name}Include`),
400
+ ...paginationProps
401
+ }
402
+ };
403
+ const getAggReqBody = {
404
+ type: "object",
405
+ required: ["agg"],
406
+ properties: {
407
+ where: ref(`${name}Where`),
408
+ agg: aggSchema,
409
+ ...paginationProps
410
+ }
411
+ };
269
412
  paths[`${base}/${handle}`] = {
270
413
  post: {
271
414
  summary: `Get ${name}`,
272
415
  tags: [tag],
273
- requestBody: jsonBody(getRequestSchema),
416
+ requestBody: jsonBody(getReqBody),
274
417
  responses: jsonOk(ref(name), `${name} object`)
275
418
  },
276
419
  put: {
@@ -290,7 +433,7 @@ function getOpenApiSpec(options, openApiOptions) {
290
433
  post: {
291
434
  summary: `Get many ${name} objects`,
292
435
  tags: [tag],
293
- requestBody: jsonBody(getManyRequestSchema),
436
+ requestBody: jsonBody(getManyReqBody),
294
437
  responses: jsonOk({
295
438
  type: "object",
296
439
  required: ["nodes"],
@@ -329,7 +472,7 @@ function getOpenApiSpec(options, openApiOptions) {
329
472
  post: {
330
473
  summary: `Aggregate ${name} data`,
331
474
  tags: [tag],
332
- requestBody: jsonBody(getAggRequestSchema),
475
+ requestBody: jsonBody(getAggReqBody),
333
476
  responses: jsonOk({
334
477
  type: "object",
335
478
  additionalProperties: {
@@ -342,7 +485,7 @@ function getOpenApiSpec(options, openApiOptions) {
342
485
  }
343
486
  for (const key in schema) {
344
487
  const entry = schema[key];
345
- if (entry.decorators?.entity || schemas[key]) continue;
488
+ if (entry.decorators?.entity) continue;
346
489
  if (entry.enumValues) {
347
490
  schemas[key] = {
348
491
  type: "string",
@@ -352,7 +495,9 @@ function getOpenApiSpec(options, openApiOptions) {
352
495
  } : {})
353
496
  };
354
497
  } else if (entry.fields) {
355
- schemas[key] = buildObjectSchema(entry, schema);
498
+ if (!schemas[key]) schemas[key] = buildObjectSchema(entry, schema);
499
+ schemas[`${key}Where`] = buildWhereSchema(key, entry, schema);
500
+ schemas[`${key}Include`] = buildIncludeSchema(entry, schema);
356
501
  }
357
502
  }
358
503
  return {
@@ -52,35 +52,94 @@ function buildObjectSchema(entity, schema, allOptional = false) {
52
52
  ...required.length > 0 ? { required } : {}
53
53
  };
54
54
  }
55
- const whereSchema = { type: "object", additionalProperties: true };
56
- const includeSchema = { type: "object", additionalProperties: true };
55
+ const WHERE_OPERATORS_DESCRIPTION = "Suffix operators: strings \u2014 _in, _ieq, _istartsWith, _startsWith, _icontains, _contains, _iendsWith, _endsWith, _gt, _gte, _lt, _lte; numbers \u2014 _in, _gt, _gte, _lt, _lte; optional fields \u2014 _isNull; arrays \u2014 _isEmpty; JSON \u2014 _jsonContains";
56
+ const relationWhereOas = () => ({
57
+ type: "object",
58
+ properties: { id: { type: "string" }, id_in: { type: "array", items: { type: "string" } } }
59
+ });
60
+ function buildWhereProps(typeDef, schema) {
61
+ const props = {};
62
+ for (const [, field] of Object.entries(typeDef.fields ?? {})) {
63
+ if (field.isInverseRelation) continue;
64
+ const { name, type, isRelation, isRequired, isArray } = field;
65
+ const entry = schema[type];
66
+ const isEnum = !!entry?.enumValues;
67
+ const isObject = !!entry?.fields;
68
+ const optional = !isRequired;
69
+ const enumSchema = isEnum ? { type: "string", enum: Object.keys(entry.enumValues) } : {};
70
+ if (isArray) {
71
+ if (isEnum) props[`${name}_arrayContains`] = enumSchema;
72
+ else if (!entry) props[`${name}_arrayContains`] = primitiveToOas(type);
73
+ else if (isRelation) props[`${name}_some`] = relationWhereOas();
74
+ else if (isObject) props[`${name}_some`] = { $ref: `#/components/schemas/${type}Where` };
75
+ props[`${name}_isEmpty`] = { type: "boolean" };
76
+ if (optional) props[`${name}_isNull`] = { type: "boolean" };
77
+ continue;
78
+ }
79
+ if (isRelation) {
80
+ props[name] = relationWhereOas();
81
+ } else if (isObject) {
82
+ props[name] = { $ref: `#/components/schemas/${type}Where` };
83
+ } else if (isEnum) {
84
+ props[name] = enumSchema;
85
+ } else if (type === "Record<string, any>") {
86
+ props[`${name}_jsonContains`] = { $ref: "#/components/schemas/WhereJsonContains" };
87
+ } else {
88
+ props[name] = primitiveToOas(type);
89
+ }
90
+ if (optional) props[`${name}_isNull`] = { type: "boolean" };
91
+ }
92
+ return props;
93
+ }
94
+ function buildWhereSchema(typeName, typeDef, schema) {
95
+ const selfRef = `#/components/schemas/${typeName}Where`;
96
+ return {
97
+ type: "object",
98
+ description: WHERE_OPERATORS_DESCRIPTION,
99
+ additionalProperties: true,
100
+ properties: {
101
+ _not: { $ref: selfRef },
102
+ _and: { type: "array", items: { $ref: selfRef } },
103
+ _or: { type: "array", items: { $ref: selfRef } },
104
+ ...buildWhereProps(typeDef, schema)
105
+ }
106
+ };
107
+ }
108
+ function buildIncludeSchema(typeDef, schema) {
109
+ const primitiveFields = [];
110
+ const includeProps = {};
111
+ for (const [, field] of Object.entries(typeDef.fields ?? {})) {
112
+ if (field.isInverseRelation) continue;
113
+ const entry = schema[field.type];
114
+ const canSubInclude = field.isRelation || entry?.fields && !entry.enumValues;
115
+ if (canSubInclude) {
116
+ includeProps[field.name] = { $ref: `#/components/schemas/${field.type}Include` };
117
+ } else {
118
+ primitiveFields.push(field.name);
119
+ }
120
+ }
121
+ return {
122
+ type: "object",
123
+ properties: {
124
+ _pick: { type: "array", items: { type: "string", enum: primitiveFields } },
125
+ ...includeProps
126
+ }
127
+ };
128
+ }
129
+ function buildAggEnum(typeDef) {
130
+ const values = ["_count"];
131
+ for (const [, field] of Object.entries(typeDef.fields ?? {})) {
132
+ if (field.isInverseRelation || field.isArray || field.isRelation) continue;
133
+ if (field.type === "number") values.push(`${field.name}_min`, `${field.name}_max`, `${field.name}_sum`);
134
+ }
135
+ return values;
136
+ }
57
137
  const paginationProps = {
58
138
  cursor: { type: "string" },
59
139
  maxItemCount: { type: "integer" },
60
140
  orderBy: { type: "string", description: 'Single order-by expression, e.g. "name_asc"' },
61
141
  orderByArray: { type: "array", items: { type: "string" }, description: "Multiple order-by expressions" }
62
142
  };
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
143
  function jsonBody(s) {
85
144
  return { required: true, content: { "application/json": { schema: s } } };
86
145
  }
@@ -93,6 +152,15 @@ export function getOpenApiSpec(options, openApiOptions) {
93
152
  const base = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
94
153
  const paths = {};
95
154
  const schemas = {};
155
+ schemas.WhereJsonContains = {
156
+ type: "object",
157
+ required: ["path"],
158
+ properties: {
159
+ path: { type: "string", description: 'JSON path, e.g. "meta.countries[0].name"' },
160
+ isNull: { type: "boolean" },
161
+ value: { description: "Expected value at path (string, number, or boolean)" }
162
+ }
163
+ };
96
164
  paths[`${base}/me`] = {
97
165
  get: {
98
166
  summary: "Get current user",
@@ -143,15 +211,36 @@ export function getOpenApiSpec(options, openApiOptions) {
143
211
  if (!entity.decorators?.entity) continue;
144
212
  const { name, handle, handlePlural } = entity;
145
213
  const tag = name;
214
+ const ref = (n) => ({ $ref: `#/components/schemas/${n}` });
146
215
  schemas[name] = buildObjectSchema(entity, schema);
147
216
  schemas[`${name}Put`] = buildObjectSchema(entity, schema, true);
148
217
  schemas[`${name}Put`].required = ["id"];
149
- const ref = (n) => ({ $ref: `#/components/schemas/${n}` });
218
+ schemas[`${name}Where`] = buildWhereSchema(name, entity, schema);
219
+ schemas[`${name}Include`] = buildIncludeSchema(entity, schema);
220
+ const aggValues = buildAggEnum(entity);
221
+ const aggSchema = {
222
+ type: "array",
223
+ items: { type: "string", enum: aggValues },
224
+ description: "Aggregation operations to compute"
225
+ };
226
+ const getReqBody = {
227
+ type: "object",
228
+ properties: { where: ref(`${name}Where`), include: ref(`${name}Include`) }
229
+ };
230
+ const getManyReqBody = {
231
+ type: "object",
232
+ properties: { where: ref(`${name}Where`), include: ref(`${name}Include`), ...paginationProps }
233
+ };
234
+ const getAggReqBody = {
235
+ type: "object",
236
+ required: ["agg"],
237
+ properties: { where: ref(`${name}Where`), agg: aggSchema, ...paginationProps }
238
+ };
150
239
  paths[`${base}/${handle}`] = {
151
240
  post: {
152
241
  summary: `Get ${name}`,
153
242
  tags: [tag],
154
- requestBody: jsonBody(getRequestSchema),
243
+ requestBody: jsonBody(getReqBody),
155
244
  responses: jsonOk(ref(name), `${name} object`)
156
245
  },
157
246
  put: {
@@ -169,7 +258,7 @@ export function getOpenApiSpec(options, openApiOptions) {
169
258
  post: {
170
259
  summary: `Get many ${name} objects`,
171
260
  tags: [tag],
172
- requestBody: jsonBody(getManyRequestSchema),
261
+ requestBody: jsonBody(getManyReqBody),
173
262
  responses: jsonOk(
174
263
  {
175
264
  type: "object",
@@ -197,7 +286,7 @@ export function getOpenApiSpec(options, openApiOptions) {
197
286
  post: {
198
287
  summary: `Aggregate ${name} data`,
199
288
  tags: [tag],
200
- requestBody: jsonBody(getAggRequestSchema),
289
+ requestBody: jsonBody(getAggReqBody),
201
290
  responses: jsonOk(
202
291
  { type: "object", additionalProperties: { type: "number", nullable: true } },
203
292
  `${name} aggregation result`
@@ -207,7 +296,7 @@ export function getOpenApiSpec(options, openApiOptions) {
207
296
  }
208
297
  for (const key in schema) {
209
298
  const entry = schema[key];
210
- if (entry.decorators?.entity || schemas[key]) continue;
299
+ if (entry.decorators?.entity) continue;
211
300
  if (entry.enumValues) {
212
301
  schemas[key] = {
213
302
  type: "string",
@@ -215,7 +304,9 @@ export function getOpenApiSpec(options, openApiOptions) {
215
304
  ...entry.comment ? { description: entry.comment } : {}
216
305
  };
217
306
  } else if (entry.fields) {
218
- schemas[key] = buildObjectSchema(entry, schema);
307
+ if (!schemas[key]) schemas[key] = buildObjectSchema(entry, schema);
308
+ schemas[`${key}Where`] = buildWhereSchema(key, entry, schema);
309
+ schemas[`${key}Include`] = buildIncludeSchema(entry, schema);
219
310
  }
220
311
  }
221
312
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rads-db",
3
- "version": "3.2.23",
3
+ "version": "3.2.25",
4
4
  "description": "Say goodbye to boilerplate code and hello to efficient and elegant syntax.",
5
5
  "author": "",
6
6
  "license": "ISC",