@classytic/arc 2.11.4 → 2.14.0
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/README.md +16 -12
- package/dist/{BaseController-swXruJ2_.mjs → BaseController-DX_T-bDB.mjs} +388 -423
- package/dist/EventTransport-CT_52aWU.d.mts +34 -0
- package/dist/EventTransport-DLWoUMHy.mjs +103 -0
- package/dist/{ResourceRegistry-DkAeAuTX.mjs → ResourceRegistry-CTERg_2x.mjs} +139 -66
- package/dist/audit/index.d.mts +2 -2
- package/dist/audit/index.mjs +1 -1
- package/dist/auth/audit.d.mts +199 -0
- package/dist/auth/audit.mjs +288 -0
- package/dist/auth/index.d.mts +3 -3
- package/dist/auth/index.mjs +117 -191
- package/dist/{betterAuthOpenApi-DwxtK3uG.mjs → betterAuthOpenApi--M_i87dQ.mjs} +1 -1
- package/dist/buildHandler-olo-gt94.mjs +610 -0
- package/dist/cache/index.mjs +3 -3
- package/dist/cli/commands/describe.d.mts +89 -13
- package/dist/cli/commands/describe.mjs +56 -2
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +147 -48
- package/dist/cli/commands/init.d.mts +13 -0
- package/dist/cli/commands/init.mjs +130 -87
- package/dist/cli/commands/introspect.mjs +8 -1
- package/dist/context/index.mjs +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +5 -5
- package/dist/core-DECn6zaU.mjs +1399 -0
- package/dist/{createActionRouter-CIKOcNA7.mjs → createActionRouter-CBxLLbn3.mjs} +7 -20
- package/dist/createAggregationRouter-CRIBv4sC.mjs +114 -0
- package/dist/{createApp-C9bRrqlX.mjs → createApp-XX2-N0Yd.mjs} +28 -22
- package/dist/{defineEvent-D1Ky9M1D.mjs → defineEvent-D5h7EvAx.mjs} +1 -1
- package/dist/docs/index.d.mts +24 -11
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-DOFoxoDs.mjs → elevation-DgoeTyfX.mjs} +1 -1
- package/dist/errorHandler-Bk-AGhkU.mjs +174 -0
- package/dist/errorHandler-DFr45ZG4.d.mts +45 -0
- package/dist/errors-j4aJm1Wg.mjs +184 -0
- package/dist/{eventPlugin-Cts2-Tfj.mjs → eventPlugin-CaKTYkYM.mjs} +28 -4
- package/dist/{eventPlugin-DDJoNEPL.d.mts → eventPlugin-qXpqTebY.d.mts} +24 -1
- package/dist/events/index.d.mts +6 -6
- package/dist/events/index.mjs +11 -35
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +2 -2
- package/dist/factory/index.mjs +2 -2
- package/dist/{fields-BRjxOAFp.d.mts → fields-COhcH3fk.d.mts} +23 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +1 -1
- package/dist/idempotency/index.mjs +1 -20
- package/dist/idempotency/redis.mjs +1 -1
- package/dist/{index-rHjXmJar.d.mts → index-BTqLEvhu.d.mts} +163 -3
- package/dist/{index-CXXRbnf8.d.mts → index-BtW7qYwa.d.mts} +660 -326
- package/dist/{index-m8mOOlFW.d.mts → index-Ds61mrJE.d.mts} +50 -4
- package/dist/{index-D9t1KNaB.d.mts → index-Dz5IKsrE.d.mts} +360 -219
- package/dist/index.d.mts +6 -7
- package/dist/index.mjs +9 -10
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/streamline.d.mts +60 -11
- package/dist/integrations/streamline.mjs +75 -85
- package/dist/integrations/websocket.mjs +2 -8
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +2 -2
- package/dist/migrations/index.d.mts +23 -3
- package/dist/migrations/index.mjs +0 -7
- package/dist/{multipartBody-CvTR1Un6.mjs → multipartBody-BOvVSVCD.mjs} +11 -8
- package/dist/openapi-noXno2CV.mjs +968 -0
- package/dist/org/index.d.mts +2 -2
- package/dist/org/index.mjs +1 -1
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +3 -3
- package/dist/{permissions-gd_aUWrR.mjs → permissions-ohQyv50e.mjs} +404 -176
- package/dist/{pipe-DVoIheVC.mjs → pipe-Zr0KXjQe.mjs} +1 -1
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +1 -1
- package/dist/plugins/index.d.mts +16 -31
- package/dist/plugins/index.mjs +33 -13
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +4 -4
- package/dist/presets/filesUpload.mjs +6 -9
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +2 -2
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +6 -8
- package/dist/{presets-Z7P5w4gF.mjs → presets-BbkjdPeH.mjs} +6 -28
- package/dist/{queryCachePlugin-Bq6bO6vc.mjs → queryCachePlugin-m1XsgAIJ.mjs} +3 -3
- package/dist/{redis-stream-xTGxB2bm.d.mts → redis-stream-D6HzR1Z_.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{replyHelpers-ByllIXXV.mjs → replyHelpers-CK-FNO8E.mjs} +3 -21
- package/dist/{resourceToTools-CxNmI6xF.mjs → resourceToTools-DLL32us3.mjs} +224 -71
- package/dist/{routerShared-BqLRb5l7.mjs → routerShared-DrOa-26E.mjs} +41 -36
- package/dist/{schemaIR-Dy2p4MxS.mjs → schemaIR-lYhC2gE5.mjs} +1 -1
- package/dist/schemas/index.d.mts +100 -30
- package/dist/schemas/index.mjs +86 -29
- package/dist/scim/index.d.mts +264 -0
- package/dist/scim/index.mjs +963 -0
- package/dist/scope/index.d.mts +3 -3
- package/dist/scope/index.mjs +4 -4
- package/dist/{sse-V7aXc3bW.mjs → sse-Bz-5ZeTt.mjs} +1 -1
- package/dist/{store-helpers-Cp4uKC1U.mjs → store-helpers-BkIN9-vu.mjs} +1 -1
- package/dist/testing/index.d.mts +2 -8
- package/dist/testing/index.mjs +16 -24
- package/dist/types/index.d.mts +4 -4
- package/dist/{types-D7KpfiL1.d.mts → types-BvqwCCSx.d.mts} +73 -25
- package/dist/{types-DDyTPc6y.d.mts → types-CTYvcwHe.d.mts} +195 -1
- package/dist/{types-AOD8fxIw.mjs → types-C_s5moIu.mjs} +117 -1
- package/dist/{types-BQ9TJQNy.d.mts → types-DQHFc8PM.d.mts} +1 -1
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +5 -5
- package/dist/{utils-CcYTj09l.mjs → utils-_h9B3c57.mjs} +1269 -1334
- package/dist/{versioning-DsglKfM_.d.mts → versioning-DTTvc80y.d.mts} +1 -1
- package/package.json +24 -34
- package/skills/arc/SKILL.md +147 -51
- package/skills/arc/references/agent-auth.md +238 -0
- package/skills/arc/references/api-reference.md +187 -0
- package/skills/arc/references/auth.md +354 -7
- package/skills/arc/references/enterprise-auth.md +94 -0
- package/skills/arc/references/events.md +8 -6
- package/skills/arc/references/mcp.md +2 -2
- package/skills/arc/references/multi-tenancy.md +11 -2
- package/skills/arc/references/production.md +10 -9
- package/skills/arc/references/scim.md +247 -0
- package/skills/arc/references/testing.md +1 -1
- package/skills/arc-code-review/SKILL.md +141 -0
- package/skills/arc-code-review/references/anti-patterns.md +911 -0
- package/skills/arc-code-review/references/arc-cheatsheet.md +380 -0
- package/skills/arc-code-review/references/migration-recipes.md +700 -0
- package/skills/arc-code-review/references/mongokit-migration.md +386 -0
- package/skills/arc-code-review/references/scaffolding.md +230 -0
- package/skills/arc-code-review/references/severity.md +127 -0
- package/dist/EventTransport-BFQjw9pB.mjs +0 -133
- package/dist/EventTransport-CYNUXdCJ.d.mts +0 -293
- package/dist/adapters/index.d.mts +0 -3
- package/dist/adapters/index.mjs +0 -2
- package/dist/adapters-DUUiiimH.mjs +0 -964
- package/dist/auth/mongoose.d.mts +0 -191
- package/dist/auth/mongoose.mjs +0 -73
- package/dist/core-CbcQRIch.mjs +0 -1054
- package/dist/errorHandler-BQm8ZxTK.mjs +0 -173
- package/dist/errorHandler-DEWmGWPz.d.mts +0 -114
- package/dist/errors-D5c-5BJL.mjs +0 -232
- package/dist/index-Rg8axYPz.d.mts +0 -370
- package/dist/openapi-D7G1V7ex.mjs +0 -557
- /package/dist/{HookSystem-CGsMd6oK.mjs → HookSystem-Iiebom92.mjs} +0 -0
- /package/dist/{actionPermissions-sUUKDhtP.mjs → actionPermissions-CyUkQu6O.mjs} +0 -0
- /package/dist/{caching-CheW3m-S.mjs → caching-SM8gghN6.mjs} +0 -0
- /package/dist/{constants-BhY1OHoH.mjs → constants-Cxde4rpC.mjs} +0 -0
- /package/dist/{elevation-BQQXZ_VR.d.mts → elevation-BXOWoGCF.d.mts} +0 -0
- /package/dist/{keys-CARyUjiR.mjs → keys-CGcCbNyu.mjs} +0 -0
- /package/dist/{loadResources-CPpkyKfM.mjs → loadResources-DBMQg_Aj.mjs} +0 -0
- /package/dist/{memory-DikHSvWa.mjs → memory-UBydS5ku.mjs} +0 -0
- /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
- /package/dist/{pluralize-CWP6MB39.mjs → pluralize-DQgqgifU.mjs} +0 -0
- /package/dist/{registry-D63ee7fl.mjs → registry-I-ogLgL9.mjs} +0 -0
- /package/dist/{requestContext-C5XeK3VA.mjs → requestContext-SSaaTgW8.mjs} +0 -0
- /package/dist/{schemaConverter-B0oKLuqI.mjs → schemaConverter-De34B1ZG.mjs} +0 -0
- /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-BzkXkvVv.mjs} +0 -0
- /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
- /package/dist/{versioning-CGPjkqAg.mjs → versioning-BUrT5aP4.mjs} +0 -0
|
@@ -1,557 +0,0 @@
|
|
|
1
|
-
import { t as getUserRoles } from "./types-DV9WDfeg.mjs";
|
|
2
|
-
import { n as convertRouteSchema } from "./schemaConverter-B0oKLuqI.mjs";
|
|
3
|
-
import { t as resolveActionPermission } from "./actionPermissions-sUUKDhtP.mjs";
|
|
4
|
-
import { t as buildActionBodySchema } from "./createActionRouter-CIKOcNA7.mjs";
|
|
5
|
-
import fp from "fastify-plugin";
|
|
6
|
-
//#region src/docs/openapi.ts
|
|
7
|
-
const openApiPlugin = async (fastify, opts = {}) => {
|
|
8
|
-
const { title = "Arc API", version = "1.0.0", description, serverUrl, prefix = "/_docs", apiPrefix = "", authRoles = [] } = opts;
|
|
9
|
-
const buildSpec = () => {
|
|
10
|
-
const arc = fastify.arc;
|
|
11
|
-
const resources = arc?.registry?.getAll() ?? [];
|
|
12
|
-
const externalPaths = arc?.externalOpenApiPaths ?? [];
|
|
13
|
-
return buildOpenApiSpec(resources, {
|
|
14
|
-
title,
|
|
15
|
-
version,
|
|
16
|
-
description,
|
|
17
|
-
serverUrl,
|
|
18
|
-
apiPrefix
|
|
19
|
-
}, externalPaths.length > 0 ? externalPaths : void 0);
|
|
20
|
-
};
|
|
21
|
-
fastify.get(`${prefix}/openapi.json`, async (request, reply) => {
|
|
22
|
-
if (authRoles.length > 0) {
|
|
23
|
-
const user = request.user;
|
|
24
|
-
const roles = getUserRoles(user);
|
|
25
|
-
if (!authRoles.some((r) => roles.includes(r)) && !roles.includes("superadmin")) {
|
|
26
|
-
reply.code(403).send({ error: "Access denied" });
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return buildSpec();
|
|
31
|
-
});
|
|
32
|
-
fastify.log?.debug?.(`OpenAPI spec available at ${prefix}/openapi.json`);
|
|
33
|
-
};
|
|
34
|
-
/**
|
|
35
|
-
* Build OpenAPI spec from registry resources.
|
|
36
|
-
* Shared by HTTP docs endpoint and CLI export command.
|
|
37
|
-
*/
|
|
38
|
-
function buildOpenApiSpec(resources, options = {}, externalPaths) {
|
|
39
|
-
const { title = "Arc API", version = "1.0.0", description, serverUrl, apiPrefix = "" } = options;
|
|
40
|
-
const paths = {};
|
|
41
|
-
const tags = [];
|
|
42
|
-
const additionalSecurity = externalPaths?.flatMap((ext) => ext.resourceSecurity ?? []) ?? [];
|
|
43
|
-
for (const resource of resources) {
|
|
44
|
-
const tagDescParts = [`${resource.displayName || resource.name} operations`];
|
|
45
|
-
if (resource.presets && resource.presets.length > 0) tagDescParts.push(`Presets: ${resource.presets.join(", ")}`);
|
|
46
|
-
if (resource.pipelineSteps && resource.pipelineSteps.length > 0) {
|
|
47
|
-
const stepNames = resource.pipelineSteps.map((s) => `${s.type}(${s.name})`);
|
|
48
|
-
tagDescParts.push(`Pipeline: ${stepNames.join(" → ")}`);
|
|
49
|
-
}
|
|
50
|
-
if (resource.events && resource.events.length > 0) tagDescParts.push(`Events: ${resource.events.join(", ")}`);
|
|
51
|
-
tags.push({
|
|
52
|
-
name: resource.tag || resource.name,
|
|
53
|
-
description: tagDescParts.join(". ")
|
|
54
|
-
});
|
|
55
|
-
const resourcePaths = generateResourcePaths(resource, apiPrefix, additionalSecurity);
|
|
56
|
-
Object.assign(paths, resourcePaths);
|
|
57
|
-
}
|
|
58
|
-
if (externalPaths) for (const ext of externalPaths) {
|
|
59
|
-
for (const [path, methods] of Object.entries(ext.paths)) paths[path] = paths[path] ? {
|
|
60
|
-
...paths[path],
|
|
61
|
-
...methods
|
|
62
|
-
} : methods;
|
|
63
|
-
if (ext.tags) {
|
|
64
|
-
for (const tag of ext.tags) if (!tags.find((t) => t.name === tag.name)) tags.push(tag);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
const externalSecuritySchemes = externalPaths?.reduce((acc, ext) => ({
|
|
68
|
-
...acc,
|
|
69
|
-
...ext.securitySchemes
|
|
70
|
-
}), {}) ?? {};
|
|
71
|
-
const externalSchemas = externalPaths?.reduce((acc, ext) => ({
|
|
72
|
-
...acc,
|
|
73
|
-
...ext.schemas
|
|
74
|
-
}), {}) ?? {};
|
|
75
|
-
return {
|
|
76
|
-
openapi: "3.0.3",
|
|
77
|
-
info: {
|
|
78
|
-
title,
|
|
79
|
-
version,
|
|
80
|
-
...description && { description }
|
|
81
|
-
},
|
|
82
|
-
...serverUrl && { servers: [{ url: serverUrl }] },
|
|
83
|
-
paths,
|
|
84
|
-
components: {
|
|
85
|
-
schemas: {
|
|
86
|
-
...generateSchemas(resources),
|
|
87
|
-
...externalSchemas
|
|
88
|
-
},
|
|
89
|
-
securitySchemes: {
|
|
90
|
-
bearerAuth: {
|
|
91
|
-
type: "http",
|
|
92
|
-
scheme: "bearer",
|
|
93
|
-
bearerFormat: "JWT"
|
|
94
|
-
},
|
|
95
|
-
orgHeader: {
|
|
96
|
-
type: "apiKey",
|
|
97
|
-
in: "header",
|
|
98
|
-
name: "x-organization-id"
|
|
99
|
-
},
|
|
100
|
-
...externalSecuritySchemes
|
|
101
|
-
}
|
|
102
|
-
},
|
|
103
|
-
tags
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Convert Fastify-style params (/:id) to OpenAPI-style params (/{id})
|
|
108
|
-
*/
|
|
109
|
-
function toOpenApiPath(path) {
|
|
110
|
-
return path.replace(/:([^/]+)/g, "{$1}");
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Convert OpenAPI schema to query parameters array
|
|
114
|
-
* Transforms { properties: { page: { type: 'integer' } } } to [{ name: 'page', in: 'query', schema: { type: 'integer' } }]
|
|
115
|
-
*/
|
|
116
|
-
function convertSchemaToParameters(schema) {
|
|
117
|
-
const params = [];
|
|
118
|
-
const properties = schema.properties || {};
|
|
119
|
-
const required = schema.required || [];
|
|
120
|
-
for (const [name, prop] of Object.entries(properties)) {
|
|
121
|
-
const description = prop.description;
|
|
122
|
-
const { description: _, ...schemaProps } = prop;
|
|
123
|
-
const param = {
|
|
124
|
-
name,
|
|
125
|
-
in: "query",
|
|
126
|
-
required: required.includes(name),
|
|
127
|
-
schema: schemaProps
|
|
128
|
-
};
|
|
129
|
-
if (description) param.description = description;
|
|
130
|
-
params.push(param);
|
|
131
|
-
}
|
|
132
|
-
return params;
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Default query parameters when no listQuery schema is provided
|
|
136
|
-
*/
|
|
137
|
-
const DEFAULT_LIST_PARAMS = [
|
|
138
|
-
{
|
|
139
|
-
name: "page",
|
|
140
|
-
in: "query",
|
|
141
|
-
schema: { type: "integer" },
|
|
142
|
-
description: "Page number"
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
name: "limit",
|
|
146
|
-
in: "query",
|
|
147
|
-
schema: { type: "integer" },
|
|
148
|
-
description: "Items per page"
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
name: "sort",
|
|
152
|
-
in: "query",
|
|
153
|
-
schema: { type: "string" },
|
|
154
|
-
description: "Sort field (prefix with - for descending)"
|
|
155
|
-
}
|
|
156
|
-
];
|
|
157
|
-
/**
|
|
158
|
-
* Generate paths for a resource
|
|
159
|
-
*/
|
|
160
|
-
function generateResourcePaths(resource, apiPrefix = "", additionalSecurity = []) {
|
|
161
|
-
const paths = {};
|
|
162
|
-
const basePath = `${apiPrefix}${resource.prefix}`;
|
|
163
|
-
if (resource.disableDefaultRoutes && (!resource.customRoutes || resource.customRoutes.length === 0) && (!resource.actions || resource.actions.length === 0)) return paths;
|
|
164
|
-
if (!resource.disableDefaultRoutes) {
|
|
165
|
-
const disabledSet = new Set(resource.disabledRoutes ?? []);
|
|
166
|
-
const updateMethod = resource.updateMethod ?? "PATCH";
|
|
167
|
-
const collectionPath = {};
|
|
168
|
-
if (!disabledSet.has("list")) collectionPath.get = createOperation(resource, "list", "List all", {
|
|
169
|
-
parameters: resource.openApiSchemas?.listQuery ? convertSchemaToParameters(resource.openApiSchemas.listQuery) : DEFAULT_LIST_PARAMS,
|
|
170
|
-
responses: { "200": {
|
|
171
|
-
description: "List of items",
|
|
172
|
-
content: { "application/json": { schema: {
|
|
173
|
-
type: "object",
|
|
174
|
-
properties: {
|
|
175
|
-
success: { type: "boolean" },
|
|
176
|
-
docs: {
|
|
177
|
-
type: "array",
|
|
178
|
-
items: { $ref: `#/components/schemas/${resource.name}` }
|
|
179
|
-
},
|
|
180
|
-
page: { type: "integer" },
|
|
181
|
-
limit: { type: "integer" },
|
|
182
|
-
total: { type: "integer" },
|
|
183
|
-
pages: { type: "integer" },
|
|
184
|
-
hasNext: { type: "boolean" },
|
|
185
|
-
hasPrev: { type: "boolean" }
|
|
186
|
-
}
|
|
187
|
-
} } }
|
|
188
|
-
} }
|
|
189
|
-
}, void 0, additionalSecurity);
|
|
190
|
-
if (!disabledSet.has("create")) collectionPath.post = createOperation(resource, "create", "Create new", {
|
|
191
|
-
requestBody: {
|
|
192
|
-
required: true,
|
|
193
|
-
content: { "application/json": { schema: { $ref: `#/components/schemas/${resource.name}Input` } } }
|
|
194
|
-
},
|
|
195
|
-
responses: { "201": {
|
|
196
|
-
description: "Created successfully",
|
|
197
|
-
content: { "application/json": { schema: {
|
|
198
|
-
type: "object",
|
|
199
|
-
properties: {
|
|
200
|
-
success: { type: "boolean" },
|
|
201
|
-
data: { $ref: `#/components/schemas/${resource.name}` },
|
|
202
|
-
message: { type: "string" }
|
|
203
|
-
}
|
|
204
|
-
} } }
|
|
205
|
-
} }
|
|
206
|
-
}, void 0, additionalSecurity);
|
|
207
|
-
if (Object.keys(collectionPath).length > 0) paths[basePath] = collectionPath;
|
|
208
|
-
const itemPath = {};
|
|
209
|
-
if (!disabledSet.has("get")) itemPath.get = createOperation(resource, "get", "Get by ID", {
|
|
210
|
-
parameters: [{
|
|
211
|
-
name: "id",
|
|
212
|
-
in: "path",
|
|
213
|
-
required: true,
|
|
214
|
-
schema: { type: "string" }
|
|
215
|
-
}],
|
|
216
|
-
responses: {
|
|
217
|
-
"200": {
|
|
218
|
-
description: "Item found",
|
|
219
|
-
content: { "application/json": { schema: {
|
|
220
|
-
type: "object",
|
|
221
|
-
properties: {
|
|
222
|
-
success: { type: "boolean" },
|
|
223
|
-
data: { $ref: `#/components/schemas/${resource.name}` }
|
|
224
|
-
}
|
|
225
|
-
} } }
|
|
226
|
-
},
|
|
227
|
-
"404": { description: "Not found" }
|
|
228
|
-
}
|
|
229
|
-
}, void 0, additionalSecurity);
|
|
230
|
-
if (!disabledSet.has("update")) {
|
|
231
|
-
const updateOp = createOperation(resource, "update", "Update", {
|
|
232
|
-
parameters: [{
|
|
233
|
-
name: "id",
|
|
234
|
-
in: "path",
|
|
235
|
-
required: true,
|
|
236
|
-
schema: { type: "string" }
|
|
237
|
-
}],
|
|
238
|
-
requestBody: {
|
|
239
|
-
required: true,
|
|
240
|
-
content: { "application/json": { schema: { $ref: `#/components/schemas/${resource.name}Input` } } }
|
|
241
|
-
},
|
|
242
|
-
responses: { "200": {
|
|
243
|
-
description: "Updated successfully",
|
|
244
|
-
content: { "application/json": { schema: {
|
|
245
|
-
type: "object",
|
|
246
|
-
properties: {
|
|
247
|
-
success: { type: "boolean" },
|
|
248
|
-
data: { $ref: `#/components/schemas/${resource.name}` },
|
|
249
|
-
message: { type: "string" }
|
|
250
|
-
}
|
|
251
|
-
} } }
|
|
252
|
-
} }
|
|
253
|
-
}, void 0, additionalSecurity);
|
|
254
|
-
if (updateMethod === "both") {
|
|
255
|
-
itemPath.put = updateOp;
|
|
256
|
-
itemPath.patch = updateOp;
|
|
257
|
-
} else if (updateMethod === "PUT") itemPath.put = updateOp;
|
|
258
|
-
else itemPath.patch = updateOp;
|
|
259
|
-
}
|
|
260
|
-
if (!disabledSet.has("delete")) itemPath.delete = createOperation(resource, "delete", "Delete", {
|
|
261
|
-
parameters: [{
|
|
262
|
-
name: "id",
|
|
263
|
-
in: "path",
|
|
264
|
-
required: true,
|
|
265
|
-
schema: { type: "string" }
|
|
266
|
-
}],
|
|
267
|
-
responses: { "200": {
|
|
268
|
-
description: "Deleted successfully",
|
|
269
|
-
content: { "application/json": { schema: {
|
|
270
|
-
type: "object",
|
|
271
|
-
properties: {
|
|
272
|
-
success: { type: "boolean" },
|
|
273
|
-
message: { type: "string" }
|
|
274
|
-
}
|
|
275
|
-
} } }
|
|
276
|
-
} }
|
|
277
|
-
}, void 0, additionalSecurity);
|
|
278
|
-
if (Object.keys(itemPath).length > 0) paths[toOpenApiPath(`${basePath}/:id`)] = itemPath;
|
|
279
|
-
}
|
|
280
|
-
for (const route of resource.customRoutes || []) {
|
|
281
|
-
const fullPath = toOpenApiPath(`${basePath}${route.path}`);
|
|
282
|
-
const method = route.method.toLowerCase();
|
|
283
|
-
if (!paths[fullPath]) paths[fullPath] = {};
|
|
284
|
-
const handlerName = route.operation ?? (typeof route.handler === "string" ? route.handler : "handler");
|
|
285
|
-
const isPublicRoute = route.permissions?._isPublic === true;
|
|
286
|
-
const requiresAuthForRoute = !!route.permissions && !isPublicRoute;
|
|
287
|
-
const extras = {
|
|
288
|
-
parameters: extractPathParams(route.path),
|
|
289
|
-
responses: { "200": { description: route.description || "Success" } }
|
|
290
|
-
};
|
|
291
|
-
const rawSchema = route.schema;
|
|
292
|
-
const routeSchema = rawSchema ? convertRouteSchema(rawSchema, "openapi-3.0") : void 0;
|
|
293
|
-
if (routeSchema?.body && [
|
|
294
|
-
"post",
|
|
295
|
-
"put",
|
|
296
|
-
"patch"
|
|
297
|
-
].includes(method)) extras.requestBody = {
|
|
298
|
-
required: true,
|
|
299
|
-
content: { "application/json": { schema: routeSchema.body } }
|
|
300
|
-
};
|
|
301
|
-
if (routeSchema?.querystring) {
|
|
302
|
-
const queryParams = convertSchemaToParameters(routeSchema.querystring);
|
|
303
|
-
extras.parameters = [...extras.parameters || [], ...queryParams];
|
|
304
|
-
}
|
|
305
|
-
if (routeSchema?.response) {
|
|
306
|
-
const responseSchemas = routeSchema.response;
|
|
307
|
-
for (const [statusCode, schema] of Object.entries(responseSchemas)) extras.responses[statusCode] = {
|
|
308
|
-
description: schema.description || `Response ${statusCode}`,
|
|
309
|
-
content: { "application/json": { schema } }
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
paths[fullPath][method] = createOperation(resource, handlerName, route.summary ?? handlerName, extras, requiresAuthForRoute, additionalSecurity);
|
|
313
|
-
}
|
|
314
|
-
if (resource.actions && resource.actions.length > 0) {
|
|
315
|
-
const actionPath = toOpenApiPath(`${basePath}/:id/action`);
|
|
316
|
-
const actionEnum = resource.actions.map((a) => a.name);
|
|
317
|
-
const actionSchemas = {};
|
|
318
|
-
for (const a of resource.actions) if (a.schema) actionSchemas[a.name] = a.schema;
|
|
319
|
-
const bodySchema = buildActionBodySchema(actionEnum, actionSchemas);
|
|
320
|
-
const descLines = [
|
|
321
|
-
"Unified action endpoint for state transitions.",
|
|
322
|
-
"",
|
|
323
|
-
"**Available actions:**"
|
|
324
|
-
];
|
|
325
|
-
for (const a of resource.actions) {
|
|
326
|
-
const roles = a.permissions?._roles;
|
|
327
|
-
const roleStr = roles?.length ? ` — requires: ${roles.join(" or ")}` : "";
|
|
328
|
-
const descStr = a.description ? ` — ${a.description}` : "";
|
|
329
|
-
descLines.push(`- \`${a.name}\`${roleStr}${descStr}`);
|
|
330
|
-
}
|
|
331
|
-
const anyAuthRequired = resource.actions.some((a) => {
|
|
332
|
-
const effective = resolveActionPermission({
|
|
333
|
-
action: { permissions: a.permissions },
|
|
334
|
-
resourcePermissions: resource.permissions,
|
|
335
|
-
resourceActionPermissions: resource.actionPermissions
|
|
336
|
-
});
|
|
337
|
-
return typeof effective === "function" && !effective._isPublic;
|
|
338
|
-
});
|
|
339
|
-
if (!paths[actionPath]) paths[actionPath] = {};
|
|
340
|
-
paths[actionPath].post = createOperation(resource, "action", `Perform action (${actionEnum.join(" / ")})`, {
|
|
341
|
-
parameters: [{
|
|
342
|
-
name: "id",
|
|
343
|
-
in: "path",
|
|
344
|
-
required: true,
|
|
345
|
-
schema: { type: "string" },
|
|
346
|
-
description: "Resource ID"
|
|
347
|
-
}],
|
|
348
|
-
description: descLines.join("\n"),
|
|
349
|
-
requestBody: {
|
|
350
|
-
required: true,
|
|
351
|
-
content: { "application/json": { schema: bodySchema } }
|
|
352
|
-
},
|
|
353
|
-
responses: {
|
|
354
|
-
"200": { description: "Action executed successfully" },
|
|
355
|
-
"400": { description: "Invalid action or missing required fields" },
|
|
356
|
-
"401": { description: "Authentication required" },
|
|
357
|
-
"403": { description: "Permission denied" }
|
|
358
|
-
}
|
|
359
|
-
}, anyAuthRequired, additionalSecurity);
|
|
360
|
-
}
|
|
361
|
-
return paths;
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* Create an operation object
|
|
365
|
-
* @param requiresAuthOverride - Override for whether auth is required (for additional routes)
|
|
366
|
-
* @param additionalSecurity - Extra security alternatives from external integrations (OR'd with bearerAuth)
|
|
367
|
-
*/
|
|
368
|
-
function createOperation(resource, operation, summary, extras, requiresAuthOverride, additionalSecurity = []) {
|
|
369
|
-
const operationPermission = (resource.permissions || {})[operation];
|
|
370
|
-
const isPublic = operationPermission?._isPublic === true;
|
|
371
|
-
operationPermission?._roles;
|
|
372
|
-
const requiresAuth = requiresAuthOverride !== void 0 ? requiresAuthOverride : typeof operationPermission === "function" && !isPublic;
|
|
373
|
-
const permAnnotation = describePermissionForOpenApi(operationPermission);
|
|
374
|
-
const descParts = [];
|
|
375
|
-
if (permAnnotation) descParts.push(`**Permission**: ${permAnnotation.type === "public" ? "Public" : permAnnotation.type === "requireRoles" ? `Requires roles: ${(permAnnotation.roles ?? []).join(", ")}` : "Requires authentication"}`);
|
|
376
|
-
if (resource.presets && resource.presets.length > 0) descParts.push(`**Presets**: ${resource.presets.join(", ")}`);
|
|
377
|
-
const applicableSteps = (resource.pipelineSteps ?? []).filter((s) => {
|
|
378
|
-
if (!s.operations) return true;
|
|
379
|
-
return s.operations.includes(operation);
|
|
380
|
-
});
|
|
381
|
-
return {
|
|
382
|
-
tags: [resource.tag || "Resource"],
|
|
383
|
-
summary: `${summary} ${(resource.displayName || resource.name).toLowerCase()}`,
|
|
384
|
-
operationId: `${resource.name}_${operation}`,
|
|
385
|
-
...descParts.length > 0 && { description: descParts.join("\n\n") },
|
|
386
|
-
...requiresAuth && { security: [{ bearerAuth: [] }, ...additionalSecurity] },
|
|
387
|
-
...permAnnotation && { "x-arc-permission": permAnnotation },
|
|
388
|
-
...applicableSteps.length > 0 && { "x-arc-pipeline": applicableSteps.map((s) => ({
|
|
389
|
-
type: s.type,
|
|
390
|
-
name: s.name
|
|
391
|
-
})) },
|
|
392
|
-
responses: {
|
|
393
|
-
...requiresAuth && {
|
|
394
|
-
"401": {
|
|
395
|
-
description: "Authentication required — no valid Bearer token provided",
|
|
396
|
-
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
|
|
397
|
-
},
|
|
398
|
-
"403": {
|
|
399
|
-
description: permAnnotation?.roles ? `Forbidden — requires one of: ${permAnnotation.roles.join(", ")}` : "Forbidden — insufficient permissions",
|
|
400
|
-
content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } }
|
|
401
|
-
}
|
|
402
|
-
},
|
|
403
|
-
"500": { description: "Internal server error" }
|
|
404
|
-
},
|
|
405
|
-
...extras
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
/**
|
|
409
|
-
* Describe a permission check function for OpenAPI.
|
|
410
|
-
* Extracts role, org role, and team permission metadata from permission functions.
|
|
411
|
-
*/
|
|
412
|
-
function describePermissionForOpenApi(check) {
|
|
413
|
-
if (!check || typeof check !== "function") return void 0;
|
|
414
|
-
const fn = check;
|
|
415
|
-
if (fn._isPublic === true) return { type: "public" };
|
|
416
|
-
const result = { type: "requireAuth" };
|
|
417
|
-
if (Array.isArray(fn._roles) && fn._roles.length > 0) {
|
|
418
|
-
result.type = "requireRoles";
|
|
419
|
-
result.roles = fn._roles;
|
|
420
|
-
}
|
|
421
|
-
if (Array.isArray(fn._orgRoles) && fn._orgRoles.length > 0) result.orgRoles = fn._orgRoles;
|
|
422
|
-
return result;
|
|
423
|
-
}
|
|
424
|
-
/**
|
|
425
|
-
* Extract path parameters from route path
|
|
426
|
-
*/
|
|
427
|
-
function extractPathParams(path) {
|
|
428
|
-
const params = [];
|
|
429
|
-
const matches = path.matchAll(/:([^/]+)/g);
|
|
430
|
-
for (const match of matches) {
|
|
431
|
-
const paramName = match[1];
|
|
432
|
-
if (paramName) params.push({
|
|
433
|
-
name: paramName,
|
|
434
|
-
in: "path",
|
|
435
|
-
required: true,
|
|
436
|
-
schema: { type: "string" }
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
return params;
|
|
440
|
-
}
|
|
441
|
-
/**
|
|
442
|
-
* Generate schema definitions from pre-stored registry schemas.
|
|
443
|
-
* Schemas are generated at resource definition time and stored in the registry.
|
|
444
|
-
*
|
|
445
|
-
* Response schema priority:
|
|
446
|
-
* 1. If resource provides explicit `openApiSchemas.response`, use it as-is
|
|
447
|
-
* 2. Otherwise, auto-generate from `createBody` + _id + timestamps
|
|
448
|
-
*
|
|
449
|
-
* Note: This is for OpenAPI documentation only - does NOT affect Fastify serialization.
|
|
450
|
-
*/
|
|
451
|
-
function generateSchemas(resources) {
|
|
452
|
-
const schemas = { Error: {
|
|
453
|
-
type: "object",
|
|
454
|
-
properties: {
|
|
455
|
-
success: {
|
|
456
|
-
type: "boolean",
|
|
457
|
-
example: false
|
|
458
|
-
},
|
|
459
|
-
error: { type: "string" },
|
|
460
|
-
code: { type: "string" },
|
|
461
|
-
requestId: { type: "string" },
|
|
462
|
-
timestamp: { type: "string" }
|
|
463
|
-
}
|
|
464
|
-
} };
|
|
465
|
-
for (const resource of resources) {
|
|
466
|
-
const storedSchemas = resource.openApiSchemas;
|
|
467
|
-
const fieldPerms = resource.fieldPermissions;
|
|
468
|
-
if (storedSchemas?.response) schemas[resource.name] = {
|
|
469
|
-
type: "object",
|
|
470
|
-
description: resource.displayName,
|
|
471
|
-
...storedSchemas.response
|
|
472
|
-
};
|
|
473
|
-
else if (storedSchemas?.createBody) schemas[resource.name] = {
|
|
474
|
-
type: "object",
|
|
475
|
-
description: resource.displayName,
|
|
476
|
-
properties: {
|
|
477
|
-
_id: {
|
|
478
|
-
type: "string",
|
|
479
|
-
description: "Unique identifier"
|
|
480
|
-
},
|
|
481
|
-
...storedSchemas.createBody.properties ?? {},
|
|
482
|
-
createdAt: {
|
|
483
|
-
type: "string",
|
|
484
|
-
format: "date-time",
|
|
485
|
-
description: "Creation timestamp"
|
|
486
|
-
},
|
|
487
|
-
updatedAt: {
|
|
488
|
-
type: "string",
|
|
489
|
-
format: "date-time",
|
|
490
|
-
description: "Last update timestamp"
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
};
|
|
494
|
-
else schemas[resource.name] = {
|
|
495
|
-
type: "object",
|
|
496
|
-
description: resource.displayName,
|
|
497
|
-
properties: {
|
|
498
|
-
_id: {
|
|
499
|
-
type: "string",
|
|
500
|
-
description: "Unique identifier"
|
|
501
|
-
},
|
|
502
|
-
createdAt: {
|
|
503
|
-
type: "string",
|
|
504
|
-
format: "date-time",
|
|
505
|
-
description: "Creation timestamp"
|
|
506
|
-
},
|
|
507
|
-
updatedAt: {
|
|
508
|
-
type: "string",
|
|
509
|
-
format: "date-time",
|
|
510
|
-
description: "Last update timestamp"
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
};
|
|
514
|
-
if (fieldPerms && schemas[resource.name]?.properties) {
|
|
515
|
-
const props = schemas[resource.name].properties;
|
|
516
|
-
for (const [field, perm] of Object.entries(fieldPerms)) if (props[field]) {
|
|
517
|
-
const desc = props[field]?.description ?? "";
|
|
518
|
-
const permDesc = formatFieldPermDescription(perm);
|
|
519
|
-
props[field].description = desc ? `${desc} (${permDesc})` : permDesc;
|
|
520
|
-
} else if (perm.type === "hidden") {}
|
|
521
|
-
}
|
|
522
|
-
if (storedSchemas?.createBody) {
|
|
523
|
-
schemas[`${resource.name}Input`] = {
|
|
524
|
-
type: "object",
|
|
525
|
-
description: `${resource.displayName} create input`,
|
|
526
|
-
...storedSchemas.createBody
|
|
527
|
-
};
|
|
528
|
-
if (storedSchemas.updateBody) schemas[`${resource.name}Update`] = {
|
|
529
|
-
type: "object",
|
|
530
|
-
description: `${resource.displayName} update input`,
|
|
531
|
-
...storedSchemas.updateBody
|
|
532
|
-
};
|
|
533
|
-
} else schemas[`${resource.name}Input`] = {
|
|
534
|
-
type: "object",
|
|
535
|
-
description: `${resource.displayName} input`
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
return schemas;
|
|
539
|
-
}
|
|
540
|
-
/**
|
|
541
|
-
* Format a field permission description for OpenAPI
|
|
542
|
-
*/
|
|
543
|
-
function formatFieldPermDescription(perm) {
|
|
544
|
-
switch (perm.type) {
|
|
545
|
-
case "hidden": return "Hidden — never returned in responses";
|
|
546
|
-
case "visibleTo": return `Visible to: ${(perm.roles ?? []).join(", ")}`;
|
|
547
|
-
case "writableBy": return `Writable by: ${(perm.roles ?? []).join(", ")}`;
|
|
548
|
-
case "redactFor": return `Redacted for: ${(perm.roles ?? []).join(", ")}`;
|
|
549
|
-
default: return perm.type;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
var openapi_default = fp(openApiPlugin, {
|
|
553
|
-
name: "arc-openapi",
|
|
554
|
-
fastify: "5.x"
|
|
555
|
-
});
|
|
556
|
-
//#endregion
|
|
557
|
-
export { openApiPlugin as n, openapi_default as r, buildOpenApiSpec as t };
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|