Skip to content

Commit 0fd0294

Browse files
authored
New version (#20)
1 parent feae006 commit 0fd0294

36 files changed

+13877
-2862
lines changed

.changeset/all-onions-slide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@zemd/flickr-rest-api": patch
3+
---
4+
5+
freeze deps version

.changeset/late-steaks-fold.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@zemd/http-client": major
3+
"@zemd/figma-rest-api": major
4+
---
5+
6+
Simplified new http-client, generate figma from openapi spec

apis/figma/package.json

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,21 @@
2929
"dist"
3030
],
3131
"scripts": {
32-
"build": "tsc",
33-
"dev": "tsc --watch"
32+
"build": "tsc && cp ./src/openapi.json ./dist/openapi.json",
33+
"dev": "tsc --watch",
34+
"generate-api": "bun ./scripts/generate-api.ts"
3435
},
3536
"devDependencies": {
37+
"@figma/rest-api-spec": "^0.24.0",
38+
"@types/node": "^22.13.10",
39+
"@zemd/openapi": "workspace:*",
3640
"@zemd/tsconfig": "catalog:",
37-
"typescript": "catalog:"
41+
"change-case": "^5.4.4",
42+
"ts-morph": "^25.0.1",
43+
"typescript": "catalog:",
44+
"yaml": "^2.7.0"
3845
},
3946
"dependencies": {
40-
"@zemd/http-client": "workspace:*",
41-
"zod": "catalog:"
47+
"@zemd/http-client": "workspace:*"
4248
}
4349
}

apis/figma/scripts/generate-api.ts

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import { parse } from "yaml";
2+
import { readFile } from "node:fs/promises";
3+
import { fileURLToPath } from "node:url";
4+
import { dirname, resolve } from "node:path";
5+
import type { OperationObject, ParameterObject, PathItemObject, PathsObject, Schema } from "@zemd/openapi";
6+
import { Project, VariableDeclarationKind } from "ts-morph";
7+
import { camelCase, pascalCase } from "change-case";
8+
9+
const __filename = fileURLToPath(import.meta.url);
10+
const __dirname = dirname(__filename);
11+
12+
const buildPathArguments = (pathParams: { name: string; origName: string; type: string }[]) => {
13+
return pathParams.reduce((acc: string, param: any) => {
14+
const arg = `${param.name}: ${param.type}`;
15+
if (acc) {
16+
return `${acc}, ${arg}`;
17+
}
18+
return arg;
19+
}, "");
20+
};
21+
22+
async function generateApi(data: Schema) {
23+
const project = new Project();
24+
const sourceFile = project.createSourceFile(resolve(__dirname, "..", "src", "api.ts"), undefined, {
25+
overwrite: true,
26+
});
27+
28+
sourceFile.addImportDeclaration({
29+
moduleSpecifier: "@zemd/http-client",
30+
namedImports: ["createEndpoint", "query", "body", "method", "prefix", "json"],
31+
});
32+
33+
sourceFile.addImportDeclaration({
34+
moduleSpecifier: "./utils",
35+
namedImports: ["figmaToken"],
36+
});
37+
38+
const versionSet = new Set<string>();
39+
const namespaceMap = new Map<string, Set<string>>();
40+
const operations: any[] = [];
41+
const namedImports = new Set<string>();
42+
const apiServerUrl = data.servers?.at(0)?.url;
43+
44+
for (const [path, methods] of Object.entries(data.paths as PathsObject)) {
45+
for (const [method, props] of Object.entries(methods as PathItemObject)) {
46+
const [, version, namespace] = path.split("/") as [undefined, string, string, ...string[]];
47+
48+
const operationId = (props as OperationObject).operationId;
49+
const returnType = pascalCase(`${operationId}Response`);
50+
const parameters = (props as OperationObject).parameters as ParameterObject[] | undefined;
51+
52+
versionSet.add(version);
53+
namespaceMap.set(version, (namespaceMap.get(version) ?? new Set()).add(namespace));
54+
namedImports.add(returnType);
55+
56+
const pathParams =
57+
parameters
58+
?.filter((param) => {
59+
return param.in === "path";
60+
})
61+
.map((param) => {
62+
const name = param.name;
63+
const type = `${pascalCase(operationId ?? "")}PathParams`;
64+
65+
namedImports.add(type);
66+
67+
return {
68+
origName: name,
69+
name: camelCase(name ?? ""),
70+
type: `${type}["${name}"]`,
71+
};
72+
}) ?? [];
73+
74+
const pathTemplate = pathParams.reduce((acc, param) => {
75+
return acc.replace(`{${param.origName}}`, `\${${param.name}}`);
76+
}, path);
77+
78+
let queryParams = null;
79+
const allQueryParams =
80+
parameters?.filter((param) => {
81+
return param.in === "query";
82+
}) ?? ([] as ParameterObject[]);
83+
84+
if (allQueryParams.length > 0) {
85+
const type = `${pascalCase(operationId ?? "")}QueryParams`;
86+
87+
namedImports.add(type);
88+
89+
queryParams = {
90+
type,
91+
required: allQueryParams.some((param) => {
92+
return param.required === true;
93+
}),
94+
};
95+
}
96+
97+
let bodyParams = null;
98+
const requestBody = (props as OperationObject).requestBody;
99+
if (requestBody) {
100+
namedImports.add(`${pascalCase(operationId ?? "")}RequestBody`);
101+
102+
bodyParams = {
103+
type: `${pascalCase(operationId ?? "")}RequestBody`,
104+
};
105+
}
106+
107+
operations.push({
108+
method,
109+
operationId,
110+
path,
111+
pathTemplate,
112+
version,
113+
namespace,
114+
pathParams,
115+
queryParams,
116+
bodyParams,
117+
returnType,
118+
});
119+
}
120+
}
121+
122+
sourceFile.addVariableStatement({
123+
declarationKind: VariableDeclarationKind.Const,
124+
isExported: true,
125+
declarations: [
126+
{
127+
name: "figma",
128+
initializer: (writer) => {
129+
writer.write(`(accessToken: string) => {`).indent();
130+
131+
writer
132+
.write(`const endpoint = createEndpoint([prefix("${apiServerUrl}"), json(), figmaToken(accessToken)]);`)
133+
.newLine();
134+
135+
writer.write(`return (`);
136+
writer.inlineBlock(() => {
137+
for (const version of versionSet) {
138+
writer.write(`${version}: `).indent();
139+
writer.block(() => {
140+
const versionNamespaces = namespaceMap.get(version) ?? new Set();
141+
for (const namespace of versionNamespaces) {
142+
writer.write(`${namespace}: {`).indent();
143+
const operationsFiltered = operations.filter((op) => {
144+
return op.version === version && op.namespace === namespace;
145+
});
146+
for (const operation of operationsFiltered) {
147+
const funcArguments = [
148+
buildPathArguments(operation.pathParams),
149+
operation.bodyParams && `obj: ${operation.bodyParams.type}`,
150+
operation.queryParams &&
151+
`options${operation.queryParams.required ? "" : "?"}: ${operation.queryParams.type}`,
152+
]
153+
.filter(Boolean)
154+
.join(", ");
155+
156+
const optionalTransformers: boolean = operation?.queryParams && !operation.queryParams.required;
157+
158+
let requiredTransformers = [
159+
`method("${operation.method.toUpperCase()}")`,
160+
operation.bodyParams && `body(JSON.stringify(obj))`,
161+
operation?.queryParams && operation.queryParams.required && `query(options)`,
162+
]
163+
.filter(Boolean)
164+
.join(",");
165+
requiredTransformers = `[${requiredTransformers}]`;
166+
167+
writer.write(`${operation.operationId}: async (${funcArguments}) => `).indent();
168+
writer.block(() => {
169+
if (optionalTransformers) {
170+
writer.write(`const transformers = ${requiredTransformers};`).indent();
171+
writer.write(`if (options) { transformers.push(query(options)); }`).indent();
172+
requiredTransformers = "transformers";
173+
}
174+
writer
175+
.write(
176+
`return endpoint<${operation.returnType}>(\`${operation.pathTemplate}\`, ${requiredTransformers})`,
177+
)
178+
.indent();
179+
});
180+
writer.write(`,`).newLine(); // ends operation function
181+
}
182+
writer.write(`},`).newLine(); // ends namespace
183+
}
184+
});
185+
writer.write(`,`).newLine(); // ends version
186+
}
187+
writer.write(``); // ends
188+
});
189+
writer.write(`)`);
190+
writer.write(`}`);
191+
},
192+
},
193+
],
194+
});
195+
196+
sourceFile.addImportDeclaration({
197+
moduleSpecifier: "@figma/rest-api-spec",
198+
isTypeOnly: true,
199+
namedImports: [...namedImports],
200+
});
201+
202+
await sourceFile.save();
203+
console.log("Source file generated successfully");
204+
}
205+
206+
async function main() {
207+
const filePath = fileURLToPath(import.meta.resolve("@figma/rest-api-spec/openapi/openapi.yaml"));
208+
const openapi = await readFile(filePath, "utf8");
209+
const data = await parse(openapi);
210+
// await writeFile(resolve(__dirname, "..", "dist", "openapi.json"), JSON.stringify(data, null, 2), "utf8");
211+
212+
await generateApi(data);
213+
}
214+
215+
main()
216+
.then(() => {
217+
console.log("Done");
218+
})
219+
.catch((error: unknown) => {
220+
console.error(error);
221+
});

0 commit comments

Comments
 (0)