@crombolo/contracts 0.0.0-stage → 0.9.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/LICENSE +21 -0
- package/NOTICE +69 -0
- package/OpenApi.json +54597 -0
- package/generated/management-api.d.ts +26515 -0
- package/package.json +31 -4
- package/src/index.ts +2 -0
- package/src/spec.ts +92 -0
- package/src/types.ts +37 -0
- package/README.md +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crombolo/contracts",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.9.1",
|
|
4
|
+
"description": "The Umbraco Management API OpenAPI document and the TypeScript types generated from it.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Crombolo",
|
|
7
|
+
"homepage": "https://github.com/kineco-au/crombolo-cms#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/kineco-au/crombolo-cms.git",
|
|
11
|
+
"directory": "packages/contracts"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/kineco-au/crombolo-cms/issues"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"bun": ">=1.3.0"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./src/index.ts",
|
|
22
|
+
"./OpenApi.json": "./OpenApi.json"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"OpenApi.json",
|
|
27
|
+
"generated",
|
|
28
|
+
"NOTICE"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/index.ts
ADDED
package/src/spec.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Loads and indexes the vendored Umbraco Management API OpenAPI contract. */
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
export const SPEC_PATH = join(import.meta.dir, '../OpenApi.json')
|
|
6
|
+
|
|
7
|
+
export const HTTP_METHODS = ['get', 'post', 'put', 'delete', 'patch'] as const
|
|
8
|
+
export type HttpMethod = (typeof HTTP_METHODS)[number]
|
|
9
|
+
|
|
10
|
+
export interface OperationInfo {
|
|
11
|
+
operationId: string
|
|
12
|
+
method: HttpMethod
|
|
13
|
+
/** Full path as written in the spec, e.g. /umbraco/management/api/v1/document/{id} */
|
|
14
|
+
path: string
|
|
15
|
+
/** Version segment parsed out of the path, e.g. "1" or "1.1" */
|
|
16
|
+
version: string | undefined
|
|
17
|
+
/** First tag, used to group operations by area. */
|
|
18
|
+
tag: string | undefined
|
|
19
|
+
/** Path segment after the version, e.g. "document" */
|
|
20
|
+
area: string | undefined
|
|
21
|
+
/**
|
|
22
|
+
* True when the operation overrides the document's global security with an
|
|
23
|
+
* empty requirement. Exactly 10 operations do this in Umbraco 18.
|
|
24
|
+
*/
|
|
25
|
+
anonymous: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface OpenApiSpec {
|
|
29
|
+
openapi: string
|
|
30
|
+
info: { title: string; version: string }
|
|
31
|
+
security?: unknown[]
|
|
32
|
+
paths: Record<
|
|
33
|
+
string,
|
|
34
|
+
Record<string, { operationId?: string; tags?: string[]; security?: unknown[] }>
|
|
35
|
+
>
|
|
36
|
+
components: { schemas: Record<string, unknown> }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let cached: OpenApiSpec | undefined
|
|
40
|
+
|
|
41
|
+
export function loadSpec(): OpenApiSpec {
|
|
42
|
+
if (!cached) cached = JSON.parse(readFileSync(SPEC_PATH, 'utf8')) as OpenApiSpec
|
|
43
|
+
return cached
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function loadSpecAsync(): Promise<OpenApiSpec> {
|
|
47
|
+
if (!cached) cached = (await Bun.file(SPEC_PATH).json()) as OpenApiSpec
|
|
48
|
+
return cached
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const VERSION_RE = /\/api\/v(\d+(?:\.\d+)?)\//
|
|
52
|
+
|
|
53
|
+
export function parsePath(path: string): { version?: string; area?: string } {
|
|
54
|
+
const m = VERSION_RE.exec(path)
|
|
55
|
+
if (!m) return {}
|
|
56
|
+
const version = m[1]
|
|
57
|
+
const rest = path.slice(m.index + m[0].length)
|
|
58
|
+
const area = rest.split('/')[0] || undefined
|
|
59
|
+
return { version, area }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function listOperations(spec: OpenApiSpec): OperationInfo[] {
|
|
63
|
+
const out: OperationInfo[] = []
|
|
64
|
+
for (const [path, item] of Object.entries(spec.paths)) {
|
|
65
|
+
const { version, area } = parsePath(path)
|
|
66
|
+
for (const method of HTTP_METHODS) {
|
|
67
|
+
const op = item[method]
|
|
68
|
+
if (!op?.operationId) continue
|
|
69
|
+
out.push({
|
|
70
|
+
operationId: op.operationId,
|
|
71
|
+
method,
|
|
72
|
+
path,
|
|
73
|
+
version,
|
|
74
|
+
tag: op.tags?.[0],
|
|
75
|
+
area,
|
|
76
|
+
anonymous: Array.isArray(op.security) && op.security.length === 0,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Operation ids must be unique — the generated client keys methods on them. */
|
|
84
|
+
export function findDuplicateOperationIds(ops: OperationInfo[]): string[] {
|
|
85
|
+
const seen = new Set<string>()
|
|
86
|
+
const dupes = new Set<string>()
|
|
87
|
+
for (const op of ops) {
|
|
88
|
+
if (seen.has(op.operationId)) dupes.add(op.operationId)
|
|
89
|
+
seen.add(op.operationId)
|
|
90
|
+
}
|
|
91
|
+
return [...dupes]
|
|
92
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed access to the generated contract types.
|
|
3
|
+
*
|
|
4
|
+
* `ResponseOf<'GetServerStatus'>` is the exact 200 body the backoffice expects,
|
|
5
|
+
* so a handler that drifts from the contract fails to compile. Regenerate with
|
|
6
|
+
* `bun run generate:types` after re-vendoring OpenApi.json.
|
|
7
|
+
*/
|
|
8
|
+
import type { components, operations } from '../generated/management-api.d.ts'
|
|
9
|
+
|
|
10
|
+
export type Schemas = components['schemas']
|
|
11
|
+
export type Operations = operations
|
|
12
|
+
export type OperationId = keyof operations
|
|
13
|
+
|
|
14
|
+
type JsonContent<T> = T extends { content: { 'application/json': infer B } } ? B : never
|
|
15
|
+
|
|
16
|
+
/** The 200/201 JSON response body for an operation, or never if it has none. */
|
|
17
|
+
export type ResponseOf<K extends OperationId> = operations[K] extends {
|
|
18
|
+
responses: infer R
|
|
19
|
+
}
|
|
20
|
+
? R extends { 200: infer Ok }
|
|
21
|
+
? JsonContent<Ok>
|
|
22
|
+
: R extends { 201: infer Created }
|
|
23
|
+
? JsonContent<Created>
|
|
24
|
+
: never
|
|
25
|
+
: never
|
|
26
|
+
|
|
27
|
+
/** The JSON request body for an operation, or never if it takes none. */
|
|
28
|
+
export type RequestOf<K extends OperationId> = operations[K] extends {
|
|
29
|
+
requestBody?: infer B
|
|
30
|
+
}
|
|
31
|
+
? JsonContent<NonNullable<B>>
|
|
32
|
+
: never
|
|
33
|
+
|
|
34
|
+
/** Query and path parameters declared for an operation. */
|
|
35
|
+
export type ParamsOf<K extends OperationId> = operations[K] extends { parameters: infer P }
|
|
36
|
+
? P
|
|
37
|
+
: never
|
package/README.md
DELETED