@platformatic/sql-openapi 3.4.1 → 3.5.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @platformatic/sql-openapi
2
2
 
3
- Check out the full documentation on [our website](https://docs.platformatic.dev/docs/packages/sql-openapi/overview).
3
+ Check out the full documentation on [our website](https://docs.platformatic.dev/docs/reference/sql-openapi/overview).
4
4
 
5
5
  ## Install
6
6
 
package/eslint.config.js CHANGED
@@ -1,3 +1,3 @@
1
- 'use strict'
1
+ import neostandard from 'neostandard'
2
2
 
3
- module.exports = require('neostandard')({ ts: true })
3
+ export default neostandard({ ts: true })
package/index.d.ts CHANGED
@@ -27,4 +27,7 @@ export default plugin
27
27
  export module errors {
28
28
  export const UnableToCreateTheRouteForTheReverseRelationshipError: () => FastifyError
29
29
  export const UnableToCreateTheRouteForThePKColRelationshipError: () => FastifyError
30
+ export const UnableToParseCursorStrError: () => FastifyError
31
+ export const CursorValidationError: () => FastifyError
32
+ export const PrimaryKeyNotIncludedInOrderByInCursorPaginationError: () => FastifyError
30
33
  }
package/index.js CHANGED
@@ -1,39 +1,38 @@
1
- 'use strict'
2
-
3
- const Swagger = require('@fastify/swagger')
4
- const ScalarApiReference = require('@scalar/fastify-api-reference')
5
- const deepmerge = require('@fastify/deepmerge')({ all: true })
6
- const { mapSQLEntityToJSONSchema } = require('@platformatic/sql-json-schema-mapper')
7
- const { findNearestString } = require('@platformatic/utils')
8
- const entityPlugin = require('./lib/entity-to-routes')
9
- const manyToMany = require('./lib/many-to-many')
10
- const { getSchemaOverrideFromOpenApiPathItem } = require('./lib/utils')
11
- const fp = require('fastify-plugin')
12
- const errors = require('./lib/errors')
1
+ import Swagger from '@fastify/swagger'
2
+ import { deepmerge, findNearestString } from '@platformatic/foundation'
3
+ import { mapSQLEntityToJSONSchema } from '@platformatic/sql-json-schema-mapper'
4
+ import fp from 'fastify-plugin'
5
+ import { entityPlugin } from './lib/entity-to-routes.js'
6
+ import { manyToMany } from './lib/many-to-many.js'
7
+ import { getSchemaOverrideFromOpenApiPathItem } from './lib/utils.js'
13
8
 
14
9
  async function setupOpenAPI (app, opts) {
15
10
  const prefix = opts.prefix || ''
16
- const openapiConfig = deepmerge({
17
- exposeRoute: true,
18
- info: {
19
- title: 'Platformatic DB',
20
- description: 'Exposing a SQL database as REST',
21
- version: '1.0.0',
11
+ const openapiConfig = deepmerge(
12
+ {
13
+ exposeRoute: true,
14
+ info: {
15
+ title: 'Platformatic DB',
16
+ description: 'Exposing a SQL database as REST',
17
+ version: '1.0.0'
18
+ },
19
+ servers: [{ url: globalThis.platformatic?.runtimeBasePath ?? '/' }]
22
20
  },
23
- }, opts)
21
+ opts
22
+ )
24
23
  app.log.trace({ openapi: openapiConfig })
25
24
  await app.register(Swagger, {
26
25
  exposeRoute: openapiConfig.exposeRoute,
27
26
  openapi: {
28
- ...openapiConfig,
27
+ ...openapiConfig
29
28
  },
30
29
  refResolver: {
31
30
  buildLocalReference (json, baseUri, fragment, i) {
32
31
  // TODO figure out if we need def-${i}
33
32
  /* istanbul ignore next */
34
33
  return json.$id || `def-${i}`
35
- },
36
- },
34
+ }
35
+ }
37
36
  })
38
37
 
39
38
  const ignore = opts.ignore || []
@@ -42,26 +41,27 @@ async function setupOpenAPI (app, opts) {
42
41
  const paths = opts.paths || {}
43
42
 
44
43
  const { default: scalarTheme } = await import('@platformatic/scalar-theme')
44
+ const { default: scalarApiReference } = await import('@scalar/fastify-api-reference')
45
45
  const routePrefix = opts.swaggerPrefix || '/documentation'
46
46
 
47
47
  app.get(`${routePrefix}/json`, { schema: { hide: true }, logLevel: 'warn' }, async () => app.swagger())
48
48
  app.get(`${routePrefix}/yaml`, { schema: { hide: true }, logLevel: 'warn' }, async () => app.swagger({ yaml: true }))
49
49
 
50
- app.register(ScalarApiReference, {
50
+ app.register(scalarApiReference, {
51
51
  ...opts,
52
52
  logLevel: 'warn',
53
53
  prefix: undefined,
54
54
  routePrefix,
55
55
  configuration: {
56
- customCss: scalarTheme.theme,
57
- },
56
+ customCss: scalarTheme.theme
57
+ }
58
58
  })
59
59
 
60
- app.addHook('onRoute', (routeOptions) => {
60
+ app.addHook('onRoute', routeOptions => {
61
61
  if (paths[routeOptions.url]) {
62
62
  routeOptions.schema = {
63
63
  ...routeOptions.schema,
64
- ...getSchemaOverrideFromOpenApiPathItem(paths[routeOptions.url], routeOptions.method),
64
+ ...getSchemaOverrideFromOpenApiPathItem(paths[routeOptions.url], routeOptions.method)
65
65
  }
66
66
  }
67
67
  })
@@ -97,7 +97,7 @@ async function setupOpenAPI (app, opts) {
97
97
  const targetEntity = app.platformatic.entities[targetEntityName]
98
98
  const reverseRelationship = {
99
99
  sourceEntity: relation.entityName,
100
- relation,
100
+ relation
101
101
  }
102
102
  /* istanbul ignore next */
103
103
  targetEntity.reverseRelationships = targetEntity.reverseRelationships || []
@@ -105,8 +105,7 @@ async function setupOpenAPI (app, opts) {
105
105
  }
106
106
  }
107
107
 
108
- const entitiesNames = Object.values(app.platformatic.entities)
109
- .map(entity => entity.singularName)
108
+ const entitiesNames = Object.values(app.platformatic.entities).map(entity => entity.singularName)
110
109
 
111
110
  for (const ignoredEntity of Object.keys(ignore)) {
112
111
  if (!entitiesNames.includes(ignoredEntity)) {
@@ -141,7 +140,7 @@ async function setupOpenAPI (app, opts) {
141
140
  entity,
142
141
  prefix: localPrefix,
143
142
  ignore: ignore[entity.singularName] || {},
144
- ignoreRoutes,
143
+ ignoreRoutes
145
144
  })
146
145
  } else {
147
146
  // TODO support ignore
@@ -149,11 +148,11 @@ async function setupOpenAPI (app, opts) {
149
148
  entity,
150
149
  prefix: localPrefix,
151
150
  ignore,
152
- ignoreRoutes,
151
+ ignoreRoutes
153
152
  })
154
153
  }
155
154
  }
156
155
  }
157
156
 
158
- module.exports = fp(setupOpenAPI)
159
- module.exports.errors = errors
157
+ export default fp(setupOpenAPI)
158
+ export * as errors from './lib/errors.js'
package/lib/cursor.js ADDED
@@ -0,0 +1,94 @@
1
+ import Ajv from 'ajv'
2
+ import camelCase from 'camelcase'
3
+ import fjs from 'fast-json-stringify'
4
+ import fastUri from 'fast-uri'
5
+ import sjson from 'secure-json-parse'
6
+ import {
7
+ CursorValidationError,
8
+ PrimaryKeyNotIncludedInOrderByInCursorPaginationError,
9
+ UnableToParseCursorStrError
10
+ } from './errors.js'
11
+
12
+ const ajvOptions = {
13
+ coerceTypes: 'array',
14
+ useDefaults: true,
15
+ removeAdditional: true,
16
+ uriResolver: fastUri,
17
+ addUsedSchema: false,
18
+ allErrors: false
19
+ }
20
+ const ajv = new Ajv(ajvOptions)
21
+
22
+ export function buildCursorUtils (app, entity) {
23
+ const entitySchema = app.getSchema(entity.name)
24
+ const cursorSchema = {
25
+ $id: entity.name + 'Cursor',
26
+ title: entitySchema.title + ' Cursor',
27
+ description: entitySchema.description + ' cursor',
28
+ type: 'object',
29
+ properties: entitySchema.properties,
30
+ additionalProperties: false
31
+ }
32
+ const validateCursor = ajv.compile(cursorSchema)
33
+ const stringifyCursor = fjs(cursorSchema, {
34
+ ajv: ajvOptions
35
+ })
36
+
37
+ function encodeCursor (cursor) {
38
+ return Buffer.from(stringifyCursor(cursor)).toString('base64')
39
+ }
40
+
41
+ function decodeCursor (cursorBase64) {
42
+ const cursorString = Buffer.from(cursorBase64, 'base64').toString()
43
+ let parsedCursor
44
+ try {
45
+ parsedCursor = sjson.parse(cursorString)
46
+ } catch (error) {
47
+ throw new UnableToParseCursorStrError(error.message)
48
+ }
49
+ if (!validateCursor(parsedCursor)) {
50
+ const error = validateCursor.errors[0]
51
+ throw new CursorValidationError(`${error.instancePath} ${error.message}`)
52
+ }
53
+ return parsedCursor
54
+ }
55
+
56
+ function transformQueryToCursor ({ startAfter, endBefore }) {
57
+ const parsedData = {
58
+ nextPage: true,
59
+ cursor: null
60
+ }
61
+ if (startAfter) {
62
+ parsedData.cursor = decodeCursor(startAfter)
63
+ } else if (endBefore) {
64
+ parsedData.cursor = decodeCursor(endBefore)
65
+ parsedData.nextPage = false
66
+ }
67
+ return parsedData
68
+ }
69
+
70
+ function buildCursorHeaders ({ findResult, orderBy, primaryKeys }) {
71
+ const firstItem = findResult.at(0)
72
+ const lastItem = findResult.at(-1)
73
+ const firstItemCursor = {}
74
+ const lastItemCursor = {}
75
+ const camelCasedPrimaryKeys = Array.from(primaryKeys).map(key => camelCase(key))
76
+ let hasPrimaryKey = false
77
+ for (const { field } of orderBy) {
78
+ if (firstItem[field] === undefined) continue
79
+ firstItemCursor[field] = firstItem[field]
80
+ lastItemCursor[field] = lastItem[field]
81
+ if (camelCasedPrimaryKeys.includes(field)) hasPrimaryKey = true
82
+ }
83
+ if (!hasPrimaryKey) throw new PrimaryKeyNotIncludedInOrderByInCursorPaginationError()
84
+ return {
85
+ endBefore: encodeCursor(firstItemCursor),
86
+ startAfter: encodeCursor(lastItemCursor)
87
+ }
88
+ }
89
+
90
+ return {
91
+ transformQueryToCursor,
92
+ buildCursorHeaders
93
+ }
94
+ }