From f3129b54010189b3aa5eaafa43c3f933b981bde4 Mon Sep 17 00:00:00 2001 From: Stephen Gunn Date: Thu, 12 Dec 2024 19:14:49 -0600 Subject: [PATCH 1/8] added directus system collections under feature flag add --includeSystemCollections or -s to include the directus system collections to the output. This allows custom fields on collections like directus_users from causing type errors in the SDK. --- src/cli.ts | 7 +++++++ src/index.ts | 19 +++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 63d6998..1672bc2 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`, @@ -46,6 +52,7 @@ const main = async (): Promise => { const spec = await readSpecFile(argv); 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..f04ee56 100644 --- a/src/index.ts +++ b/src/index.ts @@ -55,13 +55,14 @@ export const readSpecFile = async ( return (await fetch(new URL(`/server/specs/oas`, options.host), { headers: { - "Authorization": `Bearer ${access_token}`, + Authorization: `Bearer ${access_token}`, "Content-Type": `application/json`, }, }).then((response) => response.json())) as unknown; }; type GenerateTypeScriptOptions = { + readonly includeSystemCollections?: boolean; readonly typeName: string; }; @@ -69,7 +70,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}`); @@ -119,6 +120,20 @@ export const generateTypeScript = async ( } } + // 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) { + source += ` ${x_collection}: components["schemas"]["${schema_key}"];\n`; + } + } + } + source += `};\n`; return source; From 07e2abaa031c279d6a102db910200eb68b17be9b Mon Sep 17 00:00:00 2001 From: Stephen Gunn Date: Thu, 12 Dec 2024 19:15:24 -0600 Subject: [PATCH 2/8] 1.1.0 --- package-lock.json | 4 ++-- package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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..1f4f66a 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.1.0" +} From 2c927af723874294f15cf297ce8e838a62e2eb06 Mon Sep 17 00:00:00 2001 From: Stephen Gunn Date: Thu, 12 Dec 2024 19:19:13 -0600 Subject: [PATCH 3/8] Updated the readme to include the --includeSystemCollections flag Made it easier to read the system collections info in readme changed the readme to follow original flow and added my flag to the end reverted small readme changes for clarity reverted small style changes my lsp setup decided to do on its own more formatting changes to match original fixed formatting to match original --- README.md | 4 ++++ src/index.ts | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) 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/src/index.ts b/src/index.ts index f04ee56..55667ec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -55,7 +55,7 @@ export const readSpecFile = async ( return (await fetch(new URL(`/server/specs/oas`, options.host), { headers: { - Authorization: `Bearer ${access_token}`, + "Authorization": `Bearer ${access_token}`, "Content-Type": `application/json`, }, }).then((response) => response.json())) as unknown; @@ -126,9 +126,9 @@ export const generateTypeScript = async ( spec.components.schemas, )) { const x_collection = (schema_value as Record)[ - "x-collection" + `x-collection` ] as string | undefined; - if (typeof x_collection === "string" && x_collection.length > 0) { + if (typeof x_collection === `string` && x_collection.length > 0) { source += ` ${x_collection}: components["schemas"]["${schema_key}"];\n`; } } From ba024d8733c394dc7c00315af92415fd02dba9c5 Mon Sep 17 00:00:00 2001 From: Stephen Gunn Date: Thu, 12 Dec 2024 20:10:08 -0600 Subject: [PATCH 4/8] Added logic to prevent duplication of types when adding system collections to the Schema type --- src/index.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 55667ec..476b0ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -80,6 +80,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_]+)$/; @@ -115,7 +118,10 @@ 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}"][]`; + } } } } @@ -129,11 +135,19 @@ export const generateTypeScript = async ( `x-collection` ] as string | undefined; if (typeof x_collection === `string` && x_collection.length > 0) { - source += ` ${x_collection}: components["schemas"]["${schema_key}"];\n`; + // 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`; return source; From e53ab28c3f00d27d7208df51d14f0898d1d224c4 Mon Sep 17 00:00:00 2001 From: Stephen Gunn Date: Wed, 18 Dec 2024 10:01:29 -0600 Subject: [PATCH 5/8] adding build files to my repo so i can install from github --- .gitignore | 2 +- build/cli.cjs | 186 ++++++++++++++++++++++++++++++++++++++++++++ build/cli.cjs.map | 7 ++ build/cli.d.ts | 2 + build/cli.mjs | 163 ++++++++++++++++++++++++++++++++++++++ build/cli.mjs.map | 7 ++ build/index.cjs | 132 +++++++++++++++++++++++++++++++ build/index.cjs.map | 7 ++ build/index.d.ts | 14 ++++ build/index.mjs | 101 ++++++++++++++++++++++++ build/index.mjs.map | 7 ++ 11 files changed, 627 insertions(+), 1 deletion(-) create mode 100755 build/cli.cjs create mode 100644 build/cli.cjs.map create mode 100644 build/cli.d.ts create mode 100755 build/cli.mjs create mode 100644 build/cli.mjs.map create mode 100644 build/index.cjs create mode 100644 build/index.cjs.map create mode 100644 build/index.d.ts create mode 100644 build/index.mjs create mode 100644 build/index.mjs.map 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/build/cli.cjs b/build/cli.cjs new file mode 100755 index 0000000..fd06cf8 --- /dev/null +++ b/build/cli.cjs @@ -0,0 +1,186 @@ +#!/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}"][]`; + } + } + } + } + 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 += `}; +`; + 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..147b23d --- /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 // 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 return source;\n};\n"], + "mappings": "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", + "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..3c27a6a --- /dev/null +++ b/build/cli.mjs @@ -0,0 +1,163 @@ +#!/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}"][]`; + } + } + } + } + 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 += `}; +`; + 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..c570b6f --- /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 // 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 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..49d887b --- /dev/null +++ b/build/index.cjs @@ -0,0 +1,132 @@ +"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}"][]`; + } + } + } + } + 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 += `}; +`; + return source; +}; +//# sourceMappingURL=index.cjs.map diff --git a/build/index.cjs.map b/build/index.cjs.map new file mode 100644 index 0000000..bea0ad4 --- /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 // 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 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..0c426b1 --- /dev/null +++ b/build/index.mjs @@ -0,0 +1,101 @@ +// 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}"][]`; + } + } + } + } + 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 += `}; +`; + 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..522b6b6 --- /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 // 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 return source;\n};\n"], + "mappings": "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", + "names": [] +} From c5c06b87b6541eefd418208fcb811442d5043c7e Mon Sep 17 00:00:00 2001 From: Stephen Gunn Date: Wed, 18 Dec 2024 16:36:09 -0600 Subject: [PATCH 6/8] added individual exports for directus types --- build/cli.cjs | 6 ++++++ build/cli.cjs.map | 4 ++-- build/cli.mjs | 6 ++++++ build/cli.mjs.map | 4 ++-- build/index.cjs | 6 ++++++ build/index.cjs.map | 4 ++-- build/index.mjs | 6 ++++++ build/index.mjs.map | 4 ++-- package.json | 2 +- src/index.ts | 13 +++++++++++++ 10 files changed, 46 insertions(+), 9 deletions(-) diff --git a/build/cli.cjs b/build/cli.cjs index fd06cf8..b1a4277 100755 --- a/build/cli.cjs +++ b/build/cli.cjs @@ -122,6 +122,12 @@ export type ${typeName} = { } 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; }; diff --git a/build/cli.cjs.map b/build/cli.cjs.map index 147b23d..8086fb0 100644 --- a/build/cli.cjs.map +++ b/build/cli.cjs.map @@ -1,7 +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 // 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 return source;\n};\n"], - "mappings": "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", + "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 // 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, ` `) // Replace separators with space\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": ["import_promises", "openapiTS", "yargs"] } diff --git a/build/cli.mjs b/build/cli.mjs index 3c27a6a..b564518 100755 --- a/build/cli.mjs +++ b/build/cli.mjs @@ -99,6 +99,12 @@ export type ${typeName} = { } 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; }; diff --git a/build/cli.mjs.map b/build/cli.mjs.map index c570b6f..f6cd7af 100644 --- a/build/cli.mjs.map +++ b/build/cli.mjs.map @@ -1,7 +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 // 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 return source;\n};\n"], - "mappings": "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", + "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 // 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, ` `) // Replace separators with space\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 index 49d887b..8d6c17b 100644 --- a/build/index.cjs +++ b/build/index.cjs @@ -127,6 +127,12 @@ export type ${typeName} = { } 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 index bea0ad4..95a0d83 100644 --- a/build/index.cjs.map +++ b/build/index.cjs.map @@ -1,7 +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 // 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 return source;\n};\n"], - "mappings": "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", + "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 // 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, ` `) // Replace separators with space\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.mjs b/build/index.mjs index 0c426b1..9c1a1f6 100644 --- a/build/index.mjs +++ b/build/index.mjs @@ -92,6 +92,12 @@ export type ${typeName} = { } 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 { diff --git a/build/index.mjs.map b/build/index.mjs.map index 522b6b6..40e5136 100644 --- a/build/index.mjs.map +++ b/build/index.mjs.map @@ -1,7 +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 // 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 return source;\n};\n"], - "mappings": "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", + "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 // 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, ` `) // Replace separators with space\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.json b/package.json index 1f4f66a..3fe014f 100644 --- a/package.json +++ b/package.json @@ -47,5 +47,5 @@ }, "type": "module", "types": "./build/index.d.ts", - "version": "1.1.0" + "version": "1.2.0" } diff --git a/src/index.ts b/src/index.ts index 476b0ce..04df529 100644 --- a/src/index.ts +++ b/src/index.ts @@ -150,5 +150,18 @@ export const generateTypeScript = async ( source += `};\n`; + const toPascalCase = (str: string): string => + str + .replace(/[_\- ]+/g, ` `) // Replace separators with space + .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; }; From 83a1d473dc36f2b3265beec463786069a0b78a29 Mon Sep 17 00:00:00 2001 From: Stephen Gunn Date: Wed, 18 Dec 2024 19:24:44 -0600 Subject: [PATCH 7/8] trying to add relationships --- build/cli.cjs | 18 ++++++++++++++++++ build/cli.cjs.map | 4 ++-- build/cli.mjs | 18 ++++++++++++++++++ build/cli.mjs.map | 4 ++-- build/index.cjs | 18 ++++++++++++++++++ build/index.cjs.map | 4 ++-- build/index.mjs | 18 ++++++++++++++++++ build/index.mjs.map | 4 ++-- package.json | 2 +- src/index.ts | 43 ++++++++++++++++++++++++++++++++++++++++++- 10 files changed, 123 insertions(+), 10 deletions(-) diff --git a/build/cli.cjs b/build/cli.cjs index b1a4277..b1aaf3f 100755 --- a/build/cli.cjs +++ b/build/cli.cjs @@ -104,6 +104,24 @@ export type ${typeName} = { } } } + 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 diff --git a/build/cli.cjs.map b/build/cli.cjs.map index 8086fb0..91f4747 100644 --- a/build/cli.cjs.map +++ b/build/cli.cjs.map @@ -1,7 +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 // 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, ` `) // Replace separators with space\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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", + "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": ["import_promises", "openapiTS", "yargs"] } diff --git a/build/cli.mjs b/build/cli.mjs index b564518..c838e95 100755 --- a/build/cli.mjs +++ b/build/cli.mjs @@ -81,6 +81,24 @@ export type ${typeName} = { } } } + 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 diff --git a/build/cli.mjs.map b/build/cli.mjs.map index f6cd7af..5e83ee1 100644 --- a/build/cli.mjs.map +++ b/build/cli.mjs.map @@ -1,7 +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 // 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, ` `) // Replace separators with space\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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", + "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 index 8d6c17b..02d057d 100644 --- a/build/index.cjs +++ b/build/index.cjs @@ -109,6 +109,24 @@ export type ${typeName} = { } } } + 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 diff --git a/build/index.cjs.map b/build/index.cjs.map index 95a0d83..09fef32 100644 --- a/build/index.cjs.map +++ b/build/index.cjs.map @@ -1,7 +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 // 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, ` `) // Replace separators with space\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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", + "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.mjs b/build/index.mjs index 9c1a1f6..fbedcd3 100644 --- a/build/index.mjs +++ b/build/index.mjs @@ -74,6 +74,24 @@ export type ${typeName} = { } } } + 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 diff --git a/build/index.mjs.map b/build/index.mjs.map index 40e5136..aa92806 100644 --- a/build/index.mjs.map +++ b/build/index.mjs.map @@ -1,7 +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 // 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, ` `) // Replace separators with space\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": ";AAAA,SAAS,gBAAgB;AAGzB,OAAO,eAAe;AACtB,SAAS,SAAS;AASlB,IAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,MAAM,EAAE,OAAO;AAAA,IACb,cAAc,EAAE,OAAO;AAAA,IACvB,SAAS,EAAE,OAAO,EAAE,IAAI;AAAA,IACxB,eAAe,EAAE,OAAO;AAAA,EAC1B,CAAC;AACH,CAAC;AAEM,IAAM,eAAe,OAC1B,YACqB;AACrB,MAAI,OAAO,QAAQ,aAAa,UAAU;AACxC,WAAO,KAAK;AAAA,MACV,MAAM,SAAS,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,MAAM,UAAU,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,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;", + "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.json b/package.json index 3fe014f..da7b92c 100644 --- a/package.json +++ b/package.json @@ -47,5 +47,5 @@ }, "type": "module", "types": "./build/index.d.ts", - "version": "1.2.0" + "version": "1.3.0" } diff --git a/src/index.ts b/src/index.ts index 04df529..71e0135 100644 --- a/src/index.ts +++ b/src/index.ts @@ -126,6 +126,47 @@ export const generateTypeScript = async ( } } + // 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 + 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; + } + // 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( @@ -152,7 +193,7 @@ export const generateTypeScript = async ( const toPascalCase = (str: string): string => str - .replace(/[_\- ]+/g, ` `) // Replace separators with space + .replace(/[_\- ]+/g, ` `) .split(` `) .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(``); From a5c5076a0c9b3a2c6bd76f6aba5262a8d753bbd2 Mon Sep 17 00:00:00 2001 From: Stephen Gunn Date: Wed, 18 Dec 2024 19:37:22 -0600 Subject: [PATCH 8/8] small fix --- package.json | 2 +- src/cli.ts | 2 ++ src/index.ts | 29 +++++++++++++++-------------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index da7b92c..f2cef2e 100644 --- a/package.json +++ b/package.json @@ -47,5 +47,5 @@ }, "type": "module", "types": "./build/index.d.ts", - "version": "1.3.0" + "version": "1.3.1" } diff --git a/src/cli.ts b/src/cli.ts index 1672bc2..d433596 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -51,6 +51,8 @@ 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 71e0135..e61c212 100644 --- a/src/index.ts +++ b/src/index.ts @@ -61,6 +61,7 @@ export const readSpecFile = async ( }).then((response) => response.json())) as unknown; }; + type GenerateTypeScriptOptions = { readonly includeSystemCollections?: boolean; readonly typeName: string; @@ -99,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 @@ -141,16 +142,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