Skip to content

Commit 133675d

Browse files
committed
feat: enhance tsed-flow-package-symbols with Markdown support, error handling, and schema updates
Added support for a `markdown_url` to enrich package symbols with Markdown-based documentation. Improved error handling in migration logic for presets and introduced several schema enhancements, including new fields in the `package_symbols` collection and updated display options.
1 parent c590e66 commit 133675d

File tree

9 files changed

+241
-56
lines changed

9 files changed

+241
-56
lines changed
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.

extensions/tsed-flow-package-symbols/src/api.ts

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { inject } from "@tsed/di";
33
import { wrapOperation } from "@tsed-cms/infra/bootstrap/directus.js";
44
import type { Package } from "@tsed-cms/infra/directus/interfaces/DirectusSchema.js";
55
import { HttpClient } from "@tsed-cms/infra/http/HttpClient.js";
6+
import { validate } from "@tsed-cms/infra/validators/validate.js";
67
import { PackageSymbolsService } from "@tsed-cms/usecases/package-symbols/PackageSymbolsService.js";
78
import { PackagesService } from "@tsed-cms/usecases/packages/PackagesService.js";
89

9-
import { type ApiJsonModule, type ApiJsonResponse, mapApiSymbolToDirectus } from "./mappers/mapApiSymbolToDirectus.js";
10+
import { mapApiSymbolToDirectus } from "./mappers/mapApiSymbolToDirectus.js";
11+
import { type ApiPayload, ApiPayloadSchema } from "./schema/ApiPayloadSchema.js";
1012

1113
export type Options = {
1214
url?: string;
@@ -28,25 +30,42 @@ async function ensurePackage(pkgName: string): Promise<Package> {
2830

2931
export default defineOperationApi<Options>({
3032
id: "tsed-flow-package-symbols",
31-
handler: wrapOperation(async (opts) => {
33+
handler: wrapOperation(async (opts, context) => {
3234
const startedAt = Date.now();
3335
const url = (opts?.url?.trim() || "https://tsed.dev/api.json").toString();
3436
const markdownUrl = (opts?.markdown_url?.trim() || "https://tsed.dev/ai/references").toString();
35-
3637
const http = inject(HttpClient);
3738
const symbolsService = inject(PackageSymbolsService);
3839

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+
3964
let processed = 0;
4065
let upserted = 0;
4166
const errors: { name: string; pkg: string; error: string }[] = [];
4267

43-
// Fetch the JSON (typed)
44-
const data = await http.get<ApiJsonResponse>(url);
45-
46-
// Origin pour construire les URL de doc
47-
const origin = new URL(url).origin;
48-
49-
for (const [pkgName, mod] of Object.entries<ApiJsonModule>(data.modules)) {
68+
for (const [pkgName, mod] of Object.entries(data.modules)) {
5069
for (const s of mod.symbols) {
5170
try {
5271
const pkg = await ensurePackage(pkgName);
@@ -59,7 +78,6 @@ export default defineOperationApi<Options>({
5978
upserted += 1;
6079
processed += 1;
6180
} catch (er: any) {
62-
console.log(er);
6381
errors.push({
6482
name: s.symbolName,
6583
pkg: pkgName,

extensions/tsed-flow-package-symbols/src/mappers/mapApiSymbolToDirectus.spec.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
import { type ApiJsonModuleSymbol, mapApiSymbolToDirectus } from "./mapApiSymbolToDirectus.js";
1+
import type { ApiSymbol } from "../schema/ApiPayloadSchema.js";
2+
import { mapApiSymbolToDirectus } from "./mapApiSymbolToDirectus.js";
23

34
describe("mapApiSymbolToDirectus", () => {
45
const baseOrigin = "https://tsed.dev";
56
const markdownOrigin = "https://tsed.dev/ai/references";
6-
const pkgId = "pkg-1";
77

88
it("mappe les champs de base correctement", () => {
9-
const input: ApiJsonModuleSymbol = {
9+
const input: ApiSymbol = {
1010
id: "sym-1",
1111
path: "/api/core/Controller",
1212
symbolName: "Controller",
1313
module: "@tsed/core",
1414
symbolType: "class",
1515
status: []
16-
};
16+
} as any;
1717

18-
const out = mapApiSymbolToDirectus(pkgId, input, baseOrigin, markdownOrigin);
18+
const out = mapApiSymbolToDirectus(input, baseOrigin, markdownOrigin);
1919

2020
expect(out).toMatchObject({
2121
id: "sym-1",
@@ -25,38 +25,37 @@ describe("mapApiSymbolToDirectus", () => {
2525
doc_url: "https://tsed.dev/api/core/Controller",
2626
markdown_url: "https://tsed.dev/ai/references/api/core/Controller.md",
2727
versions: [],
28-
package: pkgId,
2928
deprecated: false,
3029
tags: []
3130
});
3231
});
3332

34-
it("gère le statut deprecated → deprecated=true et tags=['deprecated']", () => {
35-
const input: ApiJsonModuleSymbol = {
33+
it("manages the deprecated status → deprecated=true and tags=[]", () => {
34+
const input: ApiSymbol = {
3635
id: "sym-2",
3736
path: "/api/schema/UseJsonMapper.html",
3837
symbolName: "UseJsonMapper",
3938
module: "@tsed/schema",
4039
symbolType: "decorator",
4140
status: ["deprecated"]
42-
};
41+
} as any;
4342

44-
const out = mapApiSymbolToDirectus(pkgId, input, baseOrigin);
43+
const out = mapApiSymbolToDirectus(input, baseOrigin);
4544

4645
expect(out.deprecated).toBe(true);
47-
expect(out.tags).toEqual(["deprecated"]);
46+
expect(out.tags).toEqual([]);
4847
});
4948

50-
it("status non défini → deprecated=false et tags=[]", () => {
51-
const input: ApiJsonModuleSymbol = {
49+
it("status undefined → deprecated=false and tags=[]", () => {
50+
const input: ApiSymbol = {
5251
id: "sym-3",
5352
path: "/api/schema/JsonEntityStore.html",
5453
symbolName: "JsonEntityStore",
5554
module: "@tsed/schema",
5655
symbolType: "class"
5756
} as any;
5857

59-
const out = mapApiSymbolToDirectus(pkgId, input, baseOrigin);
58+
const out = mapApiSymbolToDirectus(input, baseOrigin);
6059

6160
expect(out.deprecated).toBe(false);
6261
expect(out.tags).toEqual([]);

extensions/tsed-flow-package-symbols/src/mappers/mapApiSymbolToDirectus.ts

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,6 @@
11
import type { PackageSymbol } from "@tsed-cms/infra/directus/interfaces/DirectusSchema.js";
22

3-
// Typed response for https://tsed.dev/api.json (stable format)
4-
export type ApiJsonModuleSymbol = {
5-
id: string;
6-
path: string;
7-
symbolName: string;
8-
module: string;
9-
symbolType: PackageSymbol["type"];
10-
status?: string[];
11-
};
12-
13-
export type ApiJsonModule = {
14-
name: string;
15-
symbols: ApiJsonModuleSymbol[];
16-
};
17-
18-
export type ApiJsonResponse = {
19-
version: string;
20-
scope: string;
21-
symbolTypes: { value: PackageSymbol["type"]; label: string; code: string }[];
22-
symbolStatus: { value: string; label: string }[];
23-
modules: Record<string, ApiJsonModule>;
24-
};
3+
import type { ApiSymbol } from "../schema/ApiPayloadSchema.js";
254

265
/**
276
* Build a valid PackageSymbol payload (without the `package` relation)
@@ -30,15 +9,15 @@ export type ApiJsonResponse = {
309
* Responsibility: mapping fields and normalizing optional metadata only.
3110
* Caller is responsible for resolving/ensuring the `package` id.
3211
*/
33-
export function mapApiSymbolToDirectus(symbol: ApiJsonModuleSymbol, baseOrigin: string, markdownUrl?: string) {
12+
export function mapApiSymbolToDirectus(symbol: ApiSymbol, baseOrigin: string, markdownUrl?: string) {
3413
const deprecated = symbol.status ? symbol.status.includes("deprecated") : false;
3514
const tags = (symbol.status || []).filter((t) => t !== "deprecated");
3615

3716
return {
3817
id: symbol.id,
3918
status: "published",
4019
name: symbol.symbolName,
41-
type: symbol.symbolType,
20+
type: symbol.symbolType as PackageSymbol["type"],
4221
doc_url: `${baseOrigin}${symbol.path}`,
4322
markdown_url: `${markdownUrl}${symbol.path}.md`,
4423
additional_doc_url: "",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { s } from "@tsed/schema";
2+
3+
const ApiSymbolTypeSchema = s
4+
.object({
5+
value: s.string().enum("decorator", "class", "enum", "function", "interface", "const", "service", "type").required(),
6+
label: s.string().required(),
7+
code: s.string().required()
8+
})
9+
10+
const ApiSymbolSchema = s
11+
.object({
12+
id: s.string().required(),
13+
path: s.string().required(),
14+
module: s.string().required(),
15+
symbolName: s.string().required(),
16+
symbolType: s.string().required(),
17+
symbolCode: s.string().required(),
18+
status: s.array(s.string()).required()
19+
})
20+
21+
export const ApiPayloadSchema = s
22+
.object({
23+
version: s.string().required(),
24+
scope: s.string().required(),
25+
symbolTypes: s.array(ApiSymbolTypeSchema).required(),
26+
symbolStatus: s.array(
27+
s.object({
28+
label: s.string().required(),
29+
value: s.string().required()
30+
})
31+
),
32+
modules: s.record(
33+
s.object({
34+
name: s.string().required(),
35+
symbols: s.array(ApiSymbolSchema).required()
36+
})
37+
)
38+
})
39+
40+
export type ApiPayload = s.infer<typeof ApiPayloadSchema>;
41+
export type ApiSymbol = s.infer<typeof ApiSymbolSchema>;

migrations/data/directus_flows.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,5 +154,22 @@
154154
"operation": "c651e60f-159c-4b4d-b636-c9d50debe3de",
155155
"date_created": "2025-11-15T10:33:07.296Z",
156156
"user_created": "dc515c74-5ed5-4464-b522-6ea1f2f6d270"
157+
},
158+
{
159+
"id": "9039bef8-fdc3-4f31-b5ab-7fee31273921",
160+
"name": "Trigger update package symbols",
161+
"icon": "bolt",
162+
"color": null,
163+
"description": null,
164+
"status": "active",
165+
"trigger": "webhook",
166+
"accountability": "all",
167+
"options": {
168+
"async": true,
169+
"method": "POST"
170+
},
171+
"operation": "a2391589-d955-4677-9ff5-1bb846cebf98",
172+
"date_created": "2025-11-15T11:49:36.406Z",
173+
"user_created": "dc515c74-5ed5-4464-b522-6ea1f2f6d270"
157174
}
158175
]

migrations/data/directus_operations.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,5 +123,22 @@
123123
"flow": "569895a0-7a65-4cb2-94aa-67f77a776a08",
124124
"date_created": "2025-11-15T10:35:32.381Z",
125125
"user_created": "dc515c74-5ed5-4464-b522-6ea1f2f6d270"
126+
},
127+
{
128+
"id": "a2391589-d955-4677-9ff5-1bb846cebf98",
129+
"name": "Ts.ED Package Symbols importer",
130+
"key": "tsed_flow_package_symbols_uvxzc",
131+
"type": "tsed-flow-package-symbols",
132+
"position_x": 19,
133+
"position_y": 1,
134+
"options": {
135+
"url": "https://tsed.dev/api.json",
136+
"markdown_url": "https://tsed.dev/ai/references/api"
137+
},
138+
"resolve": null,
139+
"reject": null,
140+
"flow": "9039bef8-fdc3-4f31-b5ab-7fee31273921",
141+
"date_created": "2025-11-15T11:49:52.495Z",
142+
"user_created": "dc515c74-5ed5-4464-b522-6ea1f2f6d270"
126143
}
127144
]

0 commit comments

Comments
 (0)