Skip to content

Commit 6d4926c

Browse files
authored
Merge pull request #1 from tsedio/fix-improves-data
Fix improves data
2 parents d88a28b + 83fd320 commit 6d4926c

File tree

20 files changed

+1665
-167
lines changed

20 files changed

+1665
-167
lines changed

extensions/env-info/src/endpoint/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { wrapEndpoint } from "@tsed-cms/infra/bootstrap/directus.js";
77

88
import { EnvInfoService } from "./EnvInfoService.js";
99

10-
configuration().set("pkg", JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf8")));
10+
configuration().set("pkg", JSON.parse(readFileSync(join(import.meta.dirname, "..", "..", "..", "package.json"), "utf8")));
1111
configuration().set("branch", readFileSync(join(process.cwd(), "resources/release.info"), "utf8").trim());
1212
configuration().set("envs", process.env);
1313

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# tsed-flow-package-symbols
2+
3+
Directus operation that imports exported symbols of Ts.ED packages from the contractual JSON `https://tsed.dev/api.json` and writes them into the `package_symbols` collection.
4+
5+
## How it works
6+
7+
- The operation fetches `api.json` (or consumes a compatible payload provided by a previous step, see “Webhook/inline payload” below).
8+
- It iterates `modules["<package-name>"].symbols` and maps each entry to a `package_symbols` record via `src/mappers/mapApiSymbolToDirectus.ts`.
9+
- The corresponding package is ensured in the `packages` collection (created if missing) with `type: "official"`.
10+
- Upsert is performed by the symbol `id` coming from `api.json`. If a record already exists, its `versions` field is merged (union) with the new version from `api.json`.
11+
- Fields mapped per symbol:
12+
- `name ← symbolName`
13+
- `type ← symbolType`
14+
- `doc_url ← origin(api.json) + path`
15+
- `markdown_url ← markdown_base + path + ".md"`
16+
- `deprecated ← status includes "deprecated"`
17+
- `tags ← status without "deprecated"`
18+
- `versions ← []` then the global `api.json.version` is appended during import
19+
20+
## Build and run locally
21+
22+
1) Install deps at the repo root
23+
```bash
24+
corepack enable
25+
yarn install --immutable
26+
```
27+
28+
2) Build all extensions
29+
```bash
30+
yarn build
31+
```
32+
33+
3) Start Directus (dev)
34+
```bash
35+
yarn start:dev
36+
```
37+
38+
## Operation configuration (UI)
39+
40+
Directus → Flows → Add operation → “Ts.ED Package Symbols importer”.
41+
42+
Card options:
43+
- `url` (string) — `api.json` URL. Default in the card: `https://tsed.dev/api.json`.
44+
- `markdown_url` (string) — base URL where markdown files live. Default in the card: `https://tsed.dev/ai/references/api`.
45+
46+
Notes about defaults:
47+
- The operation handler also has an internal fallback for `markdown_url` to `https://tsed.dev/ai/references` if the option isn’t provided by the flow. To get the expected final markdown path `…/api/<symbol>.md`, set the card option explicitly to `https://tsed.dev/ai/references/api` (as in the exported flows).
48+
49+
Save the flow. The operation will use these options at runtime.
50+
51+
## Trigger the flow via HTTP (Directus API)
52+
53+
Manual trigger (flow id `569895a0-7a65-4cb2-94aa-67f77a776a08` in the sample exports):
54+
55+
```bash
56+
CMS_API_URL="http://localhost:8055"
57+
CMS_API_TOKEN="<ADMIN_OR_ALLOWED_TOKEN>"
58+
FLOW_ID="569895a0-7a65-4cb2-94aa-67f77a776a08"
59+
60+
curl -X POST \
61+
"$CMS_API_URL/flows/trigger/$FLOW_ID" \
62+
-H "Authorization: Bearer $CMS_API_TOKEN" \
63+
-H "Content-Type: application/json" \
64+
-d '{}'
65+
```
66+
67+
### Webhook trigger and inline payload (optional)
68+
69+
This repo also includes a webhook flow (`9039bef8-fdc3-4f31-b5ab-7fee31273921`). If you POST a body containing a payload compatible with `src/schema/ApiPayloadSchema.ts`, the operation will validate and use this body instead of fetching from `url`.
70+
71+
Example (send the whole api.json as request body):
72+
73+
```bash
74+
CMS_API_URL="http://localhost:8055"
75+
CMS_API_TOKEN="<ADMIN_OR_ALLOWED_TOKEN>"
76+
FLOW_ID="9039bef8-fdc3-4f31-b5ab-7fee31273921"
77+
78+
curl -X POST \
79+
"$CMS_API_URL/flows/trigger/$FLOW_ID" \
80+
-H "Authorization: Bearer $CMS_API_TOKEN" \
81+
-H "Content-Type: application/json" \
82+
--data-binary @api.json
83+
```
84+
85+
## Permissions required
86+
87+
This operation runs with the flow runner’s role/policy (`accountability: "all"` in the exported flows). Ensure the role used to trigger the flow has at least:
88+
89+
- `packages`: read, create, update (the operation may create missing packages)
90+
- `package_symbols`: read, create, update (the operation reads by `id`, then creates/updates symbols)
91+
92+
If these permissions are missing, the flow will fail with `403 You don't have permission to access this.` during upsert.
93+
94+
## Testing
95+
96+
Run the unit tests for the mapper and the service:
97+
98+
```bash
99+
yarn vitest run \
100+
extensions/tsed-flow-package-symbols/src/mappers/mapApiSymbolToDirectus.spec.ts \
101+
packages/usecases/package-symbols/PackageSymbolsService.spec.ts
102+
```
103+
104+
Both tests should pass.
105+
106+
## Notes
107+
108+
- The `ApiPayloadSchema` strictly types the `api.json` format used by the operation.
109+
- The mapper intentionally keeps logic minimal and assumes the contract is stable.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "tsed-flow-package-symbols",
3+
"description": "Flow operation to import Ts.ED exported symbols into Directus package_symbols collection",
4+
"icon": "code",
5+
"version": "1.0.0",
6+
"keywords": [
7+
"directus",
8+
"directus-extension",
9+
"directus-extension-operation"
10+
],
11+
"type": "module",
12+
"files": [
13+
"dist"
14+
],
15+
"directus:extension": {
16+
"type": "operation",
17+
"path": {
18+
"app": "dist/app.js",
19+
"api": "dist/api.cjs"
20+
},
21+
"source": {
22+
"app": "src/app.ts",
23+
"api": "src/api.ts"
24+
},
25+
"host": "^10.10.0"
26+
},
27+
"scripts": {
28+
"build": "directus-extension build",
29+
"dev": "directus-extension build --watch --no-minify",
30+
"link": "directus-extension link",
31+
"add": "directus-extension add"
32+
},
33+
"devDependencies": {
34+
"@directus/extensions-sdk": "13.1.1"
35+
}
36+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { defineOperationApi } from "@directus/extensions-sdk";
2+
import { inject } from "@tsed/di";
3+
import { wrapOperation } from "@tsed-cms/infra/bootstrap/directus.js";
4+
import type { Package } from "@tsed-cms/infra/directus/interfaces/DirectusSchema.js";
5+
import { HttpClient } from "@tsed-cms/infra/http/HttpClient.js";
6+
import { validate } from "@tsed-cms/infra/validators/validate.js";
7+
import { PackageSymbolsService } from "@tsed-cms/usecases/package-symbols/PackageSymbolsService.js";
8+
import { PackagesService } from "@tsed-cms/usecases/packages/PackagesService.js";
9+
10+
import { mapApiSymbolToDirectus } from "./mappers/mapApiSymbolToDirectus.js";
11+
import { type ApiPayload, ApiPayloadSchema } from "./schema/ApiPayloadSchema.js";
12+
13+
export type Options = {
14+
url?: string;
15+
markdown_url?: string;
16+
};
17+
18+
async function ensurePackage(pkgName: string): Promise<Package> {
19+
const packagesService = inject(PackagesService);
20+
21+
const existing = await packagesService.findByName(pkgName);
22+
23+
if (existing) {
24+
return existing;
25+
}
26+
27+
// Tous les modules listés dans api.json sont traités comme "official"
28+
return packagesService.upsertOne({ name: pkgName, type: "official" });
29+
}
30+
31+
export default defineOperationApi<Options>({
32+
id: "tsed-flow-package-symbols",
33+
handler: wrapOperation(async (opts, context) => {
34+
const startedAt = Date.now();
35+
const url = (opts?.url?.trim() || "https://tsed.dev/api.json").toString();
36+
const markdownUrl = (opts?.markdown_url?.trim() || "https://tsed.dev/ai/references").toString();
37+
const http = inject(HttpClient);
38+
const symbolsService = inject(PackageSymbolsService);
39+
40+
// Origin pour construire les URL de doc
41+
const origin = new URL(url).origin;
42+
let data: ApiPayload;
43+
44+
if ((context?.data?.["$last"] as any)?.body) {
45+
try {
46+
data = await validate((context?.data?.["$last"] as any)?.body, ApiPayloadSchema);
47+
} catch (er) {
48+
return {
49+
url,
50+
processed: 0,
51+
upserted: 0,
52+
durationMs: Date.now() - startedAt,
53+
errors: [
54+
{
55+
error: er?.message || String(er)
56+
}
57+
]
58+
};
59+
}
60+
} else {
61+
data = await http.get<ApiPayload>(url);
62+
}
63+
64+
let processed = 0;
65+
let upserted = 0;
66+
const errors: { name: string; pkg: string; error: string }[] = [];
67+
68+
for (const [pkgName, mod] of Object.entries(data.modules)) {
69+
for (const s of mod.symbols) {
70+
try {
71+
const pkg = await ensurePackage(pkgName);
72+
const mapped = mapApiSymbolToDirectus(s, origin, markdownUrl);
73+
74+
mapped.versions.push(data.version);
75+
76+
await symbolsService.upsertOne({ ...mapped, package: pkg.id });
77+
78+
upserted += 1;
79+
processed += 1;
80+
} catch (er: any) {
81+
errors.push({
82+
name: s.symbolName,
83+
pkg: pkgName,
84+
error: er?.message || String(er)
85+
});
86+
}
87+
}
88+
}
89+
90+
return {
91+
url,
92+
processed,
93+
upserted,
94+
durationMs: Date.now() - startedAt,
95+
errors
96+
};
97+
})
98+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { defineOperationApp } from "@directus/extensions-sdk";
2+
3+
export default defineOperationApp({
4+
id: "tsed-flow-package-symbols",
5+
name: "Ts.ED Package Symbols importer",
6+
icon: "code",
7+
description: "Retrieves exported symbols from Ts.ED packages from api.json and upserts them into package_symbols.",
8+
overview: ({ url }) => [
9+
{
10+
label: "API URL",
11+
text: url || "https://tsed.dev/api.json"
12+
}
13+
],
14+
options: [
15+
{
16+
field: "url",
17+
name: "API URL",
18+
type: "string",
19+
meta: {
20+
width: "full",
21+
interface: "input",
22+
note: "URL of JSON symbols (default: https://tsed.dev/api.json)"
23+
},
24+
schema: {
25+
default_value: "https://tsed.dev/api.json"
26+
}
27+
},
28+
{
29+
field: "markdown_url",
30+
name: "Markdown base URL",
31+
type: "string",
32+
meta: {
33+
width: "full",
34+
interface: "input",
35+
note: "URL where the markdown contents of the symbols are stored (default: https://tsed.dev/ai/references/api)"
36+
},
37+
schema: {
38+
default_value: "https://tsed.dev/ai/references/api"
39+
}
40+
}
41+
]
42+
});
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { ApiSymbol } from "../schema/ApiPayloadSchema.js";
2+
import { mapApiSymbolToDirectus } from "./mapApiSymbolToDirectus.js";
3+
4+
describe("mapApiSymbolToDirectus", () => {
5+
const baseOrigin = "https://tsed.dev";
6+
const markdownOrigin = "https://tsed.dev/ai/references";
7+
8+
it("mappe les champs de base correctement", () => {
9+
const input: ApiSymbol = {
10+
id: "sym-1",
11+
path: "/api/core/Controller",
12+
symbolName: "Controller",
13+
module: "@tsed/core",
14+
symbolType: "class",
15+
status: []
16+
} as any;
17+
18+
const out = mapApiSymbolToDirectus(input, baseOrigin, markdownOrigin);
19+
20+
expect(out).toMatchObject({
21+
id: "sym-1",
22+
status: "published",
23+
name: "Controller",
24+
type: "class",
25+
doc_url: "https://tsed.dev/api/core/Controller",
26+
markdown_url: "https://tsed.dev/ai/references/api/core/Controller.md",
27+
versions: [],
28+
deprecated: false,
29+
tags: []
30+
});
31+
});
32+
33+
it("manages the deprecated status → deprecated=true and tags=[]", () => {
34+
const input: ApiSymbol = {
35+
id: "sym-2",
36+
path: "/api/schema/UseJsonMapper.html",
37+
symbolName: "UseJsonMapper",
38+
module: "@tsed/schema",
39+
symbolType: "decorator",
40+
status: ["deprecated"]
41+
} as any;
42+
43+
const out = mapApiSymbolToDirectus(input, baseOrigin);
44+
45+
expect(out.deprecated).toBe(true);
46+
expect(out.tags).toEqual([]);
47+
});
48+
49+
it("status undefined → deprecated=false and tags=[]", () => {
50+
const input: ApiSymbol = {
51+
id: "sym-3",
52+
path: "/api/schema/JsonEntityStore.html",
53+
symbolName: "JsonEntityStore",
54+
module: "@tsed/schema",
55+
symbolType: "class"
56+
} as any;
57+
58+
const out = mapApiSymbolToDirectus(input, baseOrigin);
59+
60+
expect(out.deprecated).toBe(false);
61+
expect(out.tags).toEqual([]);
62+
});
63+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { PackageSymbol } from "@tsed-cms/infra/directus/interfaces/DirectusSchema.js";
2+
3+
import type { ApiSymbol } from "../schema/ApiPayloadSchema.js";
4+
5+
/**
6+
* Build a valid PackageSymbol payload (without the `package` relation)
7+
* from a single ApiJsonModuleSymbol item.
8+
*
9+
* Responsibility: mapping fields and normalizing optional metadata only.
10+
* Caller is responsible for resolving/ensuring the `package` id.
11+
*/
12+
export function mapApiSymbolToDirectus(symbol: ApiSymbol, baseOrigin: string, markdownUrl?: string) {
13+
const deprecated = symbol.status ? symbol.status.includes("deprecated") : false;
14+
const tags = (symbol.status || []).filter((t) => t !== "deprecated");
15+
16+
return {
17+
id: symbol.id,
18+
status: "published",
19+
name: symbol.symbolName,
20+
type: symbol.symbolType as PackageSymbol["type"],
21+
doc_url: `${baseOrigin}${symbol.path}`,
22+
markdown_url: `${markdownUrl}${symbol.path}.md`,
23+
additional_doc_url: "",
24+
versions: [] as string[],
25+
deprecated,
26+
tags
27+
} satisfies Omit<PackageSymbol, "package" | "user_created" | "date_created" | "user_updated" | "date_updated">;
28+
}

0 commit comments

Comments
 (0)