@crombolo/contracts 0.0.0-stage → 0.10.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.
package/package.json CHANGED
@@ -1,6 +1,33 @@
1
1
  {
2
2
  "name": "@crombolo/contracts",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.10.0",
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
@@ -0,0 +1,2 @@
1
+ export * from './spec.ts'
2
+ export type * from './types.ts'
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
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.