@panneau/protocol 5.0.0-alpha.3 → 5.0.0-alpha.4

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.
Files changed (40) hide show
  1. package/dist/conformance/index.d.ts.map +1 -1
  2. package/dist/conformance/index.js +31 -1
  3. package/dist/conformance/index.js.map +1 -1
  4. package/dist/index.js +2 -2
  5. package/dist/lint.d.ts +2 -1
  6. package/dist/lint.d.ts.map +1 -1
  7. package/dist/lint.js +27 -1
  8. package/dist/lint.js.map +1 -1
  9. package/dist/mock/example.d.ts +2 -1
  10. package/dist/mock/example.d.ts.map +1 -1
  11. package/dist/mock/example.js +219 -143
  12. package/dist/mock/example.js.map +1 -1
  13. package/dist/mock/index.d.ts.map +1 -1
  14. package/dist/mock/index.js +145 -5
  15. package/dist/mock/index.js.map +1 -1
  16. package/dist/openapi.d.ts.map +1 -1
  17. package/dist/openapi.js +18 -94
  18. package/dist/openapi.js.map +1 -1
  19. package/dist/schemas/api.d.ts +113 -82
  20. package/dist/schemas/api.d.ts.map +1 -1
  21. package/dist/schemas/api.js +68 -26
  22. package/dist/schemas/api.js.map +1 -1
  23. package/dist/schemas/definition.d.ts +1 -0
  24. package/dist/schemas/definition.d.ts.map +1 -1
  25. package/dist/schemas/definition.js +2 -2
  26. package/dist/schemas/definition.js.map +1 -1
  27. package/dist/schemas/field.js +3 -3
  28. package/dist/schemas/field.js.map +1 -1
  29. package/dist/schemas/index.d.ts +106 -79
  30. package/dist/schemas/index.d.ts.map +1 -1
  31. package/dist/schemas/index.js +11 -5
  32. package/dist/schemas/index.js.map +1 -1
  33. package/dist/schemas/resource.d.ts +1 -0
  34. package/dist/schemas/resource.d.ts.map +1 -1
  35. package/dist/schemas/resource.js +1 -0
  36. package/dist/schemas/resource.js.map +1 -1
  37. package/package.json +1 -1
  38. package/schema/boot.schema.json +4 -0
  39. package/schema/definition.schema.json +4 -0
  40. package/schema/openapi.json +302 -465
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/mock/index.ts"],"sourcesContent":["import { type ListQuery, fromSearchParams, isFilterRange, matchesFilterRange } from '../query';\nimport { type RuleFailure, checkValues } from '../rules';\nimport type { DefinitionInput, ResourceInput } from '../schemas';\nimport type { ActionResult, Item, Problem, Selection } from '../schemas/api';\nimport { isRecord } from '../values';\nimport { PROBLEM_CONTENT_TYPE } from '../version';\n\ntype Values = Record<string, unknown>;\n\nexport interface MockBackendOptions {\n /** The definition served by `GET /definition`. Its `api.baseUrl` is the prefix of every route. */\n definition: DefinitionInput;\n /** Initial values of the items, per resource id. Items without an `id` get one. */\n items?: Record<string, Values[]>;\n}\n\nexport interface MockBackend {\n /** A `fetch`-compatible function that answers protocol requests. */\n fetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;\n /** The current items of a resource. */\n items: (resource: string) => Item[];\n}\n\nconst RESERVED_KEYS = new Set(['id', 'version', 'abilities']);\n\nconst DEFAULT_MESSAGES: Record<string, (failure: RuleFailure) => string> = {\n required: () => 'This field is required.',\n min: (failure) => `This value must be at least ${String(failure.value)}.`,\n max: (failure) => `This value must be at most ${String(failure.value)}.`,\n pattern: () => 'This value has an invalid format.',\n email: () => 'This value must be an email address.',\n url: () => 'This value must be a URL.',\n in: () => 'This value is not one of the choices.',\n accept: () => 'This file type is not accepted.',\n maxSize: () => 'This file is too large.',\n before: () => 'This date is too late.',\n after: () => 'This date is too early.',\n};\n\nfunction message(failure: RuleFailure): string {\n return failure.message ?? DEFAULT_MESSAGES[failure.rule]?.(failure) ?? 'This value is invalid.';\n}\n\nfunction json(status: number, body: unknown, headers: Record<string, string> = {}): Response {\n return new Response(status === 204 ? null : JSON.stringify(body), {\n status,\n headers: status === 204 ? headers : { 'Content-Type': 'application/json', ...headers },\n });\n}\n\nfunction problem(status: number, title: string, extra: Partial<Problem> = {}): Response {\n const body: Problem = { type: 'about:blank', title, status, ...extra };\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': PROBLEM_CONTENT_TYPE },\n });\n}\n\nfunction etag(item: Item): Record<string, string> {\n return item.version === null ? {} : { ETag: `\"${item.version}\"` };\n}\n\nfunction parseEtag(value: string): string {\n return value.replace(/^W\\//, '').replace(/^\"(.*)\"$/, '$1');\n}\n\nfunction searchableText(value: unknown): string {\n if (typeof value === 'string') {\n return value;\n }\n if (isRecord(value)) {\n return Object.values(value).map(searchableText).join(' ');\n }\n return '';\n}\n\nfunction compare(a: unknown, b: unknown): number {\n const left = isRecord(a) ? searchableText(a) : a;\n const right = isRecord(b) ? searchableText(b) : b;\n if (left === right) {\n return 0;\n }\n if (left === undefined || left === null) {\n return 1;\n }\n if (right === undefined || right === null) {\n return -1;\n }\n return String(left).localeCompare(String(right), undefined, { numeric: true });\n}\n\nfunction applyQuery(items: Item[], query: Pick<ListQuery, 'filter' | 'q' | 'sort'>): Item[] {\n let result = items;\n Object.entries(query.filter ?? {}).forEach(([key, expected]) => {\n if (isFilterRange(expected)) {\n result = result.filter((item) => matchesFilterRange(item[key], expected));\n return;\n }\n const accepted = (Array.isArray(expected) ? expected : [expected]).map(String);\n result = result.filter((item) => {\n const value = item[key];\n const values = Array.isArray(value) ? value : [value];\n return values.some((candidate) => accepted.includes(String(candidate)));\n });\n });\n if (query.q !== undefined && query.q !== '') {\n const terms = query.q.toLowerCase();\n result = result.filter((item) =>\n Object.entries(item)\n .filter(([key]) => !RESERVED_KEYS.has(key))\n .some(([, value]) => searchableText(value).toLowerCase().includes(terms)),\n );\n }\n const sort = query.sort ?? [];\n if (sort.length > 0) {\n result = [...result].sort((a, b) => {\n for (const part of sort) {\n const descending = part.startsWith('-');\n const key = descending ? part.slice(1) : part;\n const order = compare(a[key], b[key]);\n if (order !== 0) {\n return descending ? -order : order;\n }\n }\n return 0;\n });\n }\n return result;\n}\n\n/**\n * An in-memory backend that implements the protocol's resource routes: definition, list, show,\n * create, update (with `If-Match`), delete and the built-in `delete` action. Values are checked\n * with the portable rules. Used to test the conformance kit, and to develop frontends without a\n * real backend.\n */\nexport function createMockBackend({ definition, items = {} }: MockBackendOptions): MockBackend {\n const store = new Map<string, Item[]>();\n let nextId = 1;\n\n const abilitiesFor = (resource: ResourceInput) =>\n Object.fromEntries([\n ['update', true],\n ['delete', true],\n ...(resource.actions ?? []).map((action) => [action.id, true]),\n ]);\n\n const makeItem = (resource: ResourceInput, values: Values): Item => {\n const id =\n typeof values.id === 'string' || typeof values.id === 'number'\n ? String(values.id)\n : String(nextId++);\n const rest = Object.fromEntries(\n Object.entries(values).filter(([key]) => !RESERVED_KEYS.has(key)),\n );\n return { ...rest, id, version: '1', abilities: abilitiesFor(resource) };\n };\n\n definition.resources.forEach((resource) => {\n store.set(\n resource.id,\n (items[resource.id] ?? []).map((values) => makeItem(resource, values)),\n );\n });\n\n const basePath = new URL(definition.api.baseUrl, 'http://panneau.test').pathname.replace(\n /\\/$/,\n '',\n );\n\n const validationProblem = (\n resource: ResourceInput,\n values: Values,\n mode: 'create' | 'update',\n ) => {\n const failures = checkValues(resource.fields, values, {\n mode,\n partial: mode === 'update',\n locales: definition.locales,\n });\n const paths = Object.keys(failures);\n if (paths.length === 0) {\n return null;\n }\n return problem(422, 'The given data was invalid.', {\n type: 'urn:panneau:problem:validation',\n errors: Object.fromEntries(\n paths.map((path) => [path, (failures[path] ?? []).map(message)]),\n ),\n });\n };\n\n const selectItems = (list: Item[], selection: Selection | undefined): Item[] => {\n if (selection === undefined) {\n return [];\n }\n if ('ids' in selection) {\n const ids = selection.ids as string[];\n return list.filter((item) => ids.includes(item.id));\n }\n const except = selection.except ?? [];\n const { filter, q } = selection.query;\n const query: Pick<ListQuery, 'filter' | 'q'> = {};\n if (filter !== undefined) {\n query.filter = filter as NonNullable<ListQuery['filter']>;\n }\n if (q !== undefined) {\n query.q = q;\n }\n return applyQuery(list, query).filter((item) => !except.includes(item.id));\n };\n\n const handle = async (request: Request): Promise<Response> => {\n const url = new URL(request.url, 'http://panneau.test');\n if (!url.pathname.startsWith(basePath)) {\n return problem(404, 'Not found.');\n }\n const segments = url.pathname\n .slice(basePath.length)\n .split('/')\n .filter((segment) => segment !== '')\n .map(decodeURIComponent);\n const method = request.method.toUpperCase();\n const readBody = async (): Promise<Values> => {\n const body: unknown = await request.json().catch(() => null);\n return isRecord(body) ? body : {};\n };\n\n if (segments.length === 1 && segments[0] === 'definition' && method === 'GET') {\n return json(200, definition);\n }\n if (segments[0] !== 'resources' || segments[1] === undefined) {\n return problem(404, 'Not found.');\n }\n\n const resource = definition.resources.find((candidate) => candidate.id === segments[1]);\n const list = resource !== undefined ? store.get(resource.id) : undefined;\n if (resource === undefined || list === undefined) {\n return problem(404, 'Resource not found.');\n }\n\n if (segments.length === 2) {\n if (method === 'GET') {\n const query = fromSearchParams(url.searchParams);\n const perPage = query.perPage ?? resource.list?.perPage ?? 20;\n const page = query.page ?? 1;\n const sort = query.sort ?? resource.list?.defaultSort?.split(',');\n const matching = applyQuery(list, {\n ...query,\n ...(sort !== undefined ? { sort } : {}),\n });\n return json(200, {\n data: matching.slice((page - 1) * perPage, page * perPage),\n meta: {\n page,\n perPage,\n total: matching.length,\n lastPage: Math.max(1, Math.ceil(matching.length / perPage)),\n },\n });\n }\n if (method === 'POST') {\n const values = await readBody();\n const invalid = validationProblem(resource, values, 'create');\n if (invalid !== null) {\n return invalid;\n }\n const item = makeItem(resource, { ...values, id: undefined });\n list.push(item);\n return json(201, { data: item }, etag(item));\n }\n return problem(405, 'Method not allowed.');\n }\n\n if (segments[2] === 'actions' && segments[3] !== undefined && segments.length === 4) {\n if (method !== 'POST') {\n return problem(405, 'Method not allowed.');\n }\n const actionId = segments[3];\n const action = (resource.actions ?? []).find((candidate) => candidate.id === actionId);\n const body = await readBody();\n if (body.dryRun === true) {\n // A dry run counts what the action would do, and changes nothing.\n if (action?.dryRun !== true) {\n return problem(400, 'This action does not support dry runs.');\n }\n const selected = selectItems(list, body.selection as Selection | undefined);\n const allowed = selected.filter((item) => item.abilities?.[actionId] !== false);\n const result: ActionResult = {\n summary: {\n succeeded: allowed.length,\n failed: 0,\n skipped: selected.length - allowed.length,\n },\n results: selected\n .filter((item) => !allowed.includes(item))\n .slice(0, 20)\n .map((item) => ({ id: item.id, status: 'skipped', reason: 'forbidden' })),\n };\n return json(200, result);\n }\n if (actionId !== 'delete') {\n return problem(404, 'Action not found.');\n }\n const selected = selectItems(list, body.selection as Selection | undefined);\n store.set(\n resource.id,\n list.filter((item) => !selected.includes(item)),\n );\n const result: ActionResult = {\n message: `${selected.length} deleted.`,\n summary: { succeeded: selected.length, failed: 0, skipped: 0 },\n refresh: 'list',\n };\n return json(200, result);\n }\n\n if (segments.length !== 3 || segments[2] === undefined) {\n return problem(404, 'Not found.');\n }\n const id = segments[2];\n const index = list.findIndex((item) => item.id === id);\n const current = list[index];\n if (current === undefined) {\n return problem(404, 'Item not found.');\n }\n\n if (method === 'GET') {\n return json(200, { data: current }, etag(current));\n }\n if (method === 'PATCH') {\n const ifMatch = request.headers.get('If-Match');\n if (\n ifMatch !== null &&\n current.version !== null &&\n parseEtag(ifMatch) !== current.version\n ) {\n return problem(412, 'The item was changed by someone else.', {\n type: 'urn:panneau:problem:version-conflict',\n current,\n });\n }\n const values = await readBody();\n const invalid = validationProblem(resource, values, 'update');\n if (invalid !== null) {\n return invalid;\n }\n const updates = Object.fromEntries(\n Object.entries(values).filter(([key]) => !RESERVED_KEYS.has(key)),\n );\n const updated: Item = {\n ...current,\n ...updates,\n version: current.version === null ? null : String(Number(current.version) + 1),\n };\n list[index] = updated;\n return json(200, { data: updated }, etag(updated));\n }\n if (method === 'DELETE') {\n list.splice(index, 1);\n return json(204, null);\n }\n return problem(405, 'Method not allowed.');\n };\n\n return {\n fetch: (input, init) => {\n if (input instanceof Request) {\n return handle(input);\n }\n // Relative URLs are accepted, as a browser would resolve them.\n return handle(new Request(new URL(input, 'http://panneau.test'), init));\n },\n items: (resource) => [...(store.get(resource) ?? [])],\n };\n}\n\nexport { createExampleDefinition } from './example';\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/mock/index.ts"],"sourcesContent":["import { type ListQuery, fromSearchParams, isFilterRange, matchesFilterRange } from '../query';\nimport { type RuleFailure, checkValues } from '../rules';\nimport type { DefinitionInput, ResourceInput } from '../schemas';\nimport type {\n ActionResult,\n Item,\n MediaInfo,\n MediaReference,\n Problem,\n Selection,\n StoredFile,\n Upload,\n UploadSignResponse,\n} from '../schemas/api';\nimport { isRecord } from '../values';\nimport { PROBLEM_CONTENT_TYPE } from '../version';\n\ntype Values = Record<string, unknown>;\n\nexport interface MockBackendOptions {\n /** The definition served by `GET /definition`. Its `api.baseUrl` is the prefix of every route. */\n definition: DefinitionInput;\n /** Initial values of the items, per resource id. Items without an `id` get one. */\n items?: Record<string, Values[]>;\n}\n\nexport interface MockBackend {\n /** A `fetch`-compatible function that answers protocol requests. */\n fetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;\n /** The current items of a resource. */\n items: (resource: string) => Item[];\n}\n\nconst RESERVED_KEYS = new Set(['id', 'version', 'abilities']);\n\nconst DEFAULT_MESSAGES: Record<string, (failure: RuleFailure) => string> = {\n required: () => 'This field is required.',\n min: (failure) => `This value must be at least ${String(failure.value)}.`,\n max: (failure) => `This value must be at most ${String(failure.value)}.`,\n pattern: () => 'This value has an invalid format.',\n email: () => 'This value must be an email address.',\n url: () => 'This value must be a URL.',\n in: () => 'This value is not one of the choices.',\n accept: () => 'This file type is not accepted.',\n maxSize: () => 'This file is too large.',\n before: () => 'This date is too late.',\n after: () => 'This date is too early.',\n};\n\nfunction message(failure: RuleFailure): string {\n return failure.message ?? DEFAULT_MESSAGES[failure.rule]?.(failure) ?? 'This value is invalid.';\n}\n\nfunction json(status: number, body: unknown, headers: Record<string, string> = {}): Response {\n return new Response(status === 204 ? null : JSON.stringify(body), {\n status,\n headers: status === 204 ? headers : { 'Content-Type': 'application/json', ...headers },\n });\n}\n\nfunction problem(status: number, title: string, extra: Partial<Problem> = {}): Response {\n const body: Problem = { type: 'about:blank', title, status, ...extra };\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': PROBLEM_CONTENT_TYPE },\n });\n}\n\nfunction etag(item: Item): Record<string, string> {\n return item.version === null ? {} : { ETag: `\"${item.version}\"` };\n}\n\nfunction parseEtag(value: string): string {\n return value.replace(/^W\\//, '').replace(/^\"(.*)\"$/, '$1');\n}\n\nfunction searchableText(value: unknown): string {\n if (typeof value === 'string') {\n return value;\n }\n if (isRecord(value)) {\n return Object.values(value).map(searchableText).join(' ');\n }\n return '';\n}\n\nfunction compare(a: unknown, b: unknown): number {\n const left = isRecord(a) ? searchableText(a) : a;\n const right = isRecord(b) ? searchableText(b) : b;\n if (left === right) {\n return 0;\n }\n if (left === undefined || left === null) {\n return 1;\n }\n if (right === undefined || right === null) {\n return -1;\n }\n return String(left).localeCompare(String(right), undefined, { numeric: true });\n}\n\nfunction applyQuery(items: Item[], query: Pick<ListQuery, 'filter' | 'q' | 'sort'>): Item[] {\n let result = items;\n Object.entries(query.filter ?? {}).forEach(([key, expected]) => {\n if (isFilterRange(expected)) {\n result = result.filter((item) => matchesFilterRange(item[key], expected));\n return;\n }\n const accepted = (Array.isArray(expected) ? expected : [expected]).map(String);\n result = result.filter((item) => {\n // A filter that isn't a value of the item reads its `media`: `filter[type]=image`.\n const value =\n key in item ? item[key] : isRecord(item.media) ? item.media[key] : undefined;\n const values = Array.isArray(value) ? value : [value];\n return values.some((candidate) => accepted.includes(String(candidate)));\n });\n });\n if (query.q !== undefined && query.q !== '') {\n const terms = query.q.toLowerCase();\n result = result.filter((item) =>\n Object.entries(item)\n .filter(([key]) => !RESERVED_KEYS.has(key))\n .some(([, value]) => searchableText(value).toLowerCase().includes(terms)),\n );\n }\n const sort = query.sort ?? [];\n if (sort.length > 0) {\n result = [...result].sort((a, b) => {\n for (const part of sort) {\n const descending = part.startsWith('-');\n const key = descending ? part.slice(1) : part;\n const order = compare(a[key], b[key]);\n if (order !== 0) {\n return descending ? -order : order;\n }\n }\n return 0;\n });\n }\n return result;\n}\n\n/** The type of a media, from its MIME type. */\nfunction mediaType(mime: string | null | undefined): string {\n const family = (mime ?? '').split('/')[0] ?? '';\n if (family === 'image' || family === 'video' || family === 'audio') {\n return family;\n }\n return mime === 'application/pdf' || family === 'text' ? 'document' : 'other';\n}\n\n/** What the mock computes about a stored file: no renditions, ready at once. */\nfunction mediaOf(file: StoredFile): MediaInfo {\n const type = mediaType(file.mime);\n return {\n type,\n mime: file.mime ?? null,\n size: file.size ?? null,\n url: file.url ?? null,\n thumbnailUrl: type === 'image' ? (file.url ?? null) : null,\n status: 'ready',\n };\n}\n\n/**\n * An in-memory backend that implements the protocol's resource routes: definition, list, show,\n * create, update (with `If-Match`), delete and the built-in `delete` action. Values are checked\n * with the portable rules. Used to test the conformance kit, and to develop frontends without a\n * real backend.\n */\nexport function createMockBackend({ definition, items = {} }: MockBackendOptions): MockBackend {\n const store = new Map<string, Item[]>();\n let nextId = 1;\n\n const abilitiesFor = (resource: ResourceInput) =>\n Object.fromEntries([\n ['update', true],\n ['delete', true],\n ...(resource.actions ?? []).map((action) => [action.id, true]),\n ]);\n\n const makeItem = (resource: ResourceInput, values: Values): Item => {\n const id =\n typeof values.id === 'string' || typeof values.id === 'number'\n ? String(values.id)\n : String(nextId++);\n const rest = Object.fromEntries(\n Object.entries(values).filter(([key]) => !RESERVED_KEYS.has(key)),\n );\n return { ...rest, id, version: '1', abilities: abilitiesFor(resource) };\n };\n\n definition.resources.forEach((resource) => {\n store.set(\n resource.id,\n (items[resource.id] ?? []).map((values) => makeItem(resource, values)),\n );\n });\n\n const basePath = new URL(definition.api.baseUrl, 'http://panneau.test').pathname.replace(\n /\\/$/,\n '',\n );\n\n const validationProblem = (\n resource: ResourceInput,\n values: Values,\n mode: 'create' | 'update',\n ) => {\n const failures = checkValues(resource.fields, values, {\n mode,\n partial: mode === 'update',\n locales: definition.locales,\n });\n const paths = Object.keys(failures);\n if (paths.length === 0) {\n return null;\n }\n return problem(422, 'The given data was invalid.', {\n type: 'urn:panneau:problem:validation',\n errors: Object.fromEntries(\n paths.map((path) => [path, (failures[path] ?? []).map(message)]),\n ),\n });\n };\n\n const selectItems = (list: Item[], selection: Selection | undefined): Item[] => {\n if (selection === undefined) {\n return [];\n }\n if ('ids' in selection) {\n const ids = selection.ids as string[];\n return list.filter((item) => ids.includes(item.id));\n }\n const except = selection.except ?? [];\n const { filter, q } = selection.query;\n const query: Pick<ListQuery, 'filter' | 'q'> = {};\n if (filter !== undefined) {\n query.filter = filter as NonNullable<ListQuery['filter']>;\n }\n if (q !== undefined) {\n query.q = q;\n }\n return applyQuery(list, query).filter((item) => !except.includes(item.id));\n };\n\n // Direct uploads: the open uploads by id, with their key. The mock signs fake URLs, and\n // trusts the client when it completes an upload.\n const uploads = new Map<string, Upload>();\n let nextUpload = 1;\n\n const handleUploads = async (\n segments: string[],\n method: string,\n readBody: () => Promise<Values>,\n ): Promise<Response> => {\n if (definition.uploads?.mode !== 'presigned') {\n return problem(404, 'Not found.');\n }\n if (method !== 'POST') {\n return problem(405, 'Method not allowed.');\n }\n if (segments.length === 2 && segments[1] === 'sign') {\n const body = await readBody();\n const opens =\n body.uploadId === undefined && (body.method === 'POST' || body.method === 'PUT');\n if (opens) {\n const id = `upl_${nextUpload++}`;\n const upload: Upload = { id, key: `uploads/${id}` };\n uploads.set(id, upload);\n const response: UploadSignResponse = {\n url: `https://storage.test/${upload.key}?signature=mock`,\n key: upload.key,\n upload: id,\n };\n return json(200, response);\n }\n const upload = [...uploads.values()].find((candidate) => candidate.key === body.key);\n if (upload === undefined) {\n return problem(404, 'Upload not found.');\n }\n return json(200, { url: `https://storage.test/${upload.key}?signature=mock` });\n }\n if (segments.length === 3 && segments[2] === 'complete' && segments[1] !== undefined) {\n const upload = uploads.get(segments[1]);\n if (upload === undefined) {\n return problem(404, 'Upload not found.');\n }\n const { name } = await readBody();\n if (typeof name === 'string') {\n upload.name = name;\n }\n return json(200, { data: upload });\n }\n return problem(404, 'Not found.');\n };\n\n // The values of `file` fields at the top of a resource: an upload becomes a stored file, and\n // a stored file or null is kept. Returns the paths of uploads that don't exist.\n const resolveFiles = (\n resource: ResourceInput,\n values: Values,\n ): { values: Values; missing: string[] } => {\n const missing: string[] = [];\n const resolved = { ...values };\n const toFile = (value: unknown, path: string): unknown => {\n if (!isRecord(value) || typeof value.upload !== 'string') {\n return value;\n }\n const upload = uploads.get(value.upload);\n if (upload === undefined) {\n missing.push(path);\n return value;\n }\n const file: StoredFile = {\n name: upload.name ?? (typeof value.name === 'string' ? value.name : upload.id),\n url: `https://storage.test/${upload.key ?? upload.id}`,\n mime: upload.mime ?? (typeof value.mime === 'string' ? value.mime : null),\n size: upload.size ?? (typeof value.size === 'number' ? value.size : null),\n };\n return file;\n };\n resource.fields\n .filter((field) => field.type === 'file' && field.name in values)\n .forEach((field) => {\n const value = values[field.name];\n resolved[field.name] = Array.isArray(value)\n ? value.map((entry, index) => toFile(entry, `${field.name}.${String(index)}`))\n : toFile(value, field.name);\n });\n return { values: resolved, missing };\n };\n\n const missingMedia = (paths: string[]) =>\n problem(422, 'The given data was invalid.', {\n type: 'urn:panneau:problem:validation',\n errors: Object.fromEntries(paths.map((path) => [path, ['This media does not exist.']])),\n });\n\n const missingUploads = (paths: string[]) =>\n problem(422, 'The given data was invalid.', {\n type: 'urn:panneau:problem:validation',\n errors: Object.fromEntries(\n paths.map((path) => [path, ['This upload does not exist.']]),\n ),\n });\n\n // The values of `media` fields at the top of a resource: an id, or `{ id, transform }`,\n // becomes a reference to a media of its library. Returns the paths of media that don't exist.\n const resolveMedia = (\n resource: ResourceInput,\n values: Values,\n ): { values: Values; missing: string[] } => {\n const missing: string[] = [];\n const resolved = { ...values };\n const libraries = definition.resources.filter((candidate) => candidate.kind === 'media');\n resource.fields\n .filter((field) => field.type === 'media' && field.name in values)\n .forEach((field) => {\n const library = libraries.find((candidate) =>\n field.resource !== undefined ? candidate.id === field.resource : true,\n );\n const toReference = (value: unknown, path: string): unknown => {\n const id = isRecord(value) ? value.id : value;\n if (typeof id !== 'string' && typeof id !== 'number') {\n return value;\n }\n const media = (store.get(library?.id ?? '') ?? []).find(\n (candidate) => candidate.id === String(id),\n );\n if (media === undefined || library === undefined) {\n missing.push(path);\n return value;\n }\n const title = media[library.itemTitle ?? 'name'];\n const reference: MediaReference = {\n id: media.id,\n ...(typeof title === 'string' ? { label: title } : {}),\n ...(isRecord(media.media) ? { media: media.media as MediaInfo } : {}),\n ...(isRecord(value) && isRecord(value.transform)\n ? { transform: value.transform }\n : {}),\n };\n return reference;\n };\n const value = values[field.name];\n resolved[field.name] = Array.isArray(value)\n ? value.map((entry, index) =>\n toReference(entry, `${field.name}.${String(index)}`),\n )\n : toReference(value, field.name);\n });\n return { values: resolved, missing };\n };\n\n // Resolves the files and media of the values, and computes the `media` of a media item from\n // its file. The client never sets `media`.\n const resolveValues = (\n resource: ResourceInput,\n body: Values,\n ): { values: Values; problem: Response | null } => {\n const sent = { ...body };\n if (resource.kind === 'media') {\n delete sent.media;\n }\n const files = resolveFiles(resource, sent);\n if (files.missing.length > 0) {\n return { values: body, problem: missingUploads(files.missing) };\n }\n const media = resolveMedia(resource, files.values);\n if (media.missing.length > 0) {\n return { values: body, problem: missingMedia(media.missing) };\n }\n const values = media.values;\n if (resource.kind === 'media' && isRecord(values.file)) {\n values.media = mediaOf(values.file as StoredFile);\n }\n return { values, problem: null };\n };\n\n const handle = async (request: Request): Promise<Response> => {\n const url = new URL(request.url, 'http://panneau.test');\n if (!url.pathname.startsWith(basePath)) {\n return problem(404, 'Not found.');\n }\n const segments = url.pathname\n .slice(basePath.length)\n .split('/')\n .filter((segment) => segment !== '')\n .map(decodeURIComponent);\n const method = request.method.toUpperCase();\n const readBody = async (): Promise<Values> => {\n const body: unknown = await request.json().catch(() => null);\n return isRecord(body) ? body : {};\n };\n\n if (segments.length === 1 && segments[0] === 'definition' && method === 'GET') {\n return json(200, definition);\n }\n if (segments[0] === 'uploads') {\n return handleUploads(segments, method, readBody);\n }\n if (segments[0] !== 'resources' || segments[1] === undefined) {\n return problem(404, 'Not found.');\n }\n\n const resource = definition.resources.find((candidate) => candidate.id === segments[1]);\n const list = resource !== undefined ? store.get(resource.id) : undefined;\n if (resource === undefined || list === undefined) {\n return problem(404, 'Resource not found.');\n }\n\n if (segments.length === 2) {\n if (method === 'GET') {\n const query = fromSearchParams(url.searchParams);\n const perPage = query.perPage ?? resource.list?.perPage ?? 20;\n const page = query.page ?? 1;\n const sort = query.sort ?? resource.list?.defaultSort?.split(',');\n const matching = applyQuery(list, {\n ...query,\n ...(sort !== undefined ? { sort } : {}),\n });\n return json(200, {\n data: matching.slice((page - 1) * perPage, page * perPage),\n meta: {\n page,\n perPage,\n total: matching.length,\n lastPage: Math.max(1, Math.ceil(matching.length / perPage)),\n },\n });\n }\n if (method === 'POST') {\n const body = await readBody();\n const invalid = validationProblem(resource, body, 'create');\n if (invalid !== null) {\n return invalid;\n }\n const { values, problem: unresolved } = resolveValues(resource, body);\n if (unresolved !== null) {\n return unresolved;\n }\n const item = makeItem(resource, { ...values, id: undefined });\n list.push(item);\n return json(201, { data: item }, etag(item));\n }\n return problem(405, 'Method not allowed.');\n }\n\n if (segments[2] === 'actions' && segments[3] !== undefined && segments.length === 4) {\n if (method !== 'POST') {\n return problem(405, 'Method not allowed.');\n }\n const actionId = segments[3];\n const action = (resource.actions ?? []).find((candidate) => candidate.id === actionId);\n const body = await readBody();\n if (body.dryRun === true) {\n // A dry run counts what the action would do, and changes nothing.\n if (action?.dryRun !== true) {\n return problem(400, 'This action does not support dry runs.');\n }\n const selected = selectItems(list, body.selection as Selection | undefined);\n const allowed = selected.filter((item) => item.abilities?.[actionId] !== false);\n const result: ActionResult = {\n summary: {\n succeeded: allowed.length,\n failed: 0,\n skipped: selected.length - allowed.length,\n },\n results: selected\n .filter((item) => !allowed.includes(item))\n .slice(0, 20)\n .map((item) => ({ id: item.id, status: 'skipped', reason: 'forbidden' })),\n };\n return json(200, result);\n }\n if (actionId !== 'delete') {\n return problem(404, 'Action not found.');\n }\n const selected = selectItems(list, body.selection as Selection | undefined);\n store.set(\n resource.id,\n list.filter((item) => !selected.includes(item)),\n );\n const result: ActionResult = {\n message: `${selected.length} deleted.`,\n summary: { succeeded: selected.length, failed: 0, skipped: 0 },\n refresh: 'list',\n };\n return json(200, result);\n }\n\n if (segments.length !== 3 || segments[2] === undefined) {\n return problem(404, 'Not found.');\n }\n const id = segments[2];\n const index = list.findIndex((item) => item.id === id);\n const current = list[index];\n if (current === undefined) {\n return problem(404, 'Item not found.');\n }\n\n if (method === 'GET') {\n return json(200, { data: current }, etag(current));\n }\n if (method === 'PATCH') {\n const ifMatch = request.headers.get('If-Match');\n if (\n ifMatch !== null &&\n current.version !== null &&\n parseEtag(ifMatch) !== current.version\n ) {\n return problem(412, 'The item was changed by someone else.', {\n type: 'urn:panneau:problem:version-conflict',\n current,\n });\n }\n const body = await readBody();\n const invalid = validationProblem(resource, body, 'update');\n if (invalid !== null) {\n return invalid;\n }\n const { values, problem: unresolved } = resolveValues(resource, body);\n if (unresolved !== null) {\n return unresolved;\n }\n const updates = Object.fromEntries(\n Object.entries(values).filter(([key]) => !RESERVED_KEYS.has(key)),\n );\n const updated: Item = {\n ...current,\n ...updates,\n version: current.version === null ? null : String(Number(current.version) + 1),\n };\n list[index] = updated;\n return json(200, { data: updated }, etag(updated));\n }\n if (method === 'DELETE') {\n list.splice(index, 1);\n return json(204, null);\n }\n return problem(405, 'Method not allowed.');\n };\n\n return {\n fetch: (input, init) => {\n if (input instanceof Request) {\n return handle(input);\n }\n // Relative URLs are accepted, as a browser would resolve them.\n return handle(new Request(new URL(input, 'http://panneau.test'), init));\n },\n items: (resource) => [...(store.get(resource) ?? [])],\n };\n}\n\nexport { createExampleDefinition } from './example';\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAQA,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AA4E1C,oEAAoE;AACpE,wBAAgB,sBAAsB,IAAI,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAWnE;AAED,6DAA6D;AAC7D,wBAAgB,0BAA0B,IAAI,UAAU,CAQvD;AAED,oEAAoE;AACpE,wBAAgB,qBAAqB,IAAI,UAAU,CAQlD;AAED,wFAAwF;AACxF,wBAAgB,0BAA0B,IAAI,UAAU,CAQvD;AAED,wDAAwD;AACxD,wBAAgB,qBAAqB,IAAI,UAAU,CAialD"}
1
+ {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAQA,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AA4E1C,oEAAoE;AACpE,wBAAgB,sBAAsB,IAAI,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAWnE;AAED,6DAA6D;AAC7D,wBAAgB,0BAA0B,IAAI,UAAU,CAQvD;AAED,oEAAoE;AACpE,wBAAgB,qBAAqB,IAAI,UAAU,CAQlD;AAED,wFAAwF;AACxF,wBAAgB,0BAA0B,IAAI,UAAU,CAQvD;AAED,wDAAwD;AACxD,wBAAgB,qBAAqB,IAAI,UAAU,CAyVlD"}
package/dist/openapi.js CHANGED
@@ -257,115 +257,39 @@ function createOpenApiDocument() {
257
257
  }
258
258
  }
259
259
  },
260
- "/media": {
261
- get: {
262
- operationId: "listMedia",
263
- parameters: listParameters,
264
- responses: {
265
- 200: ok("A page of media.", ref("MediaListResponse")),
266
- ...problems(401, 403)
267
- }
260
+ "/uploads/sign": { post: {
261
+ operationId: "signUpload",
262
+ summary: "Signs a request of the browser's S3 client. The first request of a file creates its upload.",
263
+ requestBody: {
264
+ required: true,
265
+ content: json(ref("UploadSignRequest"))
268
266
  },
269
- post: {
270
- operationId: "uploadMedia",
271
- requestBody: {
272
- required: true,
273
- content: { "multipart/form-data": { schema: {
274
- type: "object",
275
- required: ["file"],
276
- properties: {
277
- file: {
278
- type: "string",
279
- contentMediaType: "application/octet-stream"
280
- },
281
- name: { type: "string" },
282
- tags: {
283
- type: "array",
284
- items: { type: "string" }
285
- }
286
- }
287
- } } }
288
- },
289
- responses: {
290
- 201: ok("The created media.", ref("MediaResponse")),
291
- ...problems(401, 403, 422)
292
- }
293
- }
294
- },
295
- "/media/tags": { get: {
296
- operationId: "listMediaTags",
297
267
  responses: {
298
- 200: ok("Tags in use.", {
299
- type: "object",
300
- required: ["data"],
301
- properties: { data: {
302
- type: "array",
303
- items: { type: "string" }
304
- } }
305
- }),
306
- ...problems(401, 403)
268
+ 200: ok("The presigned request.", ref("UploadSignResponse")),
269
+ ...problems(401, 403, 404, 422)
307
270
  }
308
271
  } },
309
- "/media/{id}": {
310
- parameters: [pathParameter("id", "Media id.")],
311
- get: {
312
- operationId: "getMedia",
313
- responses: {
314
- 200: ok("The media.", ref("MediaResponse")),
315
- ...problems(401, 403, 404)
316
- }
317
- },
318
- patch: {
319
- operationId: "updateMedia",
320
- description: "Updates metadata, tags or the image `transform`.",
321
- requestBody: {
322
- required: true,
323
- content: json(values)
324
- },
325
- responses: {
326
- 200: ok("The updated media.", ref("MediaResponse")),
327
- ...problems(401, 403, 404, 422)
328
- }
329
- },
330
- delete: {
331
- operationId: "deleteMedia",
332
- responses: {
333
- 204: ok("Moved to the trash."),
334
- ...problems(401, 403, 404, 409)
335
- }
336
- }
337
- },
338
- "/media/{id}/file": {
339
- parameters: [pathParameter("id", "Media id.")],
272
+ "/uploads/{upload}/complete": {
273
+ parameters: [pathParameter("upload", "Upload id: `upl_…`.")],
340
274
  post: {
341
- operationId: "replaceMediaFile",
275
+ operationId: "completeUpload",
276
+ summary: "Once the browser completed the upload in storage, checks the object and gives the upload, which values reference.",
342
277
  requestBody: {
343
- required: true,
344
- content: { "multipart/form-data": { schema: {
278
+ required: false,
279
+ content: json({
345
280
  type: "object",
346
- required: ["file"],
347
- properties: { file: {
281
+ properties: { name: {
348
282
  type: "string",
349
- contentMediaType: "application/octet-stream"
283
+ description: "The file's name, which the key doesn't keep, since the backend chose it."
350
284
  } }
351
- } } }
285
+ })
352
286
  },
353
287
  responses: {
354
- 200: ok("The media.", ref("MediaResponse")),
288
+ 200: ok("The upload.", ref("UploadResponse")),
355
289
  ...problems(401, 403, 404, 422)
356
290
  }
357
291
  }
358
292
  },
359
- "/media/{id}/restore": {
360
- parameters: [pathParameter("id", "Media id.")],
361
- post: {
362
- operationId: "restoreMedia",
363
- responses: {
364
- 200: ok("The restored media.", ref("MediaResponse")),
365
- ...problems(401, 403, 404)
366
- }
367
- }
368
- },
369
293
  "/auth/session": { get: {
370
294
  operationId: "getSession",
371
295
  responses: {
@@ -1 +1 @@
1
- {"version":3,"file":"openapi.js","names":[],"sources":["../src/openapi.ts"],"sourcesContent":["import { z } from 'zod';\n\nimport { createSchemaRegistry } from './schemas';\nimport { BootConfig } from './schemas/boot';\nimport { Definition } from './schemas/definition';\nimport { Theme } from './schemas/theme';\nimport { PROBLEM_CONTENT_TYPE, PROTOCOL_VERSION } from './version';\n\ntype JsonObject = Record<string, unknown>;\n\nconst ref = (id: string): JsonObject => ({ $ref: `#/components/schemas/${id}` });\n\nconst json = (schema: JsonObject): JsonObject => ({ 'application/json': { schema } });\n\nconst ok = (description: string, schema?: JsonObject, headers?: JsonObject): JsonObject => ({\n description,\n ...(schema !== undefined ? { content: json(schema) } : {}),\n ...(headers !== undefined ? { headers } : {}),\n});\n\nconst PROBLEMS: Record<number, string> = {\n 400: 'Bad request, such as a dry run of an action that does not declare `dryRun`.',\n 401: 'Not authenticated.',\n 403: 'Not allowed.',\n 404: 'Not found.',\n 409: 'Conflict with a business rule.',\n 412: 'Version conflict: `If-Match` does not match the current version, which is in `current`.',\n 422: 'Invalid values: messages are in `errors`, keyed by dot path.',\n};\n\nfunction problems(...statuses: number[]): JsonObject {\n return Object.fromEntries(\n statuses.map((status) => [\n String(status),\n {\n description: PROBLEMS[status],\n content: { [PROBLEM_CONTENT_TYPE]: { schema: ref('Problem') } },\n },\n ]),\n );\n}\n\nconst etag = { ETag: { description: 'The version of the item.', schema: { type: 'string' } } };\n\nconst pathParameter = (name: string, description: string): JsonObject => ({\n name,\n in: 'path',\n required: true,\n description,\n schema: { type: 'string' },\n});\n\nconst queryParameter = (name: string, description: string, schema: JsonObject): JsonObject => ({\n name,\n in: 'query',\n required: false,\n description,\n schema,\n});\n\nconst listParameters = [\n queryParameter('page', 'Page number, from 1.', { type: 'integer', minimum: 1 }),\n queryParameter('perPage', 'Items per page.', { type: 'integer', minimum: 1 }),\n queryParameter('sort', 'Comma-separated fields; a `-` prefix sorts descending.', {\n type: 'string',\n }),\n {\n name: 'filter',\n in: 'query',\n required: false,\n style: 'deepObject',\n explode: true,\n description:\n '`filter[status]=published`; `filter[tags][]=a&filter[tags][]=b` for any of several values; `filter[published_at][from]=2026-01-01&filter[published_at][to]=2026-01-31` for the inclusive bounds of a range filter (`date-range`), without the open end.',\n schema: { type: 'object', additionalProperties: true },\n },\n queryParameter('q', 'Search.', { type: 'string' }),\n queryParameter('fields', 'Comma-separated fields to include in each item.', { type: 'string' }),\n];\n\nconst resource = pathParameter('resource', 'Resource id.');\nconst id = pathParameter('id', 'Item id.');\nconst values = { type: 'object', additionalProperties: true };\n\n/** Component schemas, generated from the protocol's Zod schemas. */\nexport function createComponentSchemas(): Record<string, JsonObject> {\n const { schemas } = z.toJSONSchema(createSchemaRegistry(), {\n io: 'input',\n uri: (schemaId) => `#/components/schemas/${schemaId}`,\n });\n return Object.fromEntries(\n Object.entries(schemas).map(([schemaId, schema]) => {\n const { $schema: _schema, $id: _id, ...rest } = schema as JsonObject;\n return [schemaId, rest];\n }),\n );\n}\n\n/** The JSON Schema of the definition (`GET /definition`). */\nexport function createDefinitionJsonSchema(): JsonObject {\n return {\n $id: 'https://github.com/folkloreinc/panneau-js/blob/v5.x/packages/protocol/schema/definition.schema.json',\n ...(z.toJSONSchema(Definition, {\n io: 'input',\n metadata: createSchemaRegistry(),\n }) as JsonObject),\n };\n}\n\n/** The JSON Schema of a theme, for themes written as JSON files. */\nexport function createThemeJsonSchema(): JsonObject {\n return {\n $id: 'https://github.com/folkloreinc/panneau-js/blob/v5.x/packages/protocol/schema/theme.schema.json',\n ...(z.toJSONSchema(Theme, {\n io: 'input',\n metadata: createSchemaRegistry(),\n }) as JsonObject),\n };\n}\n\n/** The JSON Schema of the boot config, which a server writes in the page of the app. */\nexport function createBootConfigJsonSchema(): JsonObject {\n return {\n $id: 'https://github.com/folkloreinc/panneau-js/blob/v5.x/packages/protocol/schema/boot.schema.json',\n ...(z.toJSONSchema(BootConfig, {\n io: 'input',\n metadata: createSchemaRegistry(),\n }) as JsonObject),\n };\n}\n\n/** The OpenAPI 3.1 document of the Panneau HTTP API. */\nexport function createOpenApiDocument(): JsonObject {\n return {\n openapi: '3.1.0',\n info: {\n title: 'Panneau protocol',\n version: String(PROTOCOL_VERSION),\n description:\n 'The HTTP API between a Panneau frontend and its backend. Paths are relative to `api.baseUrl` of the definition. Authentication is up to the backend (session cookie or bearer token). Errors use `application/problem+json`.',\n },\n paths: {\n '/definition': {\n get: {\n operationId: 'getDefinition',\n summary: 'The definition for the current user, in the current locale.',\n responses: {\n 200: ok('The definition.', ref('Definition')),\n ...problems(401, 403),\n },\n },\n },\n '/resources/{resource}': {\n parameters: [resource],\n get: {\n operationId: 'listItems',\n parameters: listParameters,\n responses: {\n 200: ok('A page of items.', ref('ListResponse')),\n ...problems(401, 403, 404),\n },\n },\n post: {\n operationId: 'createItem',\n requestBody: { required: true, content: json(values) },\n responses: {\n 201: ok('The created item.', ref('ItemResponse'), etag),\n ...problems(401, 403, 404, 409, 422),\n },\n },\n },\n '/resources/{resource}/{id}': {\n parameters: [resource, id],\n get: {\n operationId: 'getItem',\n responses: {\n 200: ok('The item.', ref('ItemResponse'), etag),\n ...problems(401, 403, 404),\n },\n },\n patch: {\n operationId: 'updateItem',\n description: 'Partial update: only the values sent are changed.',\n parameters: [\n {\n name: 'If-Match',\n in: 'header',\n required: false,\n description: 'The version the client edited.',\n schema: { type: 'string' },\n },\n ],\n requestBody: { required: true, content: json(values) },\n responses: {\n 200: ok('The updated item.', ref('ItemResponse'), etag),\n ...problems(401, 403, 404, 409, 412, 422),\n },\n },\n delete: {\n operationId: 'deleteItem',\n responses: { 204: ok('Deleted.'), ...problems(401, 403, 404, 409) },\n },\n },\n '/resources/{resource}/{id}/preview': {\n parameters: [resource, id],\n post: {\n operationId: 'previewItem',\n requestBody: { required: true, content: json(values) },\n responses: {\n 200: ok('A short-lived preview URL.', ref('PreviewResponse')),\n ...problems(401, 403, 404, 422),\n },\n },\n },\n '/resources/{resource}/actions/{action}': {\n parameters: [resource, pathParameter('action', 'Action id.')],\n post: {\n operationId: 'runAction',\n requestBody: {\n required: true,\n content: {\n ...json(ref('ActionRequest')),\n 'multipart/form-data': { schema: ref('ActionRequest') },\n },\n },\n responses: {\n 200: ok('The result.', ref('ActionResult')),\n 202: ok('The action runs in the background.', ref('ActionRunResponse')),\n ...problems(400, 401, 403, 404, 409, 422),\n },\n },\n },\n '/actions/runs': {\n get: {\n operationId: 'listActionRuns',\n summary: \"The current user's recent runs.\",\n responses: {\n 200: ok('Runs, newest first.', {\n type: 'object',\n required: ['data'],\n properties: { data: { type: 'array', items: ref('ActionRun') } },\n }),\n ...problems(401),\n },\n },\n },\n '/actions/runs/{run}': {\n parameters: [pathParameter('run', 'Run id.')],\n get: {\n operationId: 'getActionRun',\n responses: {\n 200: ok('The run.', ref('ActionRunResponse')),\n ...problems(401, 403, 404),\n },\n },\n delete: {\n operationId: 'cancelActionRun',\n responses: { 204: ok('Cancelled.'), ...problems(401, 403, 404, 409) },\n },\n },\n '/media': {\n get: {\n operationId: 'listMedia',\n parameters: listParameters,\n responses: {\n 200: ok('A page of media.', ref('MediaListResponse')),\n ...problems(401, 403),\n },\n },\n post: {\n operationId: 'uploadMedia',\n requestBody: {\n required: true,\n content: {\n 'multipart/form-data': {\n schema: {\n type: 'object',\n required: ['file'],\n properties: {\n file: {\n type: 'string',\n contentMediaType: 'application/octet-stream',\n },\n name: { type: 'string' },\n tags: { type: 'array', items: { type: 'string' } },\n },\n },\n },\n },\n },\n responses: {\n 201: ok('The created media.', ref('MediaResponse')),\n ...problems(401, 403, 422),\n },\n },\n },\n '/media/tags': {\n get: {\n operationId: 'listMediaTags',\n responses: {\n 200: ok('Tags in use.', {\n type: 'object',\n required: ['data'],\n properties: { data: { type: 'array', items: { type: 'string' } } },\n }),\n ...problems(401, 403),\n },\n },\n },\n '/media/{id}': {\n parameters: [pathParameter('id', 'Media id.')],\n get: {\n operationId: 'getMedia',\n responses: {\n 200: ok('The media.', ref('MediaResponse')),\n ...problems(401, 403, 404),\n },\n },\n patch: {\n operationId: 'updateMedia',\n description: 'Updates metadata, tags or the image `transform`.',\n requestBody: { required: true, content: json(values) },\n responses: {\n 200: ok('The updated media.', ref('MediaResponse')),\n ...problems(401, 403, 404, 422),\n },\n },\n delete: {\n operationId: 'deleteMedia',\n responses: { 204: ok('Moved to the trash.'), ...problems(401, 403, 404, 409) },\n },\n },\n '/media/{id}/file': {\n parameters: [pathParameter('id', 'Media id.')],\n post: {\n operationId: 'replaceMediaFile',\n requestBody: {\n required: true,\n content: {\n 'multipart/form-data': {\n schema: {\n type: 'object',\n required: ['file'],\n properties: {\n file: {\n type: 'string',\n contentMediaType: 'application/octet-stream',\n },\n },\n },\n },\n },\n },\n responses: {\n 200: ok('The media.', ref('MediaResponse')),\n ...problems(401, 403, 404, 422),\n },\n },\n },\n '/media/{id}/restore': {\n parameters: [pathParameter('id', 'Media id.')],\n post: {\n operationId: 'restoreMedia',\n responses: {\n 200: ok('The restored media.', ref('MediaResponse')),\n ...problems(401, 403, 404),\n },\n },\n },\n '/auth/session': {\n get: {\n operationId: 'getSession',\n responses: { 200: ok('The current user.', ref('Session')), ...problems(401) },\n },\n },\n '/auth/login': {\n post: {\n operationId: 'login',\n requestBody: {\n required: true,\n content: json({\n type: 'object',\n required: ['email', 'password'],\n properties: {\n email: { type: 'string' },\n password: { type: 'string' },\n remember: { type: 'boolean' },\n },\n }),\n },\n responses: {\n 200: ok('Logged in.', ref('Session')),\n 202: ok(\n 'A second factor is required: answer `/auth/two-factor/challenge`.',\n {\n type: 'object',\n properties: { twoFactor: { type: 'boolean', const: true } },\n },\n ),\n ...problems(422),\n },\n },\n },\n '/auth/logout': {\n post: { operationId: 'logout', responses: { 204: ok('Logged out.') } },\n },\n '/auth/providers/{provider}/redirect': {\n parameters: [pathParameter('provider', 'Provider id.')],\n get: {\n operationId: 'redirectToProvider',\n parameters: [\n queryParameter('next', 'Local path to go to after login.', {\n type: 'string',\n }),\n ],\n responses: {\n 302: { description: 'Redirect to the provider.' },\n ...problems(404),\n },\n },\n },\n '/auth/password/forgot': {\n post: {\n operationId: 'forgotPassword',\n requestBody: {\n required: true,\n content: json({\n type: 'object',\n required: ['email'],\n properties: { email: { type: 'string' } },\n }),\n },\n responses: {\n 204: ok('A reset link was sent, if the account exists.'),\n ...problems(422),\n },\n },\n },\n '/auth/password/reset': {\n post: {\n operationId: 'resetPassword',\n requestBody: {\n required: true,\n content: json({\n type: 'object',\n required: ['token', 'email', 'password'],\n properties: {\n token: { type: 'string' },\n email: { type: 'string' },\n password: { type: 'string' },\n },\n }),\n },\n responses: { 204: ok('The password was reset.'), ...problems(422) },\n },\n },\n '/auth/two-factor/challenge': {\n post: {\n operationId: 'twoFactorChallenge',\n requestBody: {\n required: true,\n content: json({\n type: 'object',\n properties: {\n code: { type: 'string' },\n recoveryCode: { type: 'string' },\n },\n }),\n },\n responses: { 200: ok('Logged in.', ref('Session')), ...problems(422) },\n },\n },\n '/auth/passkeys/options': {\n post: {\n operationId: 'passkeyOptions',\n responses: {\n 200: ok('WebAuthn request options.', {\n type: 'object',\n additionalProperties: true,\n }),\n },\n },\n },\n '/auth/passkeys/login': {\n post: {\n operationId: 'passkeyLogin',\n requestBody: {\n required: true,\n content: json({ type: 'object', additionalProperties: true }),\n },\n responses: { 200: ok('Logged in.', ref('Session')), ...problems(422) },\n },\n },\n '/account': {\n get: {\n operationId: 'getAccount',\n responses: {\n 200: ok('The current user.', {\n type: 'object',\n required: ['data'],\n properties: { data: ref('User') },\n }),\n ...problems(401),\n },\n },\n patch: {\n operationId: 'updateAccount',\n requestBody: { required: true, content: json(values) },\n responses: {\n 200: ok('The updated user.', {\n type: 'object',\n required: ['data'],\n properties: { data: ref('User') },\n }),\n ...problems(401, 422),\n },\n },\n },\n '/search': {\n get: {\n operationId: 'search',\n parameters: [\n {\n ...queryParameter('q', 'Search terms.', { type: 'string' }),\n required: true,\n },\n ],\n responses: {\n 200: ok('Results grouped by resource.', ref('SearchResponse')),\n ...problems(401),\n },\n },\n },\n '/dashboard/widgets/{widget}': {\n parameters: [pathParameter('widget', 'Widget id.')],\n get: {\n operationId: 'getWidgetData',\n responses: {\n 200: ok(\"The widget's data. Its shape depends on the widget type.\", {\n type: 'object',\n additionalProperties: true,\n }),\n ...problems(401, 403, 404),\n },\n },\n },\n },\n components: { schemas: createComponentSchemas() },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"openapi.js","names":[],"sources":["../src/openapi.ts"],"sourcesContent":["import { z } from 'zod';\n\nimport { createSchemaRegistry } from './schemas';\nimport { BootConfig } from './schemas/boot';\nimport { Definition } from './schemas/definition';\nimport { Theme } from './schemas/theme';\nimport { PROBLEM_CONTENT_TYPE, PROTOCOL_VERSION } from './version';\n\ntype JsonObject = Record<string, unknown>;\n\nconst ref = (id: string): JsonObject => ({ $ref: `#/components/schemas/${id}` });\n\nconst json = (schema: JsonObject): JsonObject => ({ 'application/json': { schema } });\n\nconst ok = (description: string, schema?: JsonObject, headers?: JsonObject): JsonObject => ({\n description,\n ...(schema !== undefined ? { content: json(schema) } : {}),\n ...(headers !== undefined ? { headers } : {}),\n});\n\nconst PROBLEMS: Record<number, string> = {\n 400: 'Bad request, such as a dry run of an action that does not declare `dryRun`.',\n 401: 'Not authenticated.',\n 403: 'Not allowed.',\n 404: 'Not found.',\n 409: 'Conflict with a business rule.',\n 412: 'Version conflict: `If-Match` does not match the current version, which is in `current`.',\n 422: 'Invalid values: messages are in `errors`, keyed by dot path.',\n};\n\nfunction problems(...statuses: number[]): JsonObject {\n return Object.fromEntries(\n statuses.map((status) => [\n String(status),\n {\n description: PROBLEMS[status],\n content: { [PROBLEM_CONTENT_TYPE]: { schema: ref('Problem') } },\n },\n ]),\n );\n}\n\nconst etag = { ETag: { description: 'The version of the item.', schema: { type: 'string' } } };\n\nconst pathParameter = (name: string, description: string): JsonObject => ({\n name,\n in: 'path',\n required: true,\n description,\n schema: { type: 'string' },\n});\n\nconst queryParameter = (name: string, description: string, schema: JsonObject): JsonObject => ({\n name,\n in: 'query',\n required: false,\n description,\n schema,\n});\n\nconst listParameters = [\n queryParameter('page', 'Page number, from 1.', { type: 'integer', minimum: 1 }),\n queryParameter('perPage', 'Items per page.', { type: 'integer', minimum: 1 }),\n queryParameter('sort', 'Comma-separated fields; a `-` prefix sorts descending.', {\n type: 'string',\n }),\n {\n name: 'filter',\n in: 'query',\n required: false,\n style: 'deepObject',\n explode: true,\n description:\n '`filter[status]=published`; `filter[tags][]=a&filter[tags][]=b` for any of several values; `filter[published_at][from]=2026-01-01&filter[published_at][to]=2026-01-31` for the inclusive bounds of a range filter (`date-range`), without the open end.',\n schema: { type: 'object', additionalProperties: true },\n },\n queryParameter('q', 'Search.', { type: 'string' }),\n queryParameter('fields', 'Comma-separated fields to include in each item.', { type: 'string' }),\n];\n\nconst resource = pathParameter('resource', 'Resource id.');\nconst id = pathParameter('id', 'Item id.');\nconst values = { type: 'object', additionalProperties: true };\n\n/** Component schemas, generated from the protocol's Zod schemas. */\nexport function createComponentSchemas(): Record<string, JsonObject> {\n const { schemas } = z.toJSONSchema(createSchemaRegistry(), {\n io: 'input',\n uri: (schemaId) => `#/components/schemas/${schemaId}`,\n });\n return Object.fromEntries(\n Object.entries(schemas).map(([schemaId, schema]) => {\n const { $schema: _schema, $id: _id, ...rest } = schema as JsonObject;\n return [schemaId, rest];\n }),\n );\n}\n\n/** The JSON Schema of the definition (`GET /definition`). */\nexport function createDefinitionJsonSchema(): JsonObject {\n return {\n $id: 'https://github.com/folkloreinc/panneau-js/blob/v5.x/packages/protocol/schema/definition.schema.json',\n ...(z.toJSONSchema(Definition, {\n io: 'input',\n metadata: createSchemaRegistry(),\n }) as JsonObject),\n };\n}\n\n/** The JSON Schema of a theme, for themes written as JSON files. */\nexport function createThemeJsonSchema(): JsonObject {\n return {\n $id: 'https://github.com/folkloreinc/panneau-js/blob/v5.x/packages/protocol/schema/theme.schema.json',\n ...(z.toJSONSchema(Theme, {\n io: 'input',\n metadata: createSchemaRegistry(),\n }) as JsonObject),\n };\n}\n\n/** The JSON Schema of the boot config, which a server writes in the page of the app. */\nexport function createBootConfigJsonSchema(): JsonObject {\n return {\n $id: 'https://github.com/folkloreinc/panneau-js/blob/v5.x/packages/protocol/schema/boot.schema.json',\n ...(z.toJSONSchema(BootConfig, {\n io: 'input',\n metadata: createSchemaRegistry(),\n }) as JsonObject),\n };\n}\n\n/** The OpenAPI 3.1 document of the Panneau HTTP API. */\nexport function createOpenApiDocument(): JsonObject {\n return {\n openapi: '3.1.0',\n info: {\n title: 'Panneau protocol',\n version: String(PROTOCOL_VERSION),\n description:\n 'The HTTP API between a Panneau frontend and its backend. Paths are relative to `api.baseUrl` of the definition. Authentication is up to the backend (session cookie or bearer token). Errors use `application/problem+json`.',\n },\n paths: {\n '/definition': {\n get: {\n operationId: 'getDefinition',\n summary: 'The definition for the current user, in the current locale.',\n responses: {\n 200: ok('The definition.', ref('Definition')),\n ...problems(401, 403),\n },\n },\n },\n '/resources/{resource}': {\n parameters: [resource],\n get: {\n operationId: 'listItems',\n parameters: listParameters,\n responses: {\n 200: ok('A page of items.', ref('ListResponse')),\n ...problems(401, 403, 404),\n },\n },\n post: {\n operationId: 'createItem',\n requestBody: { required: true, content: json(values) },\n responses: {\n 201: ok('The created item.', ref('ItemResponse'), etag),\n ...problems(401, 403, 404, 409, 422),\n },\n },\n },\n '/resources/{resource}/{id}': {\n parameters: [resource, id],\n get: {\n operationId: 'getItem',\n responses: {\n 200: ok('The item.', ref('ItemResponse'), etag),\n ...problems(401, 403, 404),\n },\n },\n patch: {\n operationId: 'updateItem',\n description: 'Partial update: only the values sent are changed.',\n parameters: [\n {\n name: 'If-Match',\n in: 'header',\n required: false,\n description: 'The version the client edited.',\n schema: { type: 'string' },\n },\n ],\n requestBody: { required: true, content: json(values) },\n responses: {\n 200: ok('The updated item.', ref('ItemResponse'), etag),\n ...problems(401, 403, 404, 409, 412, 422),\n },\n },\n delete: {\n operationId: 'deleteItem',\n responses: { 204: ok('Deleted.'), ...problems(401, 403, 404, 409) },\n },\n },\n '/resources/{resource}/{id}/preview': {\n parameters: [resource, id],\n post: {\n operationId: 'previewItem',\n requestBody: { required: true, content: json(values) },\n responses: {\n 200: ok('A short-lived preview URL.', ref('PreviewResponse')),\n ...problems(401, 403, 404, 422),\n },\n },\n },\n '/resources/{resource}/actions/{action}': {\n parameters: [resource, pathParameter('action', 'Action id.')],\n post: {\n operationId: 'runAction',\n requestBody: {\n required: true,\n content: {\n ...json(ref('ActionRequest')),\n 'multipart/form-data': { schema: ref('ActionRequest') },\n },\n },\n responses: {\n 200: ok('The result.', ref('ActionResult')),\n 202: ok('The action runs in the background.', ref('ActionRunResponse')),\n ...problems(400, 401, 403, 404, 409, 422),\n },\n },\n },\n '/actions/runs': {\n get: {\n operationId: 'listActionRuns',\n summary: \"The current user's recent runs.\",\n responses: {\n 200: ok('Runs, newest first.', {\n type: 'object',\n required: ['data'],\n properties: { data: { type: 'array', items: ref('ActionRun') } },\n }),\n ...problems(401),\n },\n },\n },\n '/actions/runs/{run}': {\n parameters: [pathParameter('run', 'Run id.')],\n get: {\n operationId: 'getActionRun',\n responses: {\n 200: ok('The run.', ref('ActionRunResponse')),\n ...problems(401, 403, 404),\n },\n },\n delete: {\n operationId: 'cancelActionRun',\n responses: { 204: ok('Cancelled.'), ...problems(401, 403, 404, 409) },\n },\n },\n '/uploads/sign': {\n post: {\n operationId: 'signUpload',\n summary:\n \"Signs a request of the browser's S3 client. The first request of a file creates its upload.\",\n requestBody: { required: true, content: json(ref('UploadSignRequest')) },\n responses: {\n 200: ok('The presigned request.', ref('UploadSignResponse')),\n ...problems(401, 403, 404, 422),\n },\n },\n },\n '/uploads/{upload}/complete': {\n parameters: [pathParameter('upload', 'Upload id: `upl_…`.')],\n post: {\n operationId: 'completeUpload',\n summary:\n 'Once the browser completed the upload in storage, checks the object and gives the upload, which values reference.',\n requestBody: {\n required: false,\n content: json({\n type: 'object',\n properties: {\n name: {\n type: 'string',\n description:\n \"The file's name, which the key doesn't keep, since the backend chose it.\",\n },\n },\n }),\n },\n responses: {\n 200: ok('The upload.', ref('UploadResponse')),\n ...problems(401, 403, 404, 422),\n },\n },\n },\n '/auth/session': {\n get: {\n operationId: 'getSession',\n responses: { 200: ok('The current user.', ref('Session')), ...problems(401) },\n },\n },\n '/auth/login': {\n post: {\n operationId: 'login',\n requestBody: {\n required: true,\n content: json({\n type: 'object',\n required: ['email', 'password'],\n properties: {\n email: { type: 'string' },\n password: { type: 'string' },\n remember: { type: 'boolean' },\n },\n }),\n },\n responses: {\n 200: ok('Logged in.', ref('Session')),\n 202: ok(\n 'A second factor is required: answer `/auth/two-factor/challenge`.',\n {\n type: 'object',\n properties: { twoFactor: { type: 'boolean', const: true } },\n },\n ),\n ...problems(422),\n },\n },\n },\n '/auth/logout': {\n post: { operationId: 'logout', responses: { 204: ok('Logged out.') } },\n },\n '/auth/providers/{provider}/redirect': {\n parameters: [pathParameter('provider', 'Provider id.')],\n get: {\n operationId: 'redirectToProvider',\n parameters: [\n queryParameter('next', 'Local path to go to after login.', {\n type: 'string',\n }),\n ],\n responses: {\n 302: { description: 'Redirect to the provider.' },\n ...problems(404),\n },\n },\n },\n '/auth/password/forgot': {\n post: {\n operationId: 'forgotPassword',\n requestBody: {\n required: true,\n content: json({\n type: 'object',\n required: ['email'],\n properties: { email: { type: 'string' } },\n }),\n },\n responses: {\n 204: ok('A reset link was sent, if the account exists.'),\n ...problems(422),\n },\n },\n },\n '/auth/password/reset': {\n post: {\n operationId: 'resetPassword',\n requestBody: {\n required: true,\n content: json({\n type: 'object',\n required: ['token', 'email', 'password'],\n properties: {\n token: { type: 'string' },\n email: { type: 'string' },\n password: { type: 'string' },\n },\n }),\n },\n responses: { 204: ok('The password was reset.'), ...problems(422) },\n },\n },\n '/auth/two-factor/challenge': {\n post: {\n operationId: 'twoFactorChallenge',\n requestBody: {\n required: true,\n content: json({\n type: 'object',\n properties: {\n code: { type: 'string' },\n recoveryCode: { type: 'string' },\n },\n }),\n },\n responses: { 200: ok('Logged in.', ref('Session')), ...problems(422) },\n },\n },\n '/auth/passkeys/options': {\n post: {\n operationId: 'passkeyOptions',\n responses: {\n 200: ok('WebAuthn request options.', {\n type: 'object',\n additionalProperties: true,\n }),\n },\n },\n },\n '/auth/passkeys/login': {\n post: {\n operationId: 'passkeyLogin',\n requestBody: {\n required: true,\n content: json({ type: 'object', additionalProperties: true }),\n },\n responses: { 200: ok('Logged in.', ref('Session')), ...problems(422) },\n },\n },\n '/account': {\n get: {\n operationId: 'getAccount',\n responses: {\n 200: ok('The current user.', {\n type: 'object',\n required: ['data'],\n properties: { data: ref('User') },\n }),\n ...problems(401),\n },\n },\n patch: {\n operationId: 'updateAccount',\n requestBody: { required: true, content: json(values) },\n responses: {\n 200: ok('The updated user.', {\n type: 'object',\n required: ['data'],\n properties: { data: ref('User') },\n }),\n ...problems(401, 422),\n },\n },\n },\n '/search': {\n get: {\n operationId: 'search',\n parameters: [\n {\n ...queryParameter('q', 'Search terms.', { type: 'string' }),\n required: true,\n },\n ],\n responses: {\n 200: ok('Results grouped by resource.', ref('SearchResponse')),\n ...problems(401),\n },\n },\n },\n '/dashboard/widgets/{widget}': {\n parameters: [pathParameter('widget', 'Widget id.')],\n get: {\n operationId: 'getWidgetData',\n responses: {\n 200: ok(\"The widget's data. Its shape depends on the widget type.\", {\n type: 'object',\n additionalProperties: true,\n }),\n ...problems(401, 403, 404),\n },\n },\n },\n },\n components: { schemas: createComponentSchemas() },\n };\n}\n"],"mappings":"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"}