@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 +1 -1
- package/eslint.config.js +2 -2
- package/index.d.ts +3 -0
- package/index.js +33 -34
- package/lib/cursor.js +94 -0
- package/lib/entity-to-routes.js +252 -202
- package/lib/errors.js +25 -8
- package/lib/many-to-many.js +99 -81
- package/lib/shared.js +220 -135
- package/lib/utils.js +2 -8
- package/package.json +21 -18
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/
|
|
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
|
-
|
|
1
|
+
import neostandard from 'neostandard'
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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(
|
|
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',
|
|
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
|
-
|
|
159
|
-
|
|
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
|
+
}
|