@atolis-hq/corum 0.1.0 → 0.1.5
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 +220 -223
- package/dist/src/bin/corum.js +39 -39
- package/package.json +40 -36
- package/web/app.jsx +668 -668
- package/web/index.html +41 -41
- package/web/nav.js +141 -141
- package/web/primitives.jsx +583 -583
- package/web/router.js +49 -49
- package/web/style.css +827 -827
- package/dist/src/cli.js +0 -20
- package/dist/src/openapi-to-api-endpoints.js +0 -240
package/dist/src/cli.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { parse } from "yaml";
|
|
4
|
-
import { convertOpenApiToApiEndpointDocuments, serializeApiEndpointDocument, } from "./openapi-to-api-endpoints.js";
|
|
5
|
-
const [sourcePath, targetRoot] = process.argv.slice(2);
|
|
6
|
-
if (!sourcePath || !targetRoot) {
|
|
7
|
-
console.error("Usage: node dist/src/cli.js <openapi-yaml> <graph-component-dir>");
|
|
8
|
-
process.exit(1);
|
|
9
|
-
}
|
|
10
|
-
const source = await readFile(sourcePath, "utf8");
|
|
11
|
-
const openApi = parse(source);
|
|
12
|
-
const component = path.basename(targetRoot);
|
|
13
|
-
const outputDir = path.join(targetRoot, "api-endpoints");
|
|
14
|
-
const documents = convertOpenApiToApiEndpointDocuments(openApi, { component });
|
|
15
|
-
await rm(outputDir, { recursive: true, force: true });
|
|
16
|
-
await mkdir(outputDir, { recursive: true });
|
|
17
|
-
for (const { fileName, document } of documents) {
|
|
18
|
-
await writeFile(path.join(outputDir, fileName), serializeApiEndpointDocument(document), "utf8");
|
|
19
|
-
}
|
|
20
|
-
console.log(`Wrote ${documents.length} APIEndpoint files to ${outputDir}`);
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
import { stringify } from "yaml";
|
|
2
|
-
const HTTP_METHODS = new Set(["get", "put", "post", "delete", "options", "head", "patch", "trace"]);
|
|
3
|
-
export function convertOpenApiToApiEndpointDocuments(openApi, options) {
|
|
4
|
-
const output = [];
|
|
5
|
-
for (const [path, pathItem] of Object.entries(openApi.paths ?? {})) {
|
|
6
|
-
if (!isRecord(pathItem)) {
|
|
7
|
-
continue;
|
|
8
|
-
}
|
|
9
|
-
for (const [method, operation] of Object.entries(pathItem)) {
|
|
10
|
-
if (!HTTP_METHODS.has(method) || !isRecord(operation)) {
|
|
11
|
-
continue;
|
|
12
|
-
}
|
|
13
|
-
const slug = slugify(operation.operationId ?? `${method}-${path}`);
|
|
14
|
-
const ctx = {
|
|
15
|
-
openApi,
|
|
16
|
-
schemas: {},
|
|
17
|
-
enums: {},
|
|
18
|
-
schemaNamesInProgress: new Set(),
|
|
19
|
-
};
|
|
20
|
-
const properties = {
|
|
21
|
-
method: method.toUpperCase(),
|
|
22
|
-
path,
|
|
23
|
-
responses: {},
|
|
24
|
-
};
|
|
25
|
-
const requestSchema = firstContentSchema(operation.requestBody?.content);
|
|
26
|
-
if (requestSchema) {
|
|
27
|
-
const requestName = `${slug}-request`;
|
|
28
|
-
createSchema(requestName, requestSchema, ctx, operation.requestBody?.description);
|
|
29
|
-
properties.request = requestName;
|
|
30
|
-
}
|
|
31
|
-
for (const [status, response] of Object.entries(operation.responses ?? {})) {
|
|
32
|
-
if (!isRecord(response)) {
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
const responseName = `${slug}-response-${status}`;
|
|
36
|
-
const responseSchema = firstContentSchema(response.content);
|
|
37
|
-
if (responseSchema) {
|
|
38
|
-
createSchema(responseName, responseSchema, ctx, response.description);
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
createEmptyBodySchema(responseName, response.description, ctx);
|
|
42
|
-
}
|
|
43
|
-
properties.responses[status] = responseName;
|
|
44
|
-
}
|
|
45
|
-
const document = {
|
|
46
|
-
"schema-version": "1.0",
|
|
47
|
-
id: `${options.component}.api-endpoints.${slug}`,
|
|
48
|
-
template: "APIEndpoint",
|
|
49
|
-
state: "proposed",
|
|
50
|
-
stability: "unstable",
|
|
51
|
-
name: `${method.toUpperCase()} ${path}`,
|
|
52
|
-
...(operation.description || operation.summary
|
|
53
|
-
? { description: operation.description ?? operation.summary }
|
|
54
|
-
: {}),
|
|
55
|
-
properties,
|
|
56
|
-
schemas: ctx.schemas,
|
|
57
|
-
};
|
|
58
|
-
if (Object.keys(ctx.enums).length > 0) {
|
|
59
|
-
document.enums = ctx.enums;
|
|
60
|
-
}
|
|
61
|
-
output.push({
|
|
62
|
-
fileName: `${slug}.yaml`,
|
|
63
|
-
document,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return output.sort((left, right) => left.fileName.localeCompare(right.fileName));
|
|
68
|
-
}
|
|
69
|
-
export function serializeApiEndpointDocument(document) {
|
|
70
|
-
return stringify(document, {
|
|
71
|
-
lineWidth: 0,
|
|
72
|
-
sortMapEntries: false,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
function createSchema(name, sourceSchema, ctx, description) {
|
|
76
|
-
if (ctx.schemas[name] || ctx.schemaNamesInProgress.has(name)) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
ctx.schemaNamesInProgress.add(name);
|
|
80
|
-
ctx.schemas[name] = {
|
|
81
|
-
...(description ? { description } : {}),
|
|
82
|
-
fields: {},
|
|
83
|
-
};
|
|
84
|
-
const schema = resolveSchema(sourceSchema, ctx.openApi);
|
|
85
|
-
const logicalName = typeof sourceSchema.$ref === "string" ? refTail(sourceSchema.$ref) : name;
|
|
86
|
-
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
87
|
-
const fields = {};
|
|
88
|
-
if (schema.type === "array") {
|
|
89
|
-
fields.items = createField(`${name}Item`, schema.items ?? {}, ctx, false, logicalName);
|
|
90
|
-
fields.items.cardinality = "many";
|
|
91
|
-
}
|
|
92
|
-
else if (schema.type === "object" || isRecord(schema.properties) || isRecord(schema.additionalProperties)) {
|
|
93
|
-
for (const [fieldName, propertySchema] of Object.entries(schema.properties ?? {})) {
|
|
94
|
-
if (!isRecord(propertySchema)) {
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
fields[fieldName] = createField(fieldName, propertySchema, ctx, required.has(fieldName), logicalName);
|
|
98
|
-
}
|
|
99
|
-
if (Object.keys(fields).length === 0 && isRecord(schema.additionalProperties)) {
|
|
100
|
-
fields.additionalProperties = createField("additionalProperties", schema.additionalProperties, ctx, true, logicalName);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
fields.value = createField("value", schema, ctx, true, logicalName);
|
|
105
|
-
}
|
|
106
|
-
ctx.schemas[name] = {
|
|
107
|
-
...(description || schema.description ? { description: description ?? schema.description } : {}),
|
|
108
|
-
fields,
|
|
109
|
-
};
|
|
110
|
-
ctx.schemaNamesInProgress.delete(name);
|
|
111
|
-
}
|
|
112
|
-
function createEmptyBodySchema(name, description, ctx) {
|
|
113
|
-
ctx.schemas[name] = {
|
|
114
|
-
...(description ? { description } : {}),
|
|
115
|
-
fields: {
|
|
116
|
-
message: {
|
|
117
|
-
scalarType: "string",
|
|
118
|
-
nullable: true,
|
|
119
|
-
cardinality: "one",
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
function createField(fieldName, sourceSchema, ctx, required, ownerName) {
|
|
125
|
-
const cardinality = sourceSchema.type === "array" ? "many" : "one";
|
|
126
|
-
const schema = sourceSchema.type === "array" && isRecord(sourceSchema.items) ? sourceSchema.items : sourceSchema;
|
|
127
|
-
if (isRecord(schema) && typeof schema.$ref === "string") {
|
|
128
|
-
const refName = refTail(schema.$ref);
|
|
129
|
-
const target = resolveSchema(schema, ctx.openApi);
|
|
130
|
-
createSchema(refName, target, ctx, target.description);
|
|
131
|
-
return {
|
|
132
|
-
objectRef: refName,
|
|
133
|
-
nullable: !required,
|
|
134
|
-
cardinality,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
if (Array.isArray(schema.enum)) {
|
|
138
|
-
const enumName = enumNameFor(ownerName, fieldName);
|
|
139
|
-
ctx.enums[enumName] = {
|
|
140
|
-
...(schema.description ? { description: schema.description } : {}),
|
|
141
|
-
values: Object.fromEntries(schema.enum.map((value) => [
|
|
142
|
-
slugify(String(value)),
|
|
143
|
-
{
|
|
144
|
-
name: String(value),
|
|
145
|
-
},
|
|
146
|
-
])),
|
|
147
|
-
};
|
|
148
|
-
return {
|
|
149
|
-
objectRef: enumName,
|
|
150
|
-
nullable: !required,
|
|
151
|
-
cardinality,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
if (schema.type === "object" || isRecord(schema.properties) || isRecord(schema.additionalProperties)) {
|
|
155
|
-
const objectName = pascalCase(fieldName);
|
|
156
|
-
createSchema(objectName, schema, ctx, schema.description);
|
|
157
|
-
return {
|
|
158
|
-
objectRef: objectName,
|
|
159
|
-
nullable: !required,
|
|
160
|
-
cardinality,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
return {
|
|
164
|
-
scalarType: scalarTypeFor(schema),
|
|
165
|
-
nullable: !required,
|
|
166
|
-
cardinality,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
function firstContentSchema(content) {
|
|
170
|
-
if (!isRecord(content)) {
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
173
|
-
const mediaType = ["application/json", "application/xml", "application/x-www-form-urlencoded"].find((type) => isRecord(content[type]?.schema)) ?? Object.keys(content).find((type) => isRecord(content[type]?.schema));
|
|
174
|
-
if (!mediaType) {
|
|
175
|
-
return undefined;
|
|
176
|
-
}
|
|
177
|
-
return content[mediaType].schema;
|
|
178
|
-
}
|
|
179
|
-
function resolveSchema(schema, openApi) {
|
|
180
|
-
if (typeof schema.$ref !== "string") {
|
|
181
|
-
return schema;
|
|
182
|
-
}
|
|
183
|
-
const pointer = schema.$ref.replace(/^#\//, "").split("/");
|
|
184
|
-
let current = openApi;
|
|
185
|
-
for (const segment of pointer) {
|
|
186
|
-
current = current?.[segment.replace(/~1/g, "/").replace(/~0/g, "~")];
|
|
187
|
-
}
|
|
188
|
-
if (!isRecord(current)) {
|
|
189
|
-
throw new Error(`Unable to resolve OpenAPI reference ${schema.$ref}`);
|
|
190
|
-
}
|
|
191
|
-
return current;
|
|
192
|
-
}
|
|
193
|
-
function scalarTypeFor(schema) {
|
|
194
|
-
if (schema.type === "integer") {
|
|
195
|
-
return "integer";
|
|
196
|
-
}
|
|
197
|
-
if (schema.type === "number") {
|
|
198
|
-
return "decimal";
|
|
199
|
-
}
|
|
200
|
-
if (schema.type === "boolean") {
|
|
201
|
-
return "boolean";
|
|
202
|
-
}
|
|
203
|
-
if (schema.type === "string" && schema.format === "date-time") {
|
|
204
|
-
return "datetime";
|
|
205
|
-
}
|
|
206
|
-
if (schema.type === "string" && schema.format === "date") {
|
|
207
|
-
return "date";
|
|
208
|
-
}
|
|
209
|
-
if (schema.type === "string" && schema.format === "time") {
|
|
210
|
-
return "time";
|
|
211
|
-
}
|
|
212
|
-
return "string";
|
|
213
|
-
}
|
|
214
|
-
function enumNameFor(ownerName, fieldName) {
|
|
215
|
-
if (fieldName.toLowerCase() === "status") {
|
|
216
|
-
return `${pascalCase(ownerName)}Status`;
|
|
217
|
-
}
|
|
218
|
-
return `${pascalCase(ownerName)}${pascalCase(fieldName)}`;
|
|
219
|
-
}
|
|
220
|
-
function refTail(ref) {
|
|
221
|
-
return ref.split("/").at(-1) ?? ref;
|
|
222
|
-
}
|
|
223
|
-
function slugify(value) {
|
|
224
|
-
return value
|
|
225
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
226
|
-
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
227
|
-
.replace(/^-|-$/g, "")
|
|
228
|
-
.toLowerCase();
|
|
229
|
-
}
|
|
230
|
-
function pascalCase(value) {
|
|
231
|
-
return value
|
|
232
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
233
|
-
.split(/[^a-zA-Z0-9]+/)
|
|
234
|
-
.filter(Boolean)
|
|
235
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
236
|
-
.join("");
|
|
237
|
-
}
|
|
238
|
-
function isRecord(value) {
|
|
239
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
240
|
-
}
|