diff --git a/.gitignore b/.gitignore index 566a229..e8cb62b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ node_modules/ -build/ +#build/ tmp/ *.local *.local.* diff --git a/README.md b/README.md index d3e9c58..4b617e8 100644 --- a/README.md +++ b/README.md @@ -54,3 +54,7 @@ import { MyCollections } from "./my-collections.d.ts"; const directus = new Directus(); ``` + +## System Collections + +Directus system collections are not included in the output by default. If you add custom fields to a collection like `directus_users`, you can include them in the generated types by using the `--includeSystemCollections` flag. diff --git a/build/cli.cjs b/build/cli.cjs new file mode 100755 index 0000000..b1aaf3f --- /dev/null +++ b/build/cli.cjs @@ -0,0 +1,210 @@ +#!/usr/bin/env node +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); + +// src/cli.ts +var import_promises2 = require("fs/promises"); +var import_path = require("path"); +var import_yargs = __toESM(require("yargs"), 1); + +// src/index.ts +var import_promises = require("fs/promises"); +var import_openapi_typescript = __toESM(require("openapi-typescript"), 1); +var import_zod = require("zod"); +var DirectusAuthResponse = import_zod.z.object({ + data: import_zod.z.object({ + access_token: import_zod.z.string(), + expires: import_zod.z.number().int(), + refresh_token: import_zod.z.string() + }) +}); +var readSpecFile = async (options) => { + if (typeof options.specFile === `string`) { + return JSON.parse( + await (0, import_promises.readFile)(options.specFile, { encoding: `utf-8` }) + ); + } + if (typeof options.host !== `string`) { + throw new Error(`Either inputFile or inputUrl must be specified`); + } + if (typeof options.email !== `string`) { + throw new Error(`email must be specified`); + } + if (typeof options.password !== `string`) { + throw new Error(`password must be specified`); + } + const { + data: { access_token } + } = await fetch(new URL(`/auth/login`, options.host), { + body: JSON.stringify({ + email: options.email, + password: options.password + }), + headers: { + "Content-Type": `application/json` + }, + method: `POST` + }).then((response) => response.json()).then((json) => DirectusAuthResponse.parse(json)); + return await fetch(new URL(`/server/specs/oas`, options.host), { + headers: { + "Authorization": `Bearer ${access_token}`, + "Content-Type": `application/json` + } + }).then((response) => response.json()); +}; +var validIdentifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; +var generateTypeScript = async (spec, { includeSystemCollections, typeName }) => { + if (!validIdentifier.test(typeName)) { + throw new Error(`Invalid type name: ${typeName}`); + } + let source = await (0, import_openapi_typescript.default)(spec); + source += ` + +export type ${typeName} = { +`; + const collections = {}; + if (spec.paths) { + for (const [path, pathItem] of Object.entries(spec.paths)) { + const collectionPathPattern = /^\/items\/(?[a-zA-Z0-9_]+)$/; + const collection = collectionPathPattern.exec(path)?.groups?.[`collection`]; + if (typeof collection !== `string` || collection.length === 0) { + continue; + } + if (`get` in pathItem && `responses` in pathItem.get && `200` in pathItem.get.responses && `content` in pathItem.get.responses[`200`] && `application/json` in pathItem.get.responses[`200`].content && `schema` in pathItem.get.responses[`200`].content[`application/json`] && `properties` in pathItem.get.responses[`200`].content[`application/json`].schema && `data` in pathItem.get.responses[`200`].content[`application/json`].schema.properties && `items` in pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`] && `$ref` in pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`].items) { + const $ref = pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`].items.$ref; + const refPattern = /^#\/components\/schemas\/(?[a-zA-Z0-9_]+)$/; + const ref = refPattern.exec($ref)?.groups?.[`ref`]; + if (typeof ref !== `string` || ref.length === 0) { + continue; + } + if (!collections[collection]) { + collections[collection] = `components["schemas"]["${ref}"][]`; + } + } + } + } + const relationshipPathPattern = /^\/relations\/(?[a-zA-Z0-9_]+)$/; + for (const [path, pathItem] of Object.entries(spec.paths ?? {})) { + const relation = relationshipPathPattern.exec(path)?.groups?.[`relation`]; + if (typeof relation !== `string` || relation.length === 0) { + continue; + } + if (`get` in pathItem && `responses` in pathItem.get && `200` in pathItem.get.responses && `content` in pathItem.get.responses[`200`] && `application/json` in pathItem.get.responses[`200`].content && `schema` in pathItem.get.responses[`200`].content[`application/json`] && `properties` in pathItem.get.responses[`200`].content[`application/json`].schema && `data` in pathItem.get.responses[`200`].content[`application/json`].schema.properties && `items` in pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`] && `$ref` in pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`].items) { + const $ref = pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`].items.$ref; + const refPattern = /^#\/components\/schemas\/(?[a-zA-Z0-9_]+)$/; + const ref = refPattern.exec($ref)?.groups?.[`ref`]; + if (typeof ref !== `string` || ref.length === 0) { + continue; + } + if (!collections[relation]) { + collections[relation] = `components["schemas"]["${ref}"][]`; + } + } + } + if (spec.components && spec.components.schemas && includeSystemCollections) { + for (const [schema_key, schema_value] of Object.entries( + spec.components.schemas + )) { + const x_collection = schema_value[`x-collection`]; + if (typeof x_collection === `string` && x_collection.length > 0) { + if (!collections[x_collection]) { + collections[x_collection] = `components["schemas"]["${schema_key}"]`; + } + } + } + } + for (const [collectionName, typeDef] of Object.entries(collections)) { + source += ` ${collectionName}: ${typeDef}; +`; + } + source += `}; +`; + const toPascalCase = (str) => str.replace(/[_\- ]+/g, ` `).split(` `).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(``); + for (const [collectionName, typeDef] of Object.entries(collections)) { + const pascalCaseName = toPascalCase(collectionName); + source += `export type ${pascalCaseName} = ${typeDef}; +`; + } + return source; +}; + +// src/cli.ts +var main = async () => { + const argv = await import_yargs.default.options({ + email: { + alias: `e`, + description: `Email address`, + type: `string` + }, + host: { + alias: `h`, + description: `Remote host`, + type: `string` + }, + includeSystemCollections: { + alias: `s`, + default: false, + description: `Include system collections`, + type: `boolean` + }, + outFile: { + alias: `o`, + description: `Output file`, + type: `string` + }, + password: { + alias: `p`, + description: `Password`, + type: `string` + }, + specFile: { + alias: `i`, + description: `Input spec file`, + type: `string` + }, + typeName: { + alias: `t`, + default: `Schema`, + description: `Type name`, + type: `string` + } + }).argv; + const spec = await readSpecFile(argv); + const ts = await generateTypeScript(spec, { + includeSystemCollections: argv.includeSystemCollections, + typeName: argv.typeName + }); + if (typeof argv.outFile === `string`) { + await (0, import_promises2.writeFile)((0, import_path.resolve)(process.cwd(), argv.outFile), ts, { + encoding: `utf-8` + }); + } else { + console.log(ts); + } +}; +main().catch((error) => { + console.error(error); + process.exit(1); +}); +//# sourceMappingURL=cli.cjs.map diff --git a/build/cli.cjs.map b/build/cli.cjs.map new file mode 100644 index 0000000..91f4747 --- /dev/null +++ b/build/cli.cjs.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../src/cli.ts", "../src/index.ts"], + "sourcesContent": ["#!/usr/bin/env node\n\nimport { writeFile } from \"fs/promises\";\nimport { resolve } from \"path\";\n\nimport yargs from \"yargs\";\nimport type { OpenAPI3 } from \"openapi-typescript\";\n\nimport { generateTypeScript, readSpecFile } from \".\";\n\nconst main = async (): Promise => {\n const argv = await yargs.options({\n email: {\n alias: `e`,\n description: `Email address`,\n type: `string`,\n },\n host: {\n alias: `h`,\n description: `Remote host`,\n type: `string`,\n },\n includeSystemCollections: {\n alias: `s`,\n default: false,\n description: `Include system collections`,\n type: `boolean`,\n },\n outFile: {\n alias: `o`,\n description: `Output file`,\n type: `string`,\n },\n password: {\n alias: `p`,\n description: `Password`,\n type: `string`,\n },\n specFile: {\n alias: `i`,\n description: `Input spec file`,\n type: `string`,\n },\n typeName: {\n alias: `t`,\n default: `Schema`,\n description: `Type name`,\n type: `string`,\n },\n }).argv;\n\n const spec = await readSpecFile(argv);\n\n const ts = await generateTypeScript(spec as OpenAPI3, {\n includeSystemCollections: argv.includeSystemCollections,\n typeName: argv.typeName,\n });\n\n if (typeof argv.outFile === `string`) {\n await writeFile(resolve(process.cwd(), argv.outFile), ts, {\n encoding: `utf-8`,\n });\n } else {\n console.log(ts);\n }\n};\n\nmain().catch((error) => {\n console.error(error);\n process.exit(1);\n});\n", "import { readFile } from \"fs/promises\";\n\nimport type { OpenAPI3 } from \"openapi-typescript\";\nimport openapiTS from \"openapi-typescript\";\nimport { z } from \"zod\";\n\ntype ReadSpecFileOptions = {\n readonly specFile?: undefined | string;\n readonly host?: undefined | string;\n readonly email?: undefined | string;\n readonly password?: undefined | string;\n};\n\nconst DirectusAuthResponse = z.object({\n data: z.object({\n access_token: z.string(),\n expires: z.number().int(),\n refresh_token: z.string(),\n }),\n});\n\nexport const readSpecFile = async (\n options: ReadSpecFileOptions,\n): Promise => {\n if (typeof options.specFile === `string`) {\n return JSON.parse(\n await readFile(options.specFile, { encoding: `utf-8` }),\n ) as unknown;\n }\n\n if (typeof options.host !== `string`) {\n throw new Error(`Either inputFile or inputUrl must be specified`);\n }\n if (typeof options.email !== `string`) {\n throw new Error(`email must be specified`);\n }\n if (typeof options.password !== `string`) {\n throw new Error(`password must be specified`);\n }\n\n const {\n data: { access_token },\n } = await fetch(new URL(`/auth/login`, options.host), {\n body: JSON.stringify({\n email: options.email,\n password: options.password,\n }),\n headers: {\n \"Content-Type\": `application/json`,\n },\n method: `POST`,\n })\n .then((response) => response.json())\n .then((json) => DirectusAuthResponse.parse(json));\n\n return (await fetch(new URL(`/server/specs/oas`, options.host), {\n headers: {\n \"Authorization\": `Bearer ${access_token}`,\n \"Content-Type\": `application/json`,\n },\n }).then((response) => response.json())) as unknown;\n};\n\ntype GenerateTypeScriptOptions = {\n readonly includeSystemCollections?: boolean;\n readonly typeName: string;\n};\n\nconst validIdentifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;\n\nexport const generateTypeScript = async (\n spec: OpenAPI3,\n { includeSystemCollections, typeName }: GenerateTypeScriptOptions,\n): Promise => {\n if (!validIdentifier.test(typeName)) {\n throw new Error(`Invalid type name: ${typeName}`);\n }\n\n let source = await openapiTS(spec);\n\n source += `\\n\\nexport type ${typeName} = {\\n`;\n\n // Keep a record of discovered collections to avoid duplicates\n const collections: Record = {};\n\n if (spec.paths) {\n for (const [path, pathItem] of Object.entries(spec.paths)) {\n const collectionPathPattern = /^\\/items\\/(?[a-zA-Z0-9_]+)$/;\n const collection =\n collectionPathPattern.exec(path)?.groups?.[`collection`];\n if (typeof collection !== `string` || collection.length === 0) {\n continue;\n }\n if (\n `get` in pathItem &&\n `responses` in pathItem.get &&\n `200` in pathItem.get.responses &&\n `content` in pathItem.get.responses[`200`] &&\n `application/json` in pathItem.get.responses[`200`].content &&\n `schema` in pathItem.get.responses[`200`].content[`application/json`] &&\n `properties` in\n pathItem.get.responses[`200`].content[`application/json`].schema &&\n `data` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties &&\n `items` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`] &&\n `$ref` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`].items\n ) {\n const $ref =\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`].items.$ref;\n const refPattern = /^#\\/components\\/schemas\\/(?[a-zA-Z0-9_]+)$/;\n const ref = refPattern.exec($ref)?.groups?.[`ref`];\n if (typeof ref !== `string` || ref.length === 0) {\n continue;\n }\n // Instead of adding directly to source, store in collections\n if (!collections[collection]) {\n collections[collection] = `components[\"schemas\"][\"${ref}\"][]`;\n }\n }\n }\n }\n\n // fixing relationships\n const relationshipPathPattern = /^\\/relations\\/(?[a-zA-Z0-9_]+)$/;\n for (const [path, pathItem] of Object.entries(spec.paths ?? {})) {\n const relation = relationshipPathPattern.exec(path)?.groups?.[`relation`];\n if (typeof relation !== `string` || relation.length === 0) {\n continue;\n }\n if (\n `get` in pathItem &&\n `responses` in pathItem.get &&\n `200` in pathItem.get.responses &&\n `content` in pathItem.get.responses[`200`] &&\n `application/json` in pathItem.get.responses[`200`].content &&\n `schema` in pathItem.get.responses[`200`].content[`application/json`] &&\n `properties` in\n pathItem.get.responses[`200`].content[`application/json`].schema &&\n `data` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties &&\n `items` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`] &&\n `$ref` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`].items\n ) {\n const $ref =\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`].items.$ref;\n const refPattern = /^#\\/components\\/schemas\\/(?[a-zA-Z0-9_]+)$/;\n const ref = refPattern.exec($ref)?.groups?.[`ref`];\n if (typeof ref !== `string` || ref.length === 0) {\n continue;\n }\n // Add relationship to collections\n if (!collections[relation]) {\n collections[relation] = `components[\"schemas\"][\"${ref}\"][]`;\n }\n }\n }\n\n // Add directus system collections if requested\n if (spec.components && spec.components.schemas && includeSystemCollections) {\n for (const [schema_key, schema_value] of Object.entries(\n spec.components.schemas,\n )) {\n const x_collection = (schema_value as Record)[\n `x-collection`\n ] as string | undefined;\n if (typeof x_collection === `string` && x_collection.length > 0) {\n // Only add if not already present\n if (!collections[x_collection]) {\n collections[x_collection] = `components[\"schemas\"][\"${schema_key}\"]`;\n }\n }\n }\n }\n\n // After gathering all collections, write them out once\n for (const [collectionName, typeDef] of Object.entries(collections)) {\n source += ` ${collectionName}: ${typeDef};\\n`;\n }\n\n source += `};\\n`;\n\n const toPascalCase = (str: string): string =>\n str\n .replace(/[_\\- ]+/g, ` `)\n .split(` `)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(``);\n\n // Iterate over each collection to create individual export types\n for (const [collectionName, typeDef] of Object.entries(collections)) {\n const pascalCaseName = toPascalCase(collectionName);\n source += `export type ${pascalCaseName} = ${typeDef};\\n`;\n }\n\n return source;\n};\n"], + "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;AAEA,IAAAA,mBAA0B;AAC1B,kBAAwB;AAExB,mBAAkB;;;ACLlB,sBAAyB;AAGzB,gCAAsB;AACtB,iBAAkB;AASlB,IAAM,uBAAuB,aAAE,OAAO;AAAA,EACpC,MAAM,aAAE,OAAO;AAAA,IACb,cAAc,aAAE,OAAO;AAAA,IACvB,SAAS,aAAE,OAAO,EAAE,IAAI;AAAA,IACxB,eAAe,aAAE,OAAO;AAAA,EAC1B,CAAC;AACH,CAAC;AAEM,IAAM,eAAe,OAC1B,YACqB;AACrB,MAAI,OAAO,QAAQ,aAAa,UAAU;AACxC,WAAO,KAAK;AAAA,MACV,UAAM,0BAAS,QAAQ,UAAU,EAAE,UAAU,QAAQ,CAAC;AAAA,IACxD;AAAA,EACF;AAEA,MAAI,OAAO,QAAQ,SAAS,UAAU;AACpC,UAAM,IAAI,MAAM,gDAAgD;AAAA,EAClE;AACA,MAAI,OAAO,QAAQ,UAAU,UAAU;AACrC,UAAM,IAAI,MAAM,yBAAyB;AAAA,EAC3C;AACA,MAAI,OAAO,QAAQ,aAAa,UAAU;AACxC,UAAM,IAAI,MAAM,4BAA4B;AAAA,EAC9C;AAEA,QAAM;AAAA,IACJ,MAAM,EAAE,aAAa;AAAA,EACvB,IAAI,MAAM,MAAM,IAAI,IAAI,eAAe,QAAQ,IAAI,GAAG;AAAA,IACpD,MAAM,KAAK,UAAU;AAAA,MACnB,OAAO,QAAQ;AAAA,MACf,UAAU,QAAQ;AAAA,IACpB,CAAC;AAAA,IACD,SAAS;AAAA,MACP,gBAAgB;AAAA,IAClB;AAAA,IACA,QAAQ;AAAA,EACV,CAAC,EACE,KAAK,CAAC,aAAa,SAAS,KAAK,CAAC,EAClC,KAAK,CAAC,SAAS,qBAAqB,MAAM,IAAI,CAAC;AAElD,SAAQ,MAAM,MAAM,IAAI,IAAI,qBAAqB,QAAQ,IAAI,GAAG;AAAA,IAC9D,SAAS;AAAA,MACP,iBAAiB,UAAU,YAAY;AAAA,MACvC,gBAAgB;AAAA,IAClB;AAAA,EACF,CAAC,EAAE,KAAK,CAAC,aAAa,SAAS,KAAK,CAAC;AACvC;AAOA,IAAM,kBAAkB;AAEjB,IAAM,qBAAqB,OAChC,MACA,EAAE,0BAA0B,SAAS,MACjB;AACpB,MAAI,CAAC,gBAAgB,KAAK,QAAQ,GAAG;AACnC,UAAM,IAAI,MAAM,sBAAsB,QAAQ,EAAE;AAAA,EAClD;AAEA,MAAI,SAAS,UAAM,0BAAAC,SAAU,IAAI;AAEjC,YAAU;AAAA;AAAA,cAAmB,QAAQ;AAAA;AAGrC,QAAM,cAAsC,CAAC;AAE7C,MAAI,KAAK,OAAO;AACd,eAAW,CAAC,MAAM,QAAQ,KAAK,OAAO,QAAQ,KAAK,KAAK,GAAG;AACzD,YAAM,wBAAwB;AAC9B,YAAM,aACJ,sBAAsB,KAAK,IAAI,GAAG,SAAS,YAAY;AACzD,UAAI,OAAO,eAAe,YAAY,WAAW,WAAW,GAAG;AAC7D;AAAA,MACF;AACA,UACE,SAAS,YACT,eAAe,SAAS,OACxB,SAAS,SAAS,IAAI,aACtB,aAAa,SAAS,IAAI,UAAU,KAAK,KACzC,sBAAsB,SAAS,IAAI,UAAU,KAAK,EAAE,WACpD,YAAY,SAAS,IAAI,UAAU,KAAK,EAAE,QAAQ,kBAAkB,KACpE,gBACE,SAAS,IAAI,UAAU,KAAK,EAAE,QAAQ,kBAAkB,EAAE,UAC5D,UACE,SAAS,IAAI,UAAU,KAAK,EAAE,QAAQ,kBAAkB,EAAE,OACvD,cACL,WACE,SAAS,IAAI,UAAU,KAAK,EAAE,QAAQ,kBAAkB,EAAE,OACvD,WAAW,MAAM,KACtB,UACE,SAAS,IAAI,UAAU,KAAK,EAAE,QAAQ,kBAAkB,EAAE,OACvD,WAAW,MAAM,EAAE,OACxB;AACA,cAAM,OACJ,SAAS,IAAI,UAAU,KAAK,EAAE,QAAQ,kBAAkB,EAAE,OACvD,WAAW,MAAM,EAAE,MAAM;AAC9B,cAAM,aAAa;AACnB,cAAM,MAAM,WAAW,KAAK,IAAI,GAAG,SAAS,KAAK;AACjD,YAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,GAAG;AAC/C;AAAA,QACF;AAEA,YAAI,CAAC,YAAY,UAAU,GAAG;AAC5B,sBAAY,UAAU,IAAI,0BAA0B,GAAG;AAAA,QACzD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,QAAM,0BAA0B;AAChC,aAAW,CAAC,MAAM,QAAQ,KAAK,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,GAAG;AAC/D,UAAM,WAAW,wBAAwB,KAAK,IAAI,GAAG,SAAS,UAAU;AACxE,QAAI,OAAO,aAAa,YAAY,SAAS,WAAW,GAAG;AACzD;AAAA,IACF;AACA,QACE,SAAS,YACT,eAAe,SAAS,OACxB,SAAS,SAAS,IAAI,aACtB,aAAa,SAAS,IAAI,UAAU,KAAK,KACzC,sBAAsB,SAAS,IAAI,UAAU,KAAK,EAAE,WACpD,YAAY,SAAS,IAAI,UAAU,KAAK,EAAE,QAAQ,kBAAkB,KACpE,gBACE,SAAS,IAAI,UAAU,KAAK,EAAE,QAAQ,kBAAkB,EAAE,UAC5D,UACE,SAAS,IAAI,UAAU,KAAK,EAAE,QAAQ,kBAAkB,EAAE,OACvD,cACL,WACE,SAAS,IAAI,UAAU,KAAK,EAAE,QAAQ,kBAAkB,EAAE,OACvD,WAAW,MAAM,KACtB,UACE,SAAS,IAAI,UAAU,KAAK,EAAE,QAAQ,kBAAkB,EAAE,OACvD,WAAW,MAAM,EAAE,OACxB;AACA,YAAM,OACJ,SAAS,IAAI,UAAU,KAAK,EAAE,QAAQ,kBAAkB,EAAE,OACvD,WAAW,MAAM,EAAE,MAAM;AAC9B,YAAM,aAAa;AACnB,YAAM,MAAM,WAAW,KAAK,IAAI,GAAG,SAAS,KAAK;AACjD,UAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,GAAG;AAC/C;AAAA,MACF;AAEA,UAAI,CAAC,YAAY,QAAQ,GAAG;AAC1B,oBAAY,QAAQ,IAAI,0BAA0B,GAAG;AAAA,MACvD;AAAA,IACF;AAAA,EACF;AAGA,MAAI,KAAK,cAAc,KAAK,WAAW,WAAW,0BAA0B;AAC1E,eAAW,CAAC,YAAY,YAAY,KAAK,OAAO;AAAA,MAC9C,KAAK,WAAW;AAAA,IAClB,GAAG;AACD,YAAM,eAAgB,aACpB,cACF;AACA,UAAI,OAAO,iBAAiB,YAAY,aAAa,SAAS,GAAG;AAE/D,YAAI,CAAC,YAAY,YAAY,GAAG;AAC9B,sBAAY,YAAY,IAAI,0BAA0B,UAAU;AAAA,QAClE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,aAAW,CAAC,gBAAgB,OAAO,KAAK,OAAO,QAAQ,WAAW,GAAG;AACnE,cAAU,KAAK,cAAc,KAAK,OAAO;AAAA;AAAA,EAC3C;AAEA,YAAU;AAAA;AAEV,QAAM,eAAe,CAAC,QACpB,IACG,QAAQ,YAAY,GAAG,EACvB,MAAM,GAAG,EACT,IAAI,CAAC,SAAS,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,KAAK,MAAM,CAAC,CAAC,EAC1D,KAAK,EAAE;AAGZ,aAAW,CAAC,gBAAgB,OAAO,KAAK,OAAO,QAAQ,WAAW,GAAG;AACnE,UAAM,iBAAiB,aAAa,cAAc;AAClD,cAAU,eAAe,cAAc,MAAM,OAAO;AAAA;AAAA,EACtD;AAEA,SAAO;AACT;;;ADrMA,IAAM,OAAO,YAA2B;AACtC,QAAM,OAAO,MAAM,aAAAC,QAAM,QAAQ;AAAA,IAC/B,OAAO;AAAA,MACL,OAAO;AAAA,MACP,aAAa;AAAA,MACb,MAAM;AAAA,IACR;AAAA,IACA,MAAM;AAAA,MACJ,OAAO;AAAA,MACP,aAAa;AAAA,MACb,MAAM;AAAA,IACR;AAAA,IACA,0BAA0B;AAAA,MACxB,OAAO;AAAA,MACP,SAAS;AAAA,MACT,aAAa;AAAA,MACb,MAAM;AAAA,IACR;AAAA,IACA,SAAS;AAAA,MACP,OAAO;AAAA,MACP,aAAa;AAAA,MACb,MAAM;AAAA,IACR;AAAA,IACA,UAAU;AAAA,MACR,OAAO;AAAA,MACP,aAAa;AAAA,MACb,MAAM;AAAA,IACR;AAAA,IACA,UAAU;AAAA,MACR,OAAO;AAAA,MACP,aAAa;AAAA,MACb,MAAM;AAAA,IACR;AAAA,IACA,UAAU;AAAA,MACR,OAAO;AAAA,MACP,SAAS;AAAA,MACT,aAAa;AAAA,MACb,MAAM;AAAA,IACR;AAAA,EACF,CAAC,EAAE;AAEH,QAAM,OAAO,MAAM,aAAa,IAAI;AAEpC,QAAM,KAAK,MAAM,mBAAmB,MAAkB;AAAA,IACpD,0BAA0B,KAAK;AAAA,IAC/B,UAAU,KAAK;AAAA,EACjB,CAAC;AAED,MAAI,OAAO,KAAK,YAAY,UAAU;AACpC,cAAM,gCAAU,qBAAQ,QAAQ,IAAI,GAAG,KAAK,OAAO,GAAG,IAAI;AAAA,MACxD,UAAU;AAAA,IACZ,CAAC;AAAA,EACH,OAAO;AACL,YAAQ,IAAI,EAAE;AAAA,EAChB;AACF;AAEA,KAAK,EAAE,MAAM,CAAC,UAAU;AACtB,UAAQ,MAAM,KAAK;AACnB,UAAQ,KAAK,CAAC;AAChB,CAAC;", + "names": ["import_promises", "openapiTS", "yargs"] +} diff --git a/build/cli.d.ts b/build/cli.d.ts new file mode 100644 index 0000000..b798801 --- /dev/null +++ b/build/cli.d.ts @@ -0,0 +1,2 @@ +#!/usr/bin/env node +export {}; diff --git a/build/cli.mjs b/build/cli.mjs new file mode 100755 index 0000000..c838e95 --- /dev/null +++ b/build/cli.mjs @@ -0,0 +1,187 @@ +#!/usr/bin/env node + +// src/cli.ts +import { writeFile } from "fs/promises"; +import { resolve } from "path"; +import yargs from "yargs"; + +// src/index.ts +import { readFile } from "fs/promises"; +import openapiTS from "openapi-typescript"; +import { z } from "zod"; +var DirectusAuthResponse = z.object({ + data: z.object({ + access_token: z.string(), + expires: z.number().int(), + refresh_token: z.string() + }) +}); +var readSpecFile = async (options) => { + if (typeof options.specFile === `string`) { + return JSON.parse( + await readFile(options.specFile, { encoding: `utf-8` }) + ); + } + if (typeof options.host !== `string`) { + throw new Error(`Either inputFile or inputUrl must be specified`); + } + if (typeof options.email !== `string`) { + throw new Error(`email must be specified`); + } + if (typeof options.password !== `string`) { + throw new Error(`password must be specified`); + } + const { + data: { access_token } + } = await fetch(new URL(`/auth/login`, options.host), { + body: JSON.stringify({ + email: options.email, + password: options.password + }), + headers: { + "Content-Type": `application/json` + }, + method: `POST` + }).then((response) => response.json()).then((json) => DirectusAuthResponse.parse(json)); + return await fetch(new URL(`/server/specs/oas`, options.host), { + headers: { + "Authorization": `Bearer ${access_token}`, + "Content-Type": `application/json` + } + }).then((response) => response.json()); +}; +var validIdentifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; +var generateTypeScript = async (spec, { includeSystemCollections, typeName }) => { + if (!validIdentifier.test(typeName)) { + throw new Error(`Invalid type name: ${typeName}`); + } + let source = await openapiTS(spec); + source += ` + +export type ${typeName} = { +`; + const collections = {}; + if (spec.paths) { + for (const [path, pathItem] of Object.entries(spec.paths)) { + const collectionPathPattern = /^\/items\/(?[a-zA-Z0-9_]+)$/; + const collection = collectionPathPattern.exec(path)?.groups?.[`collection`]; + if (typeof collection !== `string` || collection.length === 0) { + continue; + } + if (`get` in pathItem && `responses` in pathItem.get && `200` in pathItem.get.responses && `content` in pathItem.get.responses[`200`] && `application/json` in pathItem.get.responses[`200`].content && `schema` in pathItem.get.responses[`200`].content[`application/json`] && `properties` in pathItem.get.responses[`200`].content[`application/json`].schema && `data` in pathItem.get.responses[`200`].content[`application/json`].schema.properties && `items` in pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`] && `$ref` in pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`].items) { + const $ref = pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`].items.$ref; + const refPattern = /^#\/components\/schemas\/(?[a-zA-Z0-9_]+)$/; + const ref = refPattern.exec($ref)?.groups?.[`ref`]; + if (typeof ref !== `string` || ref.length === 0) { + continue; + } + if (!collections[collection]) { + collections[collection] = `components["schemas"]["${ref}"][]`; + } + } + } + } + const relationshipPathPattern = /^\/relations\/(?[a-zA-Z0-9_]+)$/; + for (const [path, pathItem] of Object.entries(spec.paths ?? {})) { + const relation = relationshipPathPattern.exec(path)?.groups?.[`relation`]; + if (typeof relation !== `string` || relation.length === 0) { + continue; + } + if (`get` in pathItem && `responses` in pathItem.get && `200` in pathItem.get.responses && `content` in pathItem.get.responses[`200`] && `application/json` in pathItem.get.responses[`200`].content && `schema` in pathItem.get.responses[`200`].content[`application/json`] && `properties` in pathItem.get.responses[`200`].content[`application/json`].schema && `data` in pathItem.get.responses[`200`].content[`application/json`].schema.properties && `items` in pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`] && `$ref` in pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`].items) { + const $ref = pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`].items.$ref; + const refPattern = /^#\/components\/schemas\/(?[a-zA-Z0-9_]+)$/; + const ref = refPattern.exec($ref)?.groups?.[`ref`]; + if (typeof ref !== `string` || ref.length === 0) { + continue; + } + if (!collections[relation]) { + collections[relation] = `components["schemas"]["${ref}"][]`; + } + } + } + if (spec.components && spec.components.schemas && includeSystemCollections) { + for (const [schema_key, schema_value] of Object.entries( + spec.components.schemas + )) { + const x_collection = schema_value[`x-collection`]; + if (typeof x_collection === `string` && x_collection.length > 0) { + if (!collections[x_collection]) { + collections[x_collection] = `components["schemas"]["${schema_key}"]`; + } + } + } + } + for (const [collectionName, typeDef] of Object.entries(collections)) { + source += ` ${collectionName}: ${typeDef}; +`; + } + source += `}; +`; + const toPascalCase = (str) => str.replace(/[_\- ]+/g, ` `).split(` `).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(``); + for (const [collectionName, typeDef] of Object.entries(collections)) { + const pascalCaseName = toPascalCase(collectionName); + source += `export type ${pascalCaseName} = ${typeDef}; +`; + } + return source; +}; + +// src/cli.ts +var main = async () => { + const argv = await yargs.options({ + email: { + alias: `e`, + description: `Email address`, + type: `string` + }, + host: { + alias: `h`, + description: `Remote host`, + type: `string` + }, + includeSystemCollections: { + alias: `s`, + default: false, + description: `Include system collections`, + type: `boolean` + }, + outFile: { + alias: `o`, + description: `Output file`, + type: `string` + }, + password: { + alias: `p`, + description: `Password`, + type: `string` + }, + specFile: { + alias: `i`, + description: `Input spec file`, + type: `string` + }, + typeName: { + alias: `t`, + default: `Schema`, + description: `Type name`, + type: `string` + } + }).argv; + const spec = await readSpecFile(argv); + const ts = await generateTypeScript(spec, { + includeSystemCollections: argv.includeSystemCollections, + typeName: argv.typeName + }); + if (typeof argv.outFile === `string`) { + await writeFile(resolve(process.cwd(), argv.outFile), ts, { + encoding: `utf-8` + }); + } else { + console.log(ts); + } +}; +main().catch((error) => { + console.error(error); + process.exit(1); +}); +//# sourceMappingURL=cli.mjs.map diff --git a/build/cli.mjs.map b/build/cli.mjs.map new file mode 100644 index 0000000..5e83ee1 --- /dev/null +++ b/build/cli.mjs.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../src/cli.ts", "../src/index.ts"], + "sourcesContent": ["#!/usr/bin/env node\n\nimport { writeFile } from \"fs/promises\";\nimport { resolve } from \"path\";\n\nimport yargs from \"yargs\";\nimport type { OpenAPI3 } from \"openapi-typescript\";\n\nimport { generateTypeScript, readSpecFile } from \".\";\n\nconst main = async (): Promise => {\n const argv = await yargs.options({\n email: {\n alias: `e`,\n description: `Email address`,\n type: `string`,\n },\n host: {\n alias: `h`,\n description: `Remote host`,\n type: `string`,\n },\n includeSystemCollections: {\n alias: `s`,\n default: false,\n description: `Include system collections`,\n type: `boolean`,\n },\n outFile: {\n alias: `o`,\n description: `Output file`,\n type: `string`,\n },\n password: {\n alias: `p`,\n description: `Password`,\n type: `string`,\n },\n specFile: {\n alias: `i`,\n description: `Input spec file`,\n type: `string`,\n },\n typeName: {\n alias: `t`,\n default: `Schema`,\n description: `Type name`,\n type: `string`,\n },\n }).argv;\n\n const spec = await readSpecFile(argv);\n\n const ts = await generateTypeScript(spec as OpenAPI3, {\n includeSystemCollections: argv.includeSystemCollections,\n typeName: argv.typeName,\n });\n\n if (typeof argv.outFile === `string`) {\n await writeFile(resolve(process.cwd(), argv.outFile), ts, {\n encoding: `utf-8`,\n });\n } else {\n console.log(ts);\n }\n};\n\nmain().catch((error) => {\n console.error(error);\n process.exit(1);\n});\n", "import { readFile } from \"fs/promises\";\n\nimport type { OpenAPI3 } from \"openapi-typescript\";\nimport openapiTS from \"openapi-typescript\";\nimport { z } from \"zod\";\n\ntype ReadSpecFileOptions = {\n readonly specFile?: undefined | string;\n readonly host?: undefined | string;\n readonly email?: undefined | string;\n readonly password?: undefined | string;\n};\n\nconst DirectusAuthResponse = z.object({\n data: z.object({\n access_token: z.string(),\n expires: z.number().int(),\n refresh_token: z.string(),\n }),\n});\n\nexport const readSpecFile = async (\n options: ReadSpecFileOptions,\n): Promise => {\n if (typeof options.specFile === `string`) {\n return JSON.parse(\n await readFile(options.specFile, { encoding: `utf-8` }),\n ) as unknown;\n }\n\n if (typeof options.host !== `string`) {\n throw new Error(`Either inputFile or inputUrl must be specified`);\n }\n if (typeof options.email !== `string`) {\n throw new Error(`email must be specified`);\n }\n if (typeof options.password !== `string`) {\n throw new Error(`password must be specified`);\n }\n\n const {\n data: { access_token },\n } = await fetch(new URL(`/auth/login`, options.host), {\n body: JSON.stringify({\n email: options.email,\n password: options.password,\n }),\n headers: {\n \"Content-Type\": `application/json`,\n },\n method: `POST`,\n })\n .then((response) => response.json())\n .then((json) => DirectusAuthResponse.parse(json));\n\n return (await fetch(new URL(`/server/specs/oas`, options.host), {\n headers: {\n \"Authorization\": `Bearer ${access_token}`,\n \"Content-Type\": `application/json`,\n },\n }).then((response) => response.json())) as unknown;\n};\n\ntype GenerateTypeScriptOptions = {\n readonly includeSystemCollections?: boolean;\n readonly typeName: string;\n};\n\nconst validIdentifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;\n\nexport const generateTypeScript = async (\n spec: OpenAPI3,\n { includeSystemCollections, typeName }: GenerateTypeScriptOptions,\n): Promise => {\n if (!validIdentifier.test(typeName)) {\n throw new Error(`Invalid type name: ${typeName}`);\n }\n\n let source = await openapiTS(spec);\n\n source += `\\n\\nexport type ${typeName} = {\\n`;\n\n // Keep a record of discovered collections to avoid duplicates\n const collections: Record = {};\n\n if (spec.paths) {\n for (const [path, pathItem] of Object.entries(spec.paths)) {\n const collectionPathPattern = /^\\/items\\/(?[a-zA-Z0-9_]+)$/;\n const collection =\n collectionPathPattern.exec(path)?.groups?.[`collection`];\n if (typeof collection !== `string` || collection.length === 0) {\n continue;\n }\n if (\n `get` in pathItem &&\n `responses` in pathItem.get &&\n `200` in pathItem.get.responses &&\n `content` in pathItem.get.responses[`200`] &&\n `application/json` in pathItem.get.responses[`200`].content &&\n `schema` in pathItem.get.responses[`200`].content[`application/json`] &&\n `properties` in\n pathItem.get.responses[`200`].content[`application/json`].schema &&\n `data` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties &&\n `items` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`] &&\n `$ref` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`].items\n ) {\n const $ref =\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`].items.$ref;\n const refPattern = /^#\\/components\\/schemas\\/(?[a-zA-Z0-9_]+)$/;\n const ref = refPattern.exec($ref)?.groups?.[`ref`];\n if (typeof ref !== `string` || ref.length === 0) {\n continue;\n }\n // Instead of adding directly to source, store in collections\n if (!collections[collection]) {\n collections[collection] = `components[\"schemas\"][\"${ref}\"][]`;\n }\n }\n }\n }\n\n // fixing relationships\n const relationshipPathPattern = /^\\/relations\\/(?[a-zA-Z0-9_]+)$/;\n for (const [path, pathItem] of Object.entries(spec.paths ?? {})) {\n const relation = relationshipPathPattern.exec(path)?.groups?.[`relation`];\n if (typeof relation !== `string` || relation.length === 0) {\n continue;\n }\n if (\n `get` in pathItem &&\n `responses` in pathItem.get &&\n `200` in pathItem.get.responses &&\n `content` in pathItem.get.responses[`200`] &&\n `application/json` in pathItem.get.responses[`200`].content &&\n `schema` in pathItem.get.responses[`200`].content[`application/json`] &&\n `properties` in\n pathItem.get.responses[`200`].content[`application/json`].schema &&\n `data` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties &&\n `items` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`] &&\n `$ref` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`].items\n ) {\n const $ref =\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`].items.$ref;\n const refPattern = /^#\\/components\\/schemas\\/(?[a-zA-Z0-9_]+)$/;\n const ref = refPattern.exec($ref)?.groups?.[`ref`];\n if (typeof ref !== `string` || ref.length === 0) {\n continue;\n }\n // Add relationship to collections\n if (!collections[relation]) {\n collections[relation] = `components[\"schemas\"][\"${ref}\"][]`;\n }\n }\n }\n\n // Add directus system collections if requested\n if (spec.components && spec.components.schemas && includeSystemCollections) {\n for (const [schema_key, schema_value] of Object.entries(\n spec.components.schemas,\n )) {\n const x_collection = (schema_value as Record)[\n `x-collection`\n ] as string | undefined;\n if (typeof x_collection === `string` && x_collection.length > 0) {\n // Only add if not already present\n if (!collections[x_collection]) {\n collections[x_collection] = `components[\"schemas\"][\"${schema_key}\"]`;\n }\n }\n }\n }\n\n // After gathering all collections, write them out once\n for (const [collectionName, typeDef] of Object.entries(collections)) {\n source += ` ${collectionName}: ${typeDef};\\n`;\n }\n\n source += `};\\n`;\n\n const toPascalCase = (str: string): string =>\n str\n .replace(/[_\\- ]+/g, ` `)\n .split(` `)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(``);\n\n // Iterate over each collection to create individual export types\n for (const [collectionName, typeDef] of Object.entries(collections)) {\n const pascalCaseName = toPascalCase(collectionName);\n source += `export type ${pascalCaseName} = ${typeDef};\\n`;\n }\n\n return source;\n};\n"], + "mappings": "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", + "names": [] +} diff --git a/build/index.cjs b/build/index.cjs new file mode 100644 index 0000000..02d057d --- /dev/null +++ b/build/index.cjs @@ -0,0 +1,156 @@ +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// src/index.ts +var src_exports = {}; +__export(src_exports, { + generateTypeScript: () => generateTypeScript, + readSpecFile: () => readSpecFile +}); +module.exports = __toCommonJS(src_exports); +var import_promises = require("fs/promises"); +var import_openapi_typescript = __toESM(require("openapi-typescript"), 1); +var import_zod = require("zod"); +var DirectusAuthResponse = import_zod.z.object({ + data: import_zod.z.object({ + access_token: import_zod.z.string(), + expires: import_zod.z.number().int(), + refresh_token: import_zod.z.string() + }) +}); +var readSpecFile = async (options) => { + if (typeof options.specFile === `string`) { + return JSON.parse( + await (0, import_promises.readFile)(options.specFile, { encoding: `utf-8` }) + ); + } + if (typeof options.host !== `string`) { + throw new Error(`Either inputFile or inputUrl must be specified`); + } + if (typeof options.email !== `string`) { + throw new Error(`email must be specified`); + } + if (typeof options.password !== `string`) { + throw new Error(`password must be specified`); + } + const { + data: { access_token } + } = await fetch(new URL(`/auth/login`, options.host), { + body: JSON.stringify({ + email: options.email, + password: options.password + }), + headers: { + "Content-Type": `application/json` + }, + method: `POST` + }).then((response) => response.json()).then((json) => DirectusAuthResponse.parse(json)); + return await fetch(new URL(`/server/specs/oas`, options.host), { + headers: { + "Authorization": `Bearer ${access_token}`, + "Content-Type": `application/json` + } + }).then((response) => response.json()); +}; +var validIdentifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; +var generateTypeScript = async (spec, { includeSystemCollections, typeName }) => { + if (!validIdentifier.test(typeName)) { + throw new Error(`Invalid type name: ${typeName}`); + } + let source = await (0, import_openapi_typescript.default)(spec); + source += ` + +export type ${typeName} = { +`; + const collections = {}; + if (spec.paths) { + for (const [path, pathItem] of Object.entries(spec.paths)) { + const collectionPathPattern = /^\/items\/(?[a-zA-Z0-9_]+)$/; + const collection = collectionPathPattern.exec(path)?.groups?.[`collection`]; + if (typeof collection !== `string` || collection.length === 0) { + continue; + } + if (`get` in pathItem && `responses` in pathItem.get && `200` in pathItem.get.responses && `content` in pathItem.get.responses[`200`] && `application/json` in pathItem.get.responses[`200`].content && `schema` in pathItem.get.responses[`200`].content[`application/json`] && `properties` in pathItem.get.responses[`200`].content[`application/json`].schema && `data` in pathItem.get.responses[`200`].content[`application/json`].schema.properties && `items` in pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`] && `$ref` in pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`].items) { + const $ref = pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`].items.$ref; + const refPattern = /^#\/components\/schemas\/(?[a-zA-Z0-9_]+)$/; + const ref = refPattern.exec($ref)?.groups?.[`ref`]; + if (typeof ref !== `string` || ref.length === 0) { + continue; + } + if (!collections[collection]) { + collections[collection] = `components["schemas"]["${ref}"][]`; + } + } + } + } + const relationshipPathPattern = /^\/relations\/(?[a-zA-Z0-9_]+)$/; + for (const [path, pathItem] of Object.entries(spec.paths ?? {})) { + const relation = relationshipPathPattern.exec(path)?.groups?.[`relation`]; + if (typeof relation !== `string` || relation.length === 0) { + continue; + } + if (`get` in pathItem && `responses` in pathItem.get && `200` in pathItem.get.responses && `content` in pathItem.get.responses[`200`] && `application/json` in pathItem.get.responses[`200`].content && `schema` in pathItem.get.responses[`200`].content[`application/json`] && `properties` in pathItem.get.responses[`200`].content[`application/json`].schema && `data` in pathItem.get.responses[`200`].content[`application/json`].schema.properties && `items` in pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`] && `$ref` in pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`].items) { + const $ref = pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`].items.$ref; + const refPattern = /^#\/components\/schemas\/(?[a-zA-Z0-9_]+)$/; + const ref = refPattern.exec($ref)?.groups?.[`ref`]; + if (typeof ref !== `string` || ref.length === 0) { + continue; + } + if (!collections[relation]) { + collections[relation] = `components["schemas"]["${ref}"][]`; + } + } + } + if (spec.components && spec.components.schemas && includeSystemCollections) { + for (const [schema_key, schema_value] of Object.entries( + spec.components.schemas + )) { + const x_collection = schema_value[`x-collection`]; + if (typeof x_collection === `string` && x_collection.length > 0) { + if (!collections[x_collection]) { + collections[x_collection] = `components["schemas"]["${schema_key}"]`; + } + } + } + } + for (const [collectionName, typeDef] of Object.entries(collections)) { + source += ` ${collectionName}: ${typeDef}; +`; + } + source += `}; +`; + const toPascalCase = (str) => str.replace(/[_\- ]+/g, ` `).split(` `).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(``); + for (const [collectionName, typeDef] of Object.entries(collections)) { + const pascalCaseName = toPascalCase(collectionName); + source += `export type ${pascalCaseName} = ${typeDef}; +`; + } + return source; +}; +//# sourceMappingURL=index.cjs.map diff --git a/build/index.cjs.map b/build/index.cjs.map new file mode 100644 index 0000000..09fef32 --- /dev/null +++ b/build/index.cjs.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../src/index.ts"], + "sourcesContent": ["import { readFile } from \"fs/promises\";\n\nimport type { OpenAPI3 } from \"openapi-typescript\";\nimport openapiTS from \"openapi-typescript\";\nimport { z } from \"zod\";\n\ntype ReadSpecFileOptions = {\n readonly specFile?: undefined | string;\n readonly host?: undefined | string;\n readonly email?: undefined | string;\n readonly password?: undefined | string;\n};\n\nconst DirectusAuthResponse = z.object({\n data: z.object({\n access_token: z.string(),\n expires: z.number().int(),\n refresh_token: z.string(),\n }),\n});\n\nexport const readSpecFile = async (\n options: ReadSpecFileOptions,\n): Promise => {\n if (typeof options.specFile === `string`) {\n return JSON.parse(\n await readFile(options.specFile, { encoding: `utf-8` }),\n ) as unknown;\n }\n\n if (typeof options.host !== `string`) {\n throw new Error(`Either inputFile or inputUrl must be specified`);\n }\n if (typeof options.email !== `string`) {\n throw new Error(`email must be specified`);\n }\n if (typeof options.password !== `string`) {\n throw new Error(`password must be specified`);\n }\n\n const {\n data: { access_token },\n } = await fetch(new URL(`/auth/login`, options.host), {\n body: JSON.stringify({\n email: options.email,\n password: options.password,\n }),\n headers: {\n \"Content-Type\": `application/json`,\n },\n method: `POST`,\n })\n .then((response) => response.json())\n .then((json) => DirectusAuthResponse.parse(json));\n\n return (await fetch(new URL(`/server/specs/oas`, options.host), {\n headers: {\n \"Authorization\": `Bearer ${access_token}`,\n \"Content-Type\": `application/json`,\n },\n }).then((response) => response.json())) as unknown;\n};\n\ntype GenerateTypeScriptOptions = {\n readonly includeSystemCollections?: boolean;\n readonly typeName: string;\n};\n\nconst validIdentifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;\n\nexport const generateTypeScript = async (\n spec: OpenAPI3,\n { includeSystemCollections, typeName }: GenerateTypeScriptOptions,\n): Promise => {\n if (!validIdentifier.test(typeName)) {\n throw new Error(`Invalid type name: ${typeName}`);\n }\n\n let source = await openapiTS(spec);\n\n source += `\\n\\nexport type ${typeName} = {\\n`;\n\n // Keep a record of discovered collections to avoid duplicates\n const collections: Record = {};\n\n if (spec.paths) {\n for (const [path, pathItem] of Object.entries(spec.paths)) {\n const collectionPathPattern = /^\\/items\\/(?[a-zA-Z0-9_]+)$/;\n const collection =\n collectionPathPattern.exec(path)?.groups?.[`collection`];\n if (typeof collection !== `string` || collection.length === 0) {\n continue;\n }\n if (\n `get` in pathItem &&\n `responses` in pathItem.get &&\n `200` in pathItem.get.responses &&\n `content` in pathItem.get.responses[`200`] &&\n `application/json` in pathItem.get.responses[`200`].content &&\n `schema` in pathItem.get.responses[`200`].content[`application/json`] &&\n `properties` in\n pathItem.get.responses[`200`].content[`application/json`].schema &&\n `data` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties &&\n `items` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`] &&\n `$ref` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`].items\n ) {\n const $ref =\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`].items.$ref;\n const refPattern = /^#\\/components\\/schemas\\/(?[a-zA-Z0-9_]+)$/;\n const ref = refPattern.exec($ref)?.groups?.[`ref`];\n if (typeof ref !== `string` || ref.length === 0) {\n continue;\n }\n // Instead of adding directly to source, store in collections\n if (!collections[collection]) {\n collections[collection] = `components[\"schemas\"][\"${ref}\"][]`;\n }\n }\n }\n }\n\n // fixing relationships\n const relationshipPathPattern = /^\\/relations\\/(?[a-zA-Z0-9_]+)$/;\n for (const [path, pathItem] of Object.entries(spec.paths ?? {})) {\n const relation = relationshipPathPattern.exec(path)?.groups?.[`relation`];\n if (typeof relation !== `string` || relation.length === 0) {\n continue;\n }\n if (\n `get` in pathItem &&\n `responses` in pathItem.get &&\n `200` in pathItem.get.responses &&\n `content` in pathItem.get.responses[`200`] &&\n `application/json` in pathItem.get.responses[`200`].content &&\n `schema` in pathItem.get.responses[`200`].content[`application/json`] &&\n `properties` in\n pathItem.get.responses[`200`].content[`application/json`].schema &&\n `data` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties &&\n `items` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`] &&\n `$ref` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`].items\n ) {\n const $ref =\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`].items.$ref;\n const refPattern = /^#\\/components\\/schemas\\/(?[a-zA-Z0-9_]+)$/;\n const ref = refPattern.exec($ref)?.groups?.[`ref`];\n if (typeof ref !== `string` || ref.length === 0) {\n continue;\n }\n // Add relationship to collections\n if (!collections[relation]) {\n collections[relation] = `components[\"schemas\"][\"${ref}\"][]`;\n }\n }\n }\n\n // Add directus system collections if requested\n if (spec.components && spec.components.schemas && includeSystemCollections) {\n for (const [schema_key, schema_value] of Object.entries(\n spec.components.schemas,\n )) {\n const x_collection = (schema_value as Record)[\n `x-collection`\n ] as string | undefined;\n if (typeof x_collection === `string` && x_collection.length > 0) {\n // Only add if not already present\n if (!collections[x_collection]) {\n collections[x_collection] = `components[\"schemas\"][\"${schema_key}\"]`;\n }\n }\n }\n }\n\n // After gathering all collections, write them out once\n for (const [collectionName, typeDef] of Object.entries(collections)) {\n source += ` ${collectionName}: ${typeDef};\\n`;\n }\n\n source += `};\\n`;\n\n const toPascalCase = (str: string): string =>\n str\n .replace(/[_\\- ]+/g, ` `)\n .split(` `)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(``);\n\n // Iterate over each collection to create individual export types\n for (const [collectionName, typeDef] of Object.entries(collections)) {\n const pascalCaseName = toPascalCase(collectionName);\n source += `export type ${pascalCaseName} = ${typeDef};\\n`;\n }\n\n return source;\n};\n"], + "mappings": "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", + "names": ["openapiTS"] +} diff --git a/build/index.d.ts b/build/index.d.ts new file mode 100644 index 0000000..9f9541f --- /dev/null +++ b/build/index.d.ts @@ -0,0 +1,14 @@ +import type { OpenAPI3 } from "openapi-typescript"; +type ReadSpecFileOptions = { + readonly specFile?: undefined | string; + readonly host?: undefined | string; + readonly email?: undefined | string; + readonly password?: undefined | string; +}; +export declare const readSpecFile: (options: ReadSpecFileOptions) => Promise; +type GenerateTypeScriptOptions = { + readonly includeSystemCollections?: boolean; + readonly typeName: string; +}; +export declare const generateTypeScript: (spec: OpenAPI3, { includeSystemCollections, typeName }: GenerateTypeScriptOptions) => Promise; +export {}; diff --git a/build/index.mjs b/build/index.mjs new file mode 100644 index 0000000..fbedcd3 --- /dev/null +++ b/build/index.mjs @@ -0,0 +1,125 @@ +// src/index.ts +import { readFile } from "fs/promises"; +import openapiTS from "openapi-typescript"; +import { z } from "zod"; +var DirectusAuthResponse = z.object({ + data: z.object({ + access_token: z.string(), + expires: z.number().int(), + refresh_token: z.string() + }) +}); +var readSpecFile = async (options) => { + if (typeof options.specFile === `string`) { + return JSON.parse( + await readFile(options.specFile, { encoding: `utf-8` }) + ); + } + if (typeof options.host !== `string`) { + throw new Error(`Either inputFile or inputUrl must be specified`); + } + if (typeof options.email !== `string`) { + throw new Error(`email must be specified`); + } + if (typeof options.password !== `string`) { + throw new Error(`password must be specified`); + } + const { + data: { access_token } + } = await fetch(new URL(`/auth/login`, options.host), { + body: JSON.stringify({ + email: options.email, + password: options.password + }), + headers: { + "Content-Type": `application/json` + }, + method: `POST` + }).then((response) => response.json()).then((json) => DirectusAuthResponse.parse(json)); + return await fetch(new URL(`/server/specs/oas`, options.host), { + headers: { + "Authorization": `Bearer ${access_token}`, + "Content-Type": `application/json` + } + }).then((response) => response.json()); +}; +var validIdentifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; +var generateTypeScript = async (spec, { includeSystemCollections, typeName }) => { + if (!validIdentifier.test(typeName)) { + throw new Error(`Invalid type name: ${typeName}`); + } + let source = await openapiTS(spec); + source += ` + +export type ${typeName} = { +`; + const collections = {}; + if (spec.paths) { + for (const [path, pathItem] of Object.entries(spec.paths)) { + const collectionPathPattern = /^\/items\/(?[a-zA-Z0-9_]+)$/; + const collection = collectionPathPattern.exec(path)?.groups?.[`collection`]; + if (typeof collection !== `string` || collection.length === 0) { + continue; + } + if (`get` in pathItem && `responses` in pathItem.get && `200` in pathItem.get.responses && `content` in pathItem.get.responses[`200`] && `application/json` in pathItem.get.responses[`200`].content && `schema` in pathItem.get.responses[`200`].content[`application/json`] && `properties` in pathItem.get.responses[`200`].content[`application/json`].schema && `data` in pathItem.get.responses[`200`].content[`application/json`].schema.properties && `items` in pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`] && `$ref` in pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`].items) { + const $ref = pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`].items.$ref; + const refPattern = /^#\/components\/schemas\/(?[a-zA-Z0-9_]+)$/; + const ref = refPattern.exec($ref)?.groups?.[`ref`]; + if (typeof ref !== `string` || ref.length === 0) { + continue; + } + if (!collections[collection]) { + collections[collection] = `components["schemas"]["${ref}"][]`; + } + } + } + } + const relationshipPathPattern = /^\/relations\/(?[a-zA-Z0-9_]+)$/; + for (const [path, pathItem] of Object.entries(spec.paths ?? {})) { + const relation = relationshipPathPattern.exec(path)?.groups?.[`relation`]; + if (typeof relation !== `string` || relation.length === 0) { + continue; + } + if (`get` in pathItem && `responses` in pathItem.get && `200` in pathItem.get.responses && `content` in pathItem.get.responses[`200`] && `application/json` in pathItem.get.responses[`200`].content && `schema` in pathItem.get.responses[`200`].content[`application/json`] && `properties` in pathItem.get.responses[`200`].content[`application/json`].schema && `data` in pathItem.get.responses[`200`].content[`application/json`].schema.properties && `items` in pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`] && `$ref` in pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`].items) { + const $ref = pathItem.get.responses[`200`].content[`application/json`].schema.properties[`data`].items.$ref; + const refPattern = /^#\/components\/schemas\/(?[a-zA-Z0-9_]+)$/; + const ref = refPattern.exec($ref)?.groups?.[`ref`]; + if (typeof ref !== `string` || ref.length === 0) { + continue; + } + if (!collections[relation]) { + collections[relation] = `components["schemas"]["${ref}"][]`; + } + } + } + if (spec.components && spec.components.schemas && includeSystemCollections) { + for (const [schema_key, schema_value] of Object.entries( + spec.components.schemas + )) { + const x_collection = schema_value[`x-collection`]; + if (typeof x_collection === `string` && x_collection.length > 0) { + if (!collections[x_collection]) { + collections[x_collection] = `components["schemas"]["${schema_key}"]`; + } + } + } + } + for (const [collectionName, typeDef] of Object.entries(collections)) { + source += ` ${collectionName}: ${typeDef}; +`; + } + source += `}; +`; + const toPascalCase = (str) => str.replace(/[_\- ]+/g, ` `).split(` `).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(``); + for (const [collectionName, typeDef] of Object.entries(collections)) { + const pascalCaseName = toPascalCase(collectionName); + source += `export type ${pascalCaseName} = ${typeDef}; +`; + } + return source; +}; +export { + generateTypeScript, + readSpecFile +}; +//# sourceMappingURL=index.mjs.map diff --git a/build/index.mjs.map b/build/index.mjs.map new file mode 100644 index 0000000..aa92806 --- /dev/null +++ b/build/index.mjs.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../src/index.ts"], + "sourcesContent": ["import { readFile } from \"fs/promises\";\n\nimport type { OpenAPI3 } from \"openapi-typescript\";\nimport openapiTS from \"openapi-typescript\";\nimport { z } from \"zod\";\n\ntype ReadSpecFileOptions = {\n readonly specFile?: undefined | string;\n readonly host?: undefined | string;\n readonly email?: undefined | string;\n readonly password?: undefined | string;\n};\n\nconst DirectusAuthResponse = z.object({\n data: z.object({\n access_token: z.string(),\n expires: z.number().int(),\n refresh_token: z.string(),\n }),\n});\n\nexport const readSpecFile = async (\n options: ReadSpecFileOptions,\n): Promise => {\n if (typeof options.specFile === `string`) {\n return JSON.parse(\n await readFile(options.specFile, { encoding: `utf-8` }),\n ) as unknown;\n }\n\n if (typeof options.host !== `string`) {\n throw new Error(`Either inputFile or inputUrl must be specified`);\n }\n if (typeof options.email !== `string`) {\n throw new Error(`email must be specified`);\n }\n if (typeof options.password !== `string`) {\n throw new Error(`password must be specified`);\n }\n\n const {\n data: { access_token },\n } = await fetch(new URL(`/auth/login`, options.host), {\n body: JSON.stringify({\n email: options.email,\n password: options.password,\n }),\n headers: {\n \"Content-Type\": `application/json`,\n },\n method: `POST`,\n })\n .then((response) => response.json())\n .then((json) => DirectusAuthResponse.parse(json));\n\n return (await fetch(new URL(`/server/specs/oas`, options.host), {\n headers: {\n \"Authorization\": `Bearer ${access_token}`,\n \"Content-Type\": `application/json`,\n },\n }).then((response) => response.json())) as unknown;\n};\n\ntype GenerateTypeScriptOptions = {\n readonly includeSystemCollections?: boolean;\n readonly typeName: string;\n};\n\nconst validIdentifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;\n\nexport const generateTypeScript = async (\n spec: OpenAPI3,\n { includeSystemCollections, typeName }: GenerateTypeScriptOptions,\n): Promise => {\n if (!validIdentifier.test(typeName)) {\n throw new Error(`Invalid type name: ${typeName}`);\n }\n\n let source = await openapiTS(spec);\n\n source += `\\n\\nexport type ${typeName} = {\\n`;\n\n // Keep a record of discovered collections to avoid duplicates\n const collections: Record = {};\n\n if (spec.paths) {\n for (const [path, pathItem] of Object.entries(spec.paths)) {\n const collectionPathPattern = /^\\/items\\/(?[a-zA-Z0-9_]+)$/;\n const collection =\n collectionPathPattern.exec(path)?.groups?.[`collection`];\n if (typeof collection !== `string` || collection.length === 0) {\n continue;\n }\n if (\n `get` in pathItem &&\n `responses` in pathItem.get &&\n `200` in pathItem.get.responses &&\n `content` in pathItem.get.responses[`200`] &&\n `application/json` in pathItem.get.responses[`200`].content &&\n `schema` in pathItem.get.responses[`200`].content[`application/json`] &&\n `properties` in\n pathItem.get.responses[`200`].content[`application/json`].schema &&\n `data` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties &&\n `items` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`] &&\n `$ref` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`].items\n ) {\n const $ref =\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`].items.$ref;\n const refPattern = /^#\\/components\\/schemas\\/(?[a-zA-Z0-9_]+)$/;\n const ref = refPattern.exec($ref)?.groups?.[`ref`];\n if (typeof ref !== `string` || ref.length === 0) {\n continue;\n }\n // Instead of adding directly to source, store in collections\n if (!collections[collection]) {\n collections[collection] = `components[\"schemas\"][\"${ref}\"][]`;\n }\n }\n }\n }\n\n // fixing relationships\n const relationshipPathPattern = /^\\/relations\\/(?[a-zA-Z0-9_]+)$/;\n for (const [path, pathItem] of Object.entries(spec.paths ?? {})) {\n const relation = relationshipPathPattern.exec(path)?.groups?.[`relation`];\n if (typeof relation !== `string` || relation.length === 0) {\n continue;\n }\n if (\n `get` in pathItem &&\n `responses` in pathItem.get &&\n `200` in pathItem.get.responses &&\n `content` in pathItem.get.responses[`200`] &&\n `application/json` in pathItem.get.responses[`200`].content &&\n `schema` in pathItem.get.responses[`200`].content[`application/json`] &&\n `properties` in\n pathItem.get.responses[`200`].content[`application/json`].schema &&\n `data` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties &&\n `items` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`] &&\n `$ref` in\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`].items\n ) {\n const $ref =\n pathItem.get.responses[`200`].content[`application/json`].schema\n .properties[`data`].items.$ref;\n const refPattern = /^#\\/components\\/schemas\\/(?[a-zA-Z0-9_]+)$/;\n const ref = refPattern.exec($ref)?.groups?.[`ref`];\n if (typeof ref !== `string` || ref.length === 0) {\n continue;\n }\n // Add relationship to collections\n if (!collections[relation]) {\n collections[relation] = `components[\"schemas\"][\"${ref}\"][]`;\n }\n }\n }\n\n // Add directus system collections if requested\n if (spec.components && spec.components.schemas && includeSystemCollections) {\n for (const [schema_key, schema_value] of Object.entries(\n spec.components.schemas,\n )) {\n const x_collection = (schema_value as Record)[\n `x-collection`\n ] as string | undefined;\n if (typeof x_collection === `string` && x_collection.length > 0) {\n // Only add if not already present\n if (!collections[x_collection]) {\n collections[x_collection] = `components[\"schemas\"][\"${schema_key}\"]`;\n }\n }\n }\n }\n\n // After gathering all collections, write them out once\n for (const [collectionName, typeDef] of Object.entries(collections)) {\n source += ` ${collectionName}: ${typeDef};\\n`;\n }\n\n source += `};\\n`;\n\n const toPascalCase = (str: string): string =>\n str\n .replace(/[_\\- ]+/g, ` `)\n .split(` `)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(``);\n\n // Iterate over each collection to create individual export types\n for (const [collectionName, typeDef] of Object.entries(collections)) {\n const pascalCaseName = toPascalCase(collectionName);\n source += `export type ${pascalCaseName} = ${typeDef};\\n`;\n }\n\n return source;\n};\n"], + "mappings": "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", + "names": [] +} diff --git a/package-lock.json b/package-lock.json index 4a0b913..f33a42e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "directus-typescript-gen", - "version": "1.0.0-dev", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "directus-typescript-gen", - "version": "1.0.0-dev", + "version": "1.1.0", "dependencies": { "openapi-typescript": "^6.7.0", "yargs": "^17.7.2", diff --git a/package.json b/package.json index 25d6215..f2cef2e 100644 --- a/package.json +++ b/package.json @@ -47,5 +47,5 @@ }, "type": "module", "types": "./build/index.d.ts", - "version": "1.0.0" -} \ No newline at end of file + "version": "1.3.1" +} diff --git a/src/cli.ts b/src/cli.ts index 63d6998..d433596 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -20,6 +20,12 @@ const main = async (): Promise => { description: `Remote host`, type: `string`, }, + includeSystemCollections: { + alias: `s`, + default: false, + description: `Include system collections`, + type: `boolean`, + }, outFile: { alias: `o`, description: `Output file`, @@ -45,7 +51,10 @@ const main = async (): Promise => { const spec = await readSpecFile(argv); + console.log(JSON.stringify(spec, null, 2)); // Add this for debugging + const ts = await generateTypeScript(spec as OpenAPI3, { + includeSystemCollections: argv.includeSystemCollections, typeName: argv.typeName, }); diff --git a/src/index.ts b/src/index.ts index 4b1e423..e61c212 100644 --- a/src/index.ts +++ b/src/index.ts @@ -61,7 +61,9 @@ export const readSpecFile = async ( }).then((response) => response.json())) as unknown; }; + type GenerateTypeScriptOptions = { + readonly includeSystemCollections?: boolean; readonly typeName: string; }; @@ -69,7 +71,7 @@ const validIdentifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; export const generateTypeScript = async ( spec: OpenAPI3, - { typeName }: GenerateTypeScriptOptions, + { includeSystemCollections, typeName }: GenerateTypeScriptOptions, ): Promise => { if (!validIdentifier.test(typeName)) { throw new Error(`Invalid type name: ${typeName}`); @@ -79,6 +81,9 @@ export const generateTypeScript = async ( source += `\n\nexport type ${typeName} = {\n`; + // Keep a record of discovered collections to avoid duplicates + const collections: Record = {}; + if (spec.paths) { for (const [path, pathItem] of Object.entries(spec.paths)) { const collectionPathPattern = /^\/items\/(?[a-zA-Z0-9_]+)$/; @@ -95,16 +100,16 @@ export const generateTypeScript = async ( `application/json` in pathItem.get.responses[`200`].content && `schema` in pathItem.get.responses[`200`].content[`application/json`] && `properties` in - pathItem.get.responses[`200`].content[`application/json`].schema && + Item.get.responses[`200`].content[`application/json`].schema && `data` in - pathItem.get.responses[`200`].content[`application/json`].schema - .properties && + Item.get.responses[`200`].content[`application/json`].schema + perties && `items` in - pathItem.get.responses[`200`].content[`application/json`].schema - .properties[`data`] && + Item.get.responses[`200`].content[`application/json`].schema + perties[`data`] && `$ref` in - pathItem.get.responses[`200`].content[`application/json`].schema - .properties[`data`].items + Item.get.responses[`200`].content[`application/json`].schema + perties[`data`].items ) { const $ref = pathItem.get.responses[`200`].content[`application/json`].schema @@ -114,12 +119,91 @@ export const generateTypeScript = async ( if (typeof ref !== `string` || ref.length === 0) { continue; } - source += ` ${collection}: components["schemas"]["${ref}"][];\n`; + // Instead of adding directly to source, store in collections + if (!collections[collection]) { + collections[collection] = `components["schemas"]["${ref}"][]`; + } } } } + // fixing relationships + const relationshipPathPattern = /^\/relations\/(?[a-zA-Z0-9_]+)$/; + for (const [path, pathItem] of Object.entries(spec.paths ?? {})) { + const relation = relationshipPathPattern.exec(path)?.groups?.[`relation`]; + if (typeof relation !== `string` || relation.length === 0) { + continue; + } + if ( + `get` in pathItem && + `responses` in pathItem.get && + `200` in pathItem.get.responses && + `content` in pathItem.get.responses[`200`] && + `application/json` in pathItem.get.responses[`200`].content && + `schema` in pathItem.get.responses[`200`].content[`application/json`] && + `properties` in + Item.get.responses[`200`].content[`application/json`].schema && + `data` in + Item.get.responses[`200`].content[`application/json`].schema + perties && + `items` in + Item.get.responses[`200`].content[`application/json`].schema + perties[`data`] && + `$ref` in + Item.get.responses[`200`].content[`application/json`].schema + perties[`data`].items + ) { + const $ref = + pathItem.get.responses[`200`].content[`application/json`].schema + .properties[`data`].items.$ref; + const refPattern = /^#\/components\/schemas\/(?[a-zA-Z0-9_]+)$/; + const ref = refPattern.exec($ref)?.groups?.[`ref`]; + if (typeof ref !== `string` || ref.length === 0) { + continue; + } + // Add relationship to collections + if (!collections[relation]) { + collections[relation] = `components["schemas"]["${ref}"][]`; + } + } + } + + // Add directus system collections if requested + if (spec.components && spec.components.schemas && includeSystemCollections) { + for (const [schema_key, schema_value] of Object.entries( + spec.components.schemas, + )) { + const x_collection = (schema_value as Record)[ + `x-collection` + ] as string | undefined; + if (typeof x_collection === `string` && x_collection.length > 0) { + // Only add if not already present + if (!collections[x_collection]) { + collections[x_collection] = `components["schemas"]["${schema_key}"]`; + } + } + } + } + + // After gathering all collections, write them out once + for (const [collectionName, typeDef] of Object.entries(collections)) { + source += ` ${collectionName}: ${typeDef};\n`; + } + source += `};\n`; + const toPascalCase = (str: string): string => + str + .replace(/[_\- ]+/g, ` `) + .split(` `) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(``); + + // Iterate over each collection to create individual export types + for (const [collectionName, typeDef] of Object.entries(collections)) { + const pascalCaseName = toPascalCase(collectionName); + source += `export type ${pascalCaseName} = ${typeDef};\n`; + } + return source; };