@open-mercato/shared 0.6.7-develop.6622.1.4d29ddaca4 → 0.6.7-develop.6628.1.f9e4ecf70f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/lib/openapi/attach-docs.js +44 -0
- package/dist/lib/openapi/attach-docs.js.map +7 -0
- package/dist/lib/openapi/index.js +2 -0
- package/dist/lib/openapi/index.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/openapi/__tests__/attach-docs.test.ts +105 -0
- package/src/lib/openapi/attach-docs.ts +63 -0
- package/src/lib/openapi/index.ts +1 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:shared] found
|
|
1
|
+
[build:shared] found 239 entry points
|
|
2
2
|
[build:shared] built successfully
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function manifestKey(entry) {
|
|
2
|
+
return entry.kind === "legacy" && entry.method ? `${entry.moduleId}\0${entry.method}\0${entry.path}` : `${entry.moduleId}\0${entry.path}`;
|
|
3
|
+
}
|
|
4
|
+
async function loadOpenApiExport(entry) {
|
|
5
|
+
if (!entry) return void 0;
|
|
6
|
+
try {
|
|
7
|
+
const mod = await entry.load();
|
|
8
|
+
const openApi = mod.openApi;
|
|
9
|
+
return openApi && typeof openApi === "object" ? openApi : void 0;
|
|
10
|
+
} catch {
|
|
11
|
+
return void 0;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function attachOpenApiDocsToModules(modules, manifests) {
|
|
15
|
+
const manifestsByKey = /* @__PURE__ */ new Map();
|
|
16
|
+
for (const entry of manifests) {
|
|
17
|
+
manifestsByKey.set(manifestKey(entry), entry);
|
|
18
|
+
}
|
|
19
|
+
return Promise.all(
|
|
20
|
+
modules.map(async (mod) => {
|
|
21
|
+
if (!Array.isArray(mod.apis) || !mod.apis.length) return mod;
|
|
22
|
+
const apis = await Promise.all(
|
|
23
|
+
mod.apis.map(async (api) => {
|
|
24
|
+
if (api.docs) return api;
|
|
25
|
+
if ("handlers" in api) {
|
|
26
|
+
const entry2 = manifestsByKey.get(`${mod.id}\0${api.path}`);
|
|
27
|
+
const docs2 = await loadOpenApiExport(entry2);
|
|
28
|
+
if (!docs2) return api;
|
|
29
|
+
return { ...api, docs: docs2 };
|
|
30
|
+
}
|
|
31
|
+
const entry = manifestsByKey.get(`${mod.id}\0${api.method}\0${api.path}`);
|
|
32
|
+
const docs = await loadOpenApiExport(entry);
|
|
33
|
+
if (!docs) return api;
|
|
34
|
+
return { ...api, docs };
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
return { ...mod, apis };
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
attachOpenApiDocsToModules
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=attach-docs.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/openapi/attach-docs.ts"],
|
|
4
|
+
"sourcesContent": ["import type {\n ApiRouteManifestEntry,\n Module,\n ModuleApi,\n ModuleApiLegacy,\n ModuleApiRouteFile,\n} from '../../modules/registry'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from './types'\n\nfunction manifestKey(entry: ApiRouteManifestEntry): string {\n return entry.kind === 'legacy' && entry.method\n ? `${entry.moduleId}\\0${entry.method}\\0${entry.path}`\n : `${entry.moduleId}\\0${entry.path}`\n}\n\nasync function loadOpenApiExport(entry: ApiRouteManifestEntry | undefined): Promise<unknown> {\n if (!entry) return undefined\n try {\n const mod = await entry.load()\n const openApi = (mod as { openApi?: unknown }).openApi\n return openApi && typeof openApi === 'object' ? openApi : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * The lazy runtime module registry omits each route's `docs` (the `openApi`\n * export) so route files stay dynamically imported. Documentation endpoints\n * that build the OpenAPI document from that registry must re-attach the docs\n * by loading each route module through the API route manifests on demand.\n */\nexport async function attachOpenApiDocsToModules(\n modules: Module[],\n manifests: ApiRouteManifestEntry[],\n): Promise<Module[]> {\n const manifestsByKey = new Map<string, ApiRouteManifestEntry>()\n for (const entry of manifests) {\n manifestsByKey.set(manifestKey(entry), entry)\n }\n\n return Promise.all(\n modules.map(async (mod) => {\n if (!Array.isArray(mod.apis) || !mod.apis.length) return mod\n const apis = await Promise.all(\n mod.apis.map(async (api): Promise<ModuleApi> => {\n if ((api as { docs?: unknown }).docs) return api\n if ('handlers' in api) {\n const entry = manifestsByKey.get(`${mod.id}\\0${api.path}`)\n const docs = await loadOpenApiExport(entry)\n if (!docs) return api\n return { ...api, docs: docs as OpenApiRouteDoc } satisfies ModuleApiRouteFile\n }\n const entry = manifestsByKey.get(`${mod.id}\\0${api.method}\\0${api.path}`)\n const docs = await loadOpenApiExport(entry)\n if (!docs) return api\n return { ...api, docs: docs as OpenApiMethodDoc } satisfies ModuleApiLegacy\n }),\n )\n return { ...mod, apis }\n }),\n )\n}\n"],
|
|
5
|
+
"mappings": "AASA,SAAS,YAAY,OAAsC;AACzD,SAAO,MAAM,SAAS,YAAY,MAAM,SACpC,GAAG,MAAM,QAAQ,KAAK,MAAM,MAAM,KAAK,MAAM,IAAI,KACjD,GAAG,MAAM,QAAQ,KAAK,MAAM,IAAI;AACtC;AAEA,eAAe,kBAAkB,OAA4D;AAC3F,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,KAAK;AAC7B,UAAM,UAAW,IAA8B;AAC/C,WAAO,WAAW,OAAO,YAAY,WAAW,UAAU;AAAA,EAC5D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQA,eAAsB,2BACpB,SACA,WACmB;AACnB,QAAM,iBAAiB,oBAAI,IAAmC;AAC9D,aAAW,SAAS,WAAW;AAC7B,mBAAe,IAAI,YAAY,KAAK,GAAG,KAAK;AAAA,EAC9C;AAEA,SAAO,QAAQ;AAAA,IACb,QAAQ,IAAI,OAAO,QAAQ;AACzB,UAAI,CAAC,MAAM,QAAQ,IAAI,IAAI,KAAK,CAAC,IAAI,KAAK,OAAQ,QAAO;AACzD,YAAM,OAAO,MAAM,QAAQ;AAAA,QACzB,IAAI,KAAK,IAAI,OAAO,QAA4B;AAC9C,cAAK,IAA2B,KAAM,QAAO;AAC7C,cAAI,cAAc,KAAK;AACrB,kBAAMA,SAAQ,eAAe,IAAI,GAAG,IAAI,EAAE,KAAK,IAAI,IAAI,EAAE;AACzD,kBAAMC,QAAO,MAAM,kBAAkBD,MAAK;AAC1C,gBAAI,CAACC,MAAM,QAAO;AAClB,mBAAO,EAAE,GAAG,KAAK,MAAMA,MAAwB;AAAA,UACjD;AACA,gBAAM,QAAQ,eAAe,IAAI,GAAG,IAAI,EAAE,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI,EAAE;AACxE,gBAAM,OAAO,MAAM,kBAAkB,KAAK;AAC1C,cAAI,CAAC,KAAM,QAAO;AAClB,iBAAO,EAAE,GAAG,KAAK,KAA+B;AAAA,QAClD,CAAC;AAAA,MACH;AACA,aAAO,EAAE,GAAG,KAAK,KAAK;AAAA,IACxB,CAAC;AAAA,EACH;AACF;",
|
|
6
|
+
"names": ["entry", "docs"]
|
|
7
|
+
}
|
|
@@ -2,7 +2,9 @@ export * from "./types.js";
|
|
|
2
2
|
import { buildOpenApiDocument, generateMarkdownFromOpenApi } from "./generator.js";
|
|
3
3
|
export * from "./crud.js";
|
|
4
4
|
import { sanitizeOpenApiDocument } from "./sanitize.js";
|
|
5
|
+
import { attachOpenApiDocsToModules } from "./attach-docs.js";
|
|
5
6
|
export {
|
|
7
|
+
attachOpenApiDocsToModules,
|
|
6
8
|
buildOpenApiDocument,
|
|
7
9
|
generateMarkdownFromOpenApi,
|
|
8
10
|
sanitizeOpenApiDocument
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/openapi/index.ts"],
|
|
4
|
-
"sourcesContent": ["export * from './types'\nexport { buildOpenApiDocument, generateMarkdownFromOpenApi } from './generator'\nexport * from './crud'\nexport { sanitizeOpenApiDocument } from './sanitize'\n"],
|
|
5
|
-
"mappings": "AAAA,cAAc;AACd,SAAS,sBAAsB,mCAAmC;AAClE,cAAc;AACd,SAAS,+BAA+B;",
|
|
4
|
+
"sourcesContent": ["export * from './types'\nexport { buildOpenApiDocument, generateMarkdownFromOpenApi } from './generator'\nexport * from './crud'\nexport { sanitizeOpenApiDocument } from './sanitize'\nexport { attachOpenApiDocsToModules } from './attach-docs'\n"],
|
|
5
|
+
"mappings": "AAAA,cAAc;AACd,SAAS,sBAAsB,mCAAmC;AAClE,cAAc;AACd,SAAS,+BAA+B;AACxC,SAAS,kCAAkC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/version.ts"],
|
|
4
|
-
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.7-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.7-develop.6628.1.f9e4ecf70f'\nexport const appVersion = APP_VERSION\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/shared",
|
|
3
|
-
"version": "0.6.7-develop.
|
|
3
|
+
"version": "0.6.7-develop.6628.1.f9e4ecf70f",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"@mikro-orm/core": "^7.1.5",
|
|
98
98
|
"@mikro-orm/decorators": "^7.1.5",
|
|
99
99
|
"@mikro-orm/postgresql": "^7.1.5",
|
|
100
|
-
"@open-mercato/cache": "0.6.7-develop.
|
|
100
|
+
"@open-mercato/cache": "0.6.7-develop.6628.1.f9e4ecf70f",
|
|
101
101
|
"@types/sanitize-html": "^2.16.1",
|
|
102
102
|
"dotenv": "^17.4.2",
|
|
103
103
|
"pino": "^10.3.1",
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { attachOpenApiDocsToModules } from '../attach-docs'
|
|
3
|
+
import { buildOpenApiDocument } from '../generator'
|
|
4
|
+
import type { ApiRouteManifestEntry, Module } from '../../../modules/registry'
|
|
5
|
+
|
|
6
|
+
const inviteSchema = z.object({ email: z.string().email(), roleIds: z.array(z.string()) })
|
|
7
|
+
|
|
8
|
+
const routeFileOpenApi = {
|
|
9
|
+
methods: {
|
|
10
|
+
POST: {
|
|
11
|
+
summary: 'Invite user',
|
|
12
|
+
requestBody: { schema: inviteSchema, description: 'Invitation payload' },
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const legacyOpenApi = {
|
|
18
|
+
summary: 'Legacy create',
|
|
19
|
+
requestBody: { schema: z.object({ name: z.string() }) },
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeModules(): Module[] {
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
id: 'customer_accounts',
|
|
26
|
+
apis: [
|
|
27
|
+
{
|
|
28
|
+
path: '/customer_accounts/admin/users-invite',
|
|
29
|
+
metadata: { POST: { requireAuth: true } },
|
|
30
|
+
handlers: { POST: async () => new Response(null) },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
method: 'POST',
|
|
34
|
+
path: '/customer_accounts/legacy-create',
|
|
35
|
+
handler: async () => new Response(null),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
} as unknown as Module,
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const manifests: ApiRouteManifestEntry[] = [
|
|
43
|
+
{
|
|
44
|
+
moduleId: 'customer_accounts',
|
|
45
|
+
kind: 'route-file',
|
|
46
|
+
path: '/customer_accounts/admin/users-invite',
|
|
47
|
+
methods: ['POST'],
|
|
48
|
+
load: async () => ({ openApi: routeFileOpenApi }),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
moduleId: 'customer_accounts',
|
|
52
|
+
kind: 'legacy',
|
|
53
|
+
method: 'POST',
|
|
54
|
+
path: '/customer_accounts/legacy-create',
|
|
55
|
+
methods: ['POST'],
|
|
56
|
+
load: async () => ({ openApi: legacyOpenApi }),
|
|
57
|
+
},
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
describe('attachOpenApiDocsToModules', () => {
|
|
61
|
+
it('re-attaches route docs the runtime registry omits, restoring request bodies (#4361)', async () => {
|
|
62
|
+
const bare = buildOpenApiDocument(makeModules())
|
|
63
|
+
const barePost = bare.paths['/customer_accounts/admin/users-invite']?.post as Record<string, any>
|
|
64
|
+
expect(barePost.requestBody).toBeUndefined()
|
|
65
|
+
|
|
66
|
+
const enriched = await attachOpenApiDocsToModules(makeModules(), manifests)
|
|
67
|
+
const doc = buildOpenApiDocument(enriched)
|
|
68
|
+
|
|
69
|
+
const post = doc.paths['/customer_accounts/admin/users-invite']?.post as Record<string, any>
|
|
70
|
+
expect(post.summary).toBe('Invite user')
|
|
71
|
+
const bodySchema = post.requestBody?.content?.['application/json']?.schema
|
|
72
|
+
expect(bodySchema?.properties?.email).toBeDefined()
|
|
73
|
+
expect(bodySchema?.required).toContain('email')
|
|
74
|
+
|
|
75
|
+
const legacyPost = doc.paths['/customer_accounts/legacy-create']?.post as Record<string, any>
|
|
76
|
+
expect(legacyPost.summary).toBe('Legacy create')
|
|
77
|
+
expect(legacyPost.requestBody?.content?.['application/json']?.schema?.properties?.name).toBeDefined()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('keeps entries untouched when no manifest matches or loading fails', async () => {
|
|
81
|
+
const failing: ApiRouteManifestEntry[] = [
|
|
82
|
+
{
|
|
83
|
+
moduleId: 'customer_accounts',
|
|
84
|
+
kind: 'route-file',
|
|
85
|
+
path: '/customer_accounts/admin/users-invite',
|
|
86
|
+
methods: ['POST'],
|
|
87
|
+
load: async () => {
|
|
88
|
+
throw new Error('boom')
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
]
|
|
92
|
+
const enriched = await attachOpenApiDocsToModules(makeModules(), failing)
|
|
93
|
+
const apis = enriched[0].apis ?? []
|
|
94
|
+
expect((apis[0] as { docs?: unknown }).docs).toBeUndefined()
|
|
95
|
+
expect((apis[1] as { docs?: unknown }).docs).toBeUndefined()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('does not overwrite docs that are already present', async () => {
|
|
99
|
+
const modules = makeModules()
|
|
100
|
+
const existingDocs = { methods: { POST: { summary: 'Existing' } } }
|
|
101
|
+
;(modules[0].apis?.[0] as { docs?: unknown }).docs = existingDocs
|
|
102
|
+
const enriched = await attachOpenApiDocsToModules(modules, manifests)
|
|
103
|
+
expect((enriched[0].apis?.[0] as { docs?: unknown }).docs).toBe(existingDocs)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApiRouteManifestEntry,
|
|
3
|
+
Module,
|
|
4
|
+
ModuleApi,
|
|
5
|
+
ModuleApiLegacy,
|
|
6
|
+
ModuleApiRouteFile,
|
|
7
|
+
} from '../../modules/registry'
|
|
8
|
+
import type { OpenApiMethodDoc, OpenApiRouteDoc } from './types'
|
|
9
|
+
|
|
10
|
+
function manifestKey(entry: ApiRouteManifestEntry): string {
|
|
11
|
+
return entry.kind === 'legacy' && entry.method
|
|
12
|
+
? `${entry.moduleId}\0${entry.method}\0${entry.path}`
|
|
13
|
+
: `${entry.moduleId}\0${entry.path}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function loadOpenApiExport(entry: ApiRouteManifestEntry | undefined): Promise<unknown> {
|
|
17
|
+
if (!entry) return undefined
|
|
18
|
+
try {
|
|
19
|
+
const mod = await entry.load()
|
|
20
|
+
const openApi = (mod as { openApi?: unknown }).openApi
|
|
21
|
+
return openApi && typeof openApi === 'object' ? openApi : undefined
|
|
22
|
+
} catch {
|
|
23
|
+
return undefined
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The lazy runtime module registry omits each route's `docs` (the `openApi`
|
|
29
|
+
* export) so route files stay dynamically imported. Documentation endpoints
|
|
30
|
+
* that build the OpenAPI document from that registry must re-attach the docs
|
|
31
|
+
* by loading each route module through the API route manifests on demand.
|
|
32
|
+
*/
|
|
33
|
+
export async function attachOpenApiDocsToModules(
|
|
34
|
+
modules: Module[],
|
|
35
|
+
manifests: ApiRouteManifestEntry[],
|
|
36
|
+
): Promise<Module[]> {
|
|
37
|
+
const manifestsByKey = new Map<string, ApiRouteManifestEntry>()
|
|
38
|
+
for (const entry of manifests) {
|
|
39
|
+
manifestsByKey.set(manifestKey(entry), entry)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return Promise.all(
|
|
43
|
+
modules.map(async (mod) => {
|
|
44
|
+
if (!Array.isArray(mod.apis) || !mod.apis.length) return mod
|
|
45
|
+
const apis = await Promise.all(
|
|
46
|
+
mod.apis.map(async (api): Promise<ModuleApi> => {
|
|
47
|
+
if ((api as { docs?: unknown }).docs) return api
|
|
48
|
+
if ('handlers' in api) {
|
|
49
|
+
const entry = manifestsByKey.get(`${mod.id}\0${api.path}`)
|
|
50
|
+
const docs = await loadOpenApiExport(entry)
|
|
51
|
+
if (!docs) return api
|
|
52
|
+
return { ...api, docs: docs as OpenApiRouteDoc } satisfies ModuleApiRouteFile
|
|
53
|
+
}
|
|
54
|
+
const entry = manifestsByKey.get(`${mod.id}\0${api.method}\0${api.path}`)
|
|
55
|
+
const docs = await loadOpenApiExport(entry)
|
|
56
|
+
if (!docs) return api
|
|
57
|
+
return { ...api, docs: docs as OpenApiMethodDoc } satisfies ModuleApiLegacy
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
return { ...mod, apis }
|
|
61
|
+
}),
|
|
62
|
+
)
|
|
63
|
+
}
|
package/src/lib/openapi/index.ts
CHANGED