@platformatic/sql-openapi 3.4.1 → 3.5.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.
@@ -1,12 +1,13 @@
1
- 'use strict'
2
-
3
- const { mapSQLTypeToOpenAPIType } = require('@platformatic/sql-json-schema-mapper')
4
- const { findNearestString } = require('@platformatic/utils')
5
- const camelcase = require('camelcase')
6
- const { generateArgs, rootEntityRoutes, capitalize, getFieldsForEntity } = require('./shared')
7
- const errors = require('./errors')
8
-
9
- const getEntityLinksForEntity = (app, entity) => {
1
+ import { findNearestString } from '@platformatic/foundation'
2
+ import { mapSQLTypeToOpenAPIType } from '@platformatic/sql-json-schema-mapper'
3
+ import camelcase from 'camelcase'
4
+ import {
5
+ UnableToCreateTheRouteForThePKColRelationshipError,
6
+ UnableToCreateTheRouteForTheReverseRelationshipError
7
+ } from './errors.js'
8
+ import { capitalize, generateArgs, getFieldsForEntity, rootEntityRoutes } from './shared.js'
9
+
10
+ function getEntityLinksForEntity (app, entity) {
10
11
  const entityLinks = {}
11
12
  for (const relation of entity.relations) {
12
13
  const ownField = camelcase(relation.column_name)
@@ -17,8 +18,8 @@ const getEntityLinksForEntity = (app, entity) => {
17
18
  entityLinks[getEntityById] = {
18
19
  operationId: `get${relatedEntity.name}By${relatedEntityPrimaryKeyCamelcaseCapitalized}`,
19
20
  parameters: {
20
- [relatedEntityPrimaryKeyCamelcase]: `$response.body#/${ownField}`,
21
- },
21
+ [relatedEntityPrimaryKeyCamelcase]: `$response.body#/${ownField}`
22
+ }
22
23
  }
23
24
  }
24
25
 
@@ -34,35 +35,34 @@ const getEntityLinksForEntity = (app, entity) => {
34
35
  entityLinks[getEntities] = {
35
36
  operationId: `get${capitalize(relatedEntity.pluralName)}`,
36
37
  parameters: {
37
- [`where.${theirField}.eq`]: `$response.body#/${ownField}`,
38
- },
38
+ [`where.${theirField}.eq`]: `$response.body#/${ownField}`
39
+ }
39
40
  }
40
41
  }
41
42
  return entityLinks
42
43
  }
43
44
 
44
- async function entityPlugin (app, opts) {
45
+ export async function entityPlugin (app, opts) {
45
46
  const entity = opts.entity
46
47
  const ignore = opts.ignore
47
48
  const ignoreRoutes = opts.ignoreRoutes
48
49
 
49
50
  const entitySchema = {
50
- $ref: entity.name + '#',
51
+ $ref: entity.name + '#'
51
52
  }
52
53
 
53
54
  const entitySchemaInput = {
54
- $ref: entity.name + 'Input#',
55
+ $ref: entity.name + 'Input#'
55
56
  }
56
57
 
57
- const entityFieldsNames = Object.values(entity.fields)
58
- .map(field => field.camelcase)
58
+ const entityFieldsNames = Object.values(entity.fields).map(field => field.camelcase)
59
59
 
60
60
  for (const ignoredField of Object.keys(ignore)) {
61
61
  if (!entityFieldsNames.includes(ignoredField)) {
62
62
  const nearestField = findNearestString(entityFieldsNames, ignoredField)
63
63
  app.log.warn(
64
64
  `Ignored openapi field "${ignoredField}" not found in entity "${entity.singularName}".` +
65
- ` Did you mean "${nearestField}"?`
65
+ ` Did you mean "${nearestField}"?`
66
66
  )
67
67
  }
68
68
  }
@@ -74,7 +74,7 @@ async function entityPlugin (app, opts) {
74
74
 
75
75
  const { whereArgs, orderByArgs } = generateArgs(entity, ignore)
76
76
 
77
- app.addHook('preValidation', async (req) => {
77
+ app.addHook('preValidation', async req => {
78
78
  if (typeof req.query.fields === 'string') {
79
79
  req.query.fields = req.query.fields.split(',')
80
80
  }
@@ -82,7 +82,17 @@ async function entityPlugin (app, opts) {
82
82
 
83
83
  const fields = getFieldsForEntity(entity, ignore)
84
84
 
85
- rootEntityRoutes(app, entity, whereArgs, orderByArgs, entityLinks, entitySchema, fields, entitySchemaInput, ignoreRoutes)
85
+ rootEntityRoutes(
86
+ app,
87
+ entity,
88
+ whereArgs,
89
+ orderByArgs,
90
+ entityLinks,
91
+ entitySchema,
92
+ fields,
93
+ entitySchemaInput,
94
+ ignoreRoutes
95
+ )
86
96
 
87
97
  const openapiPath = `${app.prefix}/{${primaryKeyCamelcase}}`
88
98
  const ignoredGETRoute = ignoreRoutes.find(ignoreRoute => {
@@ -90,42 +100,46 @@ async function entityPlugin (app, opts) {
90
100
  })
91
101
 
92
102
  if (!ignoredGETRoute) {
93
- app.get(`/:${primaryKeyCamelcase}`, {
94
- schema: {
95
- operationId: `get${entity.name}By${capitalize(primaryKeyCamelcase)}`,
96
- summary: `Get ${entity.name} by ${primaryKeyCamelcase}.`,
97
- description: `Fetch ${entity.name} using its ${primaryKeyCamelcase} from the database.`,
98
- params: primaryKeyParams,
99
- tags: [entity.table],
100
- querystring: {
101
- type: 'object',
102
- properties: {
103
- fields,
103
+ app.get(
104
+ `/:${primaryKeyCamelcase}`,
105
+ {
106
+ schema: {
107
+ operationId: `get${entity.name}By${capitalize(primaryKeyCamelcase)}`,
108
+ summary: `Get ${entity.name} by ${primaryKeyCamelcase}.`,
109
+ description: `Fetch ${entity.name} using its ${primaryKeyCamelcase} from the database.`,
110
+ params: primaryKeyParams,
111
+ tags: [entity.table],
112
+ querystring: {
113
+ type: 'object',
114
+ properties: {
115
+ fields
116
+ }
104
117
  },
118
+ response: {
119
+ 200: entitySchema
120
+ }
105
121
  },
106
- response: {
107
- 200: entitySchema,
108
- },
109
- },
110
- links: {
111
- 200: entityLinks,
122
+ links: {
123
+ 200: entityLinks
124
+ }
112
125
  },
113
- }, async function (request, reply) {
114
- const ctx = { app: this, reply }
115
- const res = await entity.find({
116
- ctx,
117
- where: {
118
- [primaryKeyCamelcase]: {
119
- eq: request.params[primaryKeyCamelcase],
126
+ async function (request, reply) {
127
+ const ctx = { app: this, reply }
128
+ const res = await entity.find({
129
+ ctx,
130
+ where: {
131
+ [primaryKeyCamelcase]: {
132
+ eq: request.params[primaryKeyCamelcase]
133
+ }
120
134
  },
121
- },
122
- fields: request.query.fields,
123
- })
124
- if (res.length === 0) {
125
- return reply.callNotFound()
135
+ fields: request.query.fields
136
+ })
137
+ if (res.length === 0) {
138
+ return reply.callNotFound()
139
+ }
140
+ return res[0]
126
141
  }
127
- return res[0]
128
- })
142
+ )
129
143
  }
130
144
 
131
145
  const mapRoutePathNamesReverseRelations = new Map()
@@ -136,15 +150,16 @@ async function entityPlugin (app, opts) {
136
150
  const targetEntity = app.platformatic.entities[targetEntityName]
137
151
  const targetForeignKeyCamelcase = camelcase(reverseRelationship.relation.column_name)
138
152
  const targetEntitySchema = {
139
- $ref: targetEntity.name + '#',
153
+ $ref: targetEntity.name + '#'
140
154
  }
141
155
  const entityLinks = getEntityLinksForEntity(app, targetEntity)
142
156
  // e.g. getQuotesForMovie
143
157
  const operationId = `get${capitalize(targetEntity.pluralName)}For${capitalize(entity.singularName)}`
144
158
 
145
- let routePathName = targetEntity.relations.length > 1
146
- ? camelcase([reverseRelationship.sourceEntity, targetForeignKeyCamelcase])
147
- : targetEntity.pluralName
159
+ let routePathName =
160
+ targetEntity.relations.length > 1
161
+ ? camelcase([reverseRelationship.sourceEntity, targetForeignKeyCamelcase])
162
+ : targetEntity.pluralName
148
163
 
149
164
  if (mapRoutePathNamesReverseRelations.get(routePathName)) {
150
165
  idxRoutePathNamesReverseRelations++
@@ -160,69 +175,96 @@ async function entityPlugin (app, opts) {
160
175
 
161
176
  if (!ignoredReversedGETRoute) {
162
177
  try {
163
- app.get(`/:${camelcase(primaryKey)}/${routePathName}`, {
164
- schema: {
165
- operationId,
166
- summary: `Get ${targetEntity.pluralName} for ${entity.singularName}.`,
167
- description: `Fetch all the ${targetEntity.pluralName} for ${entity.singularName} from the database.`,
168
- params: getPrimaryKeyParams(entity, ignore),
169
- tags: [entity.table],
170
- querystring: {
171
- type: 'object',
172
- properties: {
173
- fields: getFieldsForEntity(targetEntity, ignore),
174
- },
175
- },
176
- response: {
177
- 200: {
178
- type: 'array',
179
- items: targetEntitySchema,
178
+ app.get(
179
+ `/:${camelcase(primaryKey)}/${routePathName}`,
180
+ {
181
+ schema: {
182
+ operationId,
183
+ summary: `Get ${targetEntity.pluralName} for ${entity.singularName}.`,
184
+ description: `Fetch all the ${targetEntity.pluralName} for ${entity.singularName} from the database.`,
185
+ params: getPrimaryKeyParams(entity, ignore),
186
+ tags: [entity.table],
187
+ querystring: {
188
+ type: 'object',
189
+ properties: {
190
+ limit: {
191
+ type: 'integer',
192
+ description:
193
+ 'Limit will be applied by default if not passed. If the provided value exceeds the maximum allowed value a validation error will be thrown'
194
+ },
195
+ offset: { type: 'integer' },
196
+ fields: getFieldsForEntity(targetEntity, ignore),
197
+ totalCount: { type: 'boolean', default: false }
198
+ }
180
199
  },
200
+ response: {
201
+ 200: {
202
+ type: 'array',
203
+ items: targetEntitySchema
204
+ }
205
+ }
181
206
  },
207
+ links: {
208
+ 200: entityLinks
209
+ }
182
210
  },
183
- links: {
184
- 200: entityLinks,
185
- },
186
- }, async function (request, reply) {
187
- const ctx = { app: this, reply }
188
- // IF we want to have HTTP/404 in case the entity does not exist
189
- // we need to do 2 queries. One to check if the entity exists. the other to get the related entities
190
- // Improvement: this could be also done with a single query with a join,
191
-
192
- // check that the entity exists
193
- const resEntity = await entity.count({
194
- ctx,
195
- where: {
196
- [primaryKeyCamelcase]: {
197
- eq: request.params[primaryKeyCamelcase],
198
- },
199
- },
200
- })
201
- if (resEntity === 0) {
202
- return reply.callNotFound()
203
- }
204
-
205
- // get the related entities
206
- const res = await targetEntity.find({
207
- ctx,
208
- where: {
211
+ async function (request, reply) {
212
+ const { limit, offset, fields } = request.query
213
+ const ctx = { app: this, reply }
214
+ // IF we want to have HTTP/404 in case the entity does not exist
215
+ // we need to do 2 queries. One to check if the entity exists. the other to get the related entities
216
+ // Improvement: this could be also done with a single query with a join,
217
+
218
+ // check that the entity exists
219
+ const resEntity = await entity.count({
220
+ ctx,
221
+ where: {
222
+ [primaryKeyCamelcase]: {
223
+ eq: request.params[primaryKeyCamelcase]
224
+ }
225
+ }
226
+ })
227
+ if (resEntity === 0) {
228
+ return reply.callNotFound()
229
+ }
230
+
231
+ const where = {
209
232
  [targetForeignKeyCamelcase]: {
210
- eq: request.params[primaryKeyCamelcase],
211
- },
212
- },
213
- fields: request.query.fields,
214
-
215
- })
216
- if (res.length === 0) {
217
- // This is a query on a FK, so
218
- return []
233
+ eq: request.params[primaryKeyCamelcase]
234
+ }
235
+ }
236
+
237
+ // get the related entities
238
+ const res = await targetEntity.find({
239
+ ctx,
240
+ where,
241
+ fields,
242
+ limit,
243
+ offset
244
+ })
245
+
246
+ // X-Total-Count header
247
+ if (request.query.totalCount) {
248
+ let totalCount
249
+ if (((offset ?? 0) === 0 || res.length > 0) && limit !== undefined && res.length < limit) {
250
+ totalCount = (offset ?? 0) + res.length
251
+ } else {
252
+ totalCount = await targetEntity.count({ where, ctx })
253
+ }
254
+ reply.header('X-Total-Count', totalCount)
255
+ }
256
+
257
+ if (res.length === 0) {
258
+ // This is a query on a FK, so
259
+ return []
260
+ }
261
+ return res
219
262
  }
220
- return res
221
- })
263
+ )
222
264
  } catch (error) /* istanbul ignore next */ {
223
265
  app.log.error(error)
224
266
  app.log.info({ routePathName, targetEntityName, targetEntitySchema, operationId })
225
- throw new errors.UnableToCreateTheRouteForTheReverseRelationshipError()
267
+ throw new UnableToCreateTheRouteForTheReverseRelationshipError()
226
268
  }
227
269
  }
228
270
  }
@@ -246,7 +288,7 @@ async function entityPlugin (app, opts) {
246
288
  }
247
289
 
248
290
  const targetEntitySchema = {
249
- $ref: targetEntity.name + '#',
291
+ $ref: targetEntity.name + '#'
250
292
  }
251
293
  const entityLinks = getEntityLinksForEntity(app, targetEntity)
252
294
  // e.g. getMovieForQuote
@@ -260,62 +302,68 @@ async function entityPlugin (app, opts) {
260
302
 
261
303
  if (!ignoredReversedGETRoute) {
262
304
  try {
263
- app.get(`/:${camelcase(primaryKey)}/${targetRelation}`, {
264
- schema: {
265
- operationId,
266
- summary: `Get ${targetEntity.singularName} for ${entity.singularName}.`,
267
- description: `Fetch the ${targetEntity.singularName} for ${entity.singularName} from the database.`,
268
- params: getPrimaryKeyParams(entity, ignore),
269
- tags: [entity.table],
270
- querystring: {
271
- type: 'object',
272
- properties: {
273
- fields: getFieldsForEntity(targetEntity, ignore),
305
+ app.get(
306
+ `/:${camelcase(primaryKey)}/${targetRelation}`,
307
+ {
308
+ schema: {
309
+ operationId,
310
+ summary: `Get ${targetEntity.singularName} for ${entity.singularName}.`,
311
+ description: `Fetch the ${targetEntity.singularName} for ${entity.singularName} from the database.`,
312
+ params: getPrimaryKeyParams(entity, ignore),
313
+ tags: [entity.table],
314
+ querystring: {
315
+ type: 'object',
316
+ properties: {
317
+ fields: getFieldsForEntity(targetEntity, ignore)
318
+ }
274
319
  },
320
+ response: {
321
+ 200: targetEntitySchema
322
+ }
275
323
  },
276
- response: {
277
- 200: targetEntitySchema,
278
- },
279
- },
280
- links: {
281
- 200: entityLinks,
324
+ links: {
325
+ 200: entityLinks
326
+ }
282
327
  },
283
- }, async function (request, reply) {
284
- const ctx = { app: this, reply }
285
- // check that the entity exists
286
- const resEntity = (await entity.find({
287
- ctx,
288
- where: {
289
- [primaryKeyCamelcase]: {
290
- eq: request.params[primaryKeyCamelcase],
328
+ async function (request, reply) {
329
+ const ctx = { app: this, reply }
330
+ // check that the entity exists
331
+ const resEntity = (
332
+ await entity.find({
333
+ ctx,
334
+ where: {
335
+ [primaryKeyCamelcase]: {
336
+ eq: request.params[primaryKeyCamelcase]
337
+ }
338
+ }
339
+ })
340
+ )[0]
341
+
342
+ if (!resEntity) {
343
+ return reply.callNotFound()
344
+ }
345
+
346
+ // get the related entity
347
+ const res = await targetEntity.find({
348
+ ctx,
349
+ where: {
350
+ [targetForeignKeyCamelcase]: {
351
+ eq: resEntity[targetColumnCamelcase]
352
+ }
291
353
  },
292
- },
293
- }))[0]
294
-
295
- if (!resEntity) {
296
- return reply.callNotFound()
297
- }
298
-
299
- // get the related entity
300
- const res = await targetEntity.find({
301
- ctx,
302
- where: {
303
- [targetForeignKeyCamelcase]: {
304
- eq: resEntity[targetColumnCamelcase],
305
- },
306
- },
307
- fields: request.query.fields,
308
- })
354
+ fields: request.query.fields
355
+ })
309
356
 
310
- if (res.length === 0) {
311
- return reply.callNotFound()
357
+ if (res.length === 0) {
358
+ return reply.callNotFound()
359
+ }
360
+ return res[0]
312
361
  }
313
- return res[0]
314
- })
362
+ )
315
363
  } catch (error) /* istanbul ignore next */ {
316
364
  app.log.error(error)
317
365
  app.log.info({ primaryKey, targetRelation, targetEntitySchema, targetEntityName, targetEntity, operationId })
318
- throw new errors.UnableToCreateTheRouteForThePKColRelationshipError()
366
+ throw new UnableToCreateTheRouteForThePKColRelationshipError()
319
367
  }
320
368
  }
321
369
  }
@@ -337,15 +385,15 @@ async function entityPlugin (app, opts) {
337
385
  querystring: {
338
386
  type: 'object',
339
387
  properties: {
340
- fields,
341
- },
388
+ fields
389
+ }
342
390
  },
343
391
  response: {
344
- 200: entitySchema,
345
- },
392
+ 200: entitySchema
393
+ }
346
394
  },
347
395
  links: {
348
- 200: entityLinks,
396
+ 200: entityLinks
349
397
  },
350
398
  async handler (request, reply) {
351
399
  const id = request.params[primaryKeyCamelcase]
@@ -354,18 +402,18 @@ async function entityPlugin (app, opts) {
354
402
  ctx,
355
403
  input: {
356
404
  ...request.body,
357
- [primaryKeyCamelcase]: id,
405
+ [primaryKeyCamelcase]: id
358
406
  },
359
407
  where: {
360
408
  [primaryKeyCamelcase]: {
361
- eq: id,
362
- },
409
+ eq: id
410
+ }
363
411
  },
364
- fields: request.query.fields,
412
+ fields: request.query.fields
365
413
  })
366
414
  reply.header('location', `${app.prefix}/${res[primaryKeyCamelcase]}`)
367
415
  return res
368
- },
416
+ }
369
417
  })
370
418
  }
371
419
 
@@ -373,39 +421,43 @@ async function entityPlugin (app, opts) {
373
421
  return ignoreRoute.path === openapiPath && ignoreRoute.method === 'DELETE'
374
422
  })
375
423
  if (!ignoredDELETERoute) {
376
- app.delete(`/:${primaryKeyCamelcase}`, {
377
- schema: {
378
- operationId: 'delete' + capitalize(entity.pluralName),
379
- summary: `Delete ${entity.pluralName}.`,
380
- description: `Delete one or more ${entity.pluralName} from the Database.`,
381
- params: primaryKeyParams,
382
- tags: [entity.table],
383
- querystring: {
384
- type: 'object',
385
- properties: {
386
- fields,
424
+ app.delete(
425
+ `/:${primaryKeyCamelcase}`,
426
+ {
427
+ schema: {
428
+ operationId: 'delete' + capitalize(entity.pluralName),
429
+ summary: `Delete ${entity.pluralName}.`,
430
+ description: `Delete one or more ${entity.pluralName} from the Database.`,
431
+ params: primaryKeyParams,
432
+ tags: [entity.table],
433
+ querystring: {
434
+ type: 'object',
435
+ properties: {
436
+ fields
437
+ }
387
438
  },
388
- },
389
- response: {
390
- 200: entitySchema,
391
- },
439
+ response: {
440
+ 200: entitySchema
441
+ }
442
+ }
392
443
  },
393
- }, async function (request, reply) {
394
- const ctx = { app: this, reply }
395
- const res = await entity.delete({
396
- ctx,
397
- where: {
398
- [primaryKeyCamelcase]: {
399
- eq: request.params[primaryKeyCamelcase],
444
+ async function (request, reply) {
445
+ const ctx = { app: this, reply }
446
+ const res = await entity.delete({
447
+ ctx,
448
+ where: {
449
+ [primaryKeyCamelcase]: {
450
+ eq: request.params[primaryKeyCamelcase]
451
+ }
400
452
  },
401
- },
402
- fields: request.query.fields,
403
- })
404
- if (res.length === 0) {
405
- return reply.callNotFound()
453
+ fields: request.query.fields
454
+ })
455
+ if (res.length === 0) {
456
+ return reply.callNotFound()
457
+ }
458
+ return res[0]
406
459
  }
407
- return res[0]
408
- })
460
+ )
409
461
  }
410
462
  }
411
463
 
@@ -414,13 +466,11 @@ function getPrimaryKeyParams (entity, ignore) {
414
466
  const fields = entity.fields
415
467
  const field = fields[primaryKey]
416
468
  const properties = {
417
- [field.camelcase]: { type: mapSQLTypeToOpenAPIType(field.sqlType, ignore) },
469
+ [field.camelcase]: { type: mapSQLTypeToOpenAPIType(field.sqlType, ignore) }
418
470
  }
419
471
 
420
472
  return {
421
473
  type: 'object',
422
- properties,
474
+ properties
423
475
  }
424
476
  }
425
-
426
- module.exports = entityPlugin
package/lib/errors.js CHANGED
@@ -1,10 +1,27 @@
1
- 'use strict'
1
+ import createError from '@fastify/error'
2
2
 
3
- const createError = require('@fastify/error')
3
+ export const ERROR_PREFIX = 'PLT_SQL_OPENAPI'
4
4
 
5
- const ERROR_PREFIX = 'PLT_SQL_OPENAPI'
6
-
7
- module.exports = {
8
- UnableToCreateTheRouteForTheReverseRelationshipError: createError(`${ERROR_PREFIX}_UNABLE_CREATE_ROUTE_FOR_REVERSE_RELATIONSHIP`, 'Unable to create the route for the reverse relationship'),
9
- UnableToCreateTheRouteForThePKColRelationshipError: createError(`${ERROR_PREFIX}_UNABLE_CREATE_ROUTE_FOR_PK_COL_RELATIONSHIP`, 'Unable to create the route for the PK col relationship'),
10
- }
5
+ export const UnableToCreateTheRouteForTheReverseRelationshipError = createError(
6
+ `${ERROR_PREFIX}_UNABLE_CREATE_ROUTE_FOR_REVERSE_RELATIONSHIP`,
7
+ 'Unable to create the route for the reverse relationship'
8
+ )
9
+ export const UnableToCreateTheRouteForThePKColRelationshipError = createError(
10
+ `${ERROR_PREFIX}_UNABLE_CREATE_ROUTE_FOR_PK_COL_RELATIONSHIP`,
11
+ 'Unable to create the route for the PK col relationship'
12
+ )
13
+ export const UnableToParseCursorStrError = createError(
14
+ `${ERROR_PREFIX}_UNABLE_TO_PARSE_CURSOR_STR`,
15
+ 'Unable to parse cursor string. Make sure to provide valid encoding of cursor object. Error: %s',
16
+ 400
17
+ )
18
+ export const CursorValidationError = createError(
19
+ `${ERROR_PREFIX}_CURSOR_VALIDATION_ERROR`,
20
+ 'Cursor validation error. %s',
21
+ 400
22
+ )
23
+ export const PrimaryKeyNotIncludedInOrderByInCursorPaginationError = createError(
24
+ `${ERROR_PREFIX}_PRIMARY_KEY_NOT_INCLUDED_IN_ORDER_BY_IN_CURSOR_PAGINATION`,
25
+ 'At least one primary key must be included in orderBy clause in case of cursor pagination',
26
+ 400
27
+ )