@classytic/arc 2.11.2 → 2.11.4
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/LICENSE +21 -21
- package/README.md +20 -21
- package/bin/arc.js +2 -2
- package/dist/{BaseController-JNV08qOT.mjs → BaseController-swXruJ2_.mjs} +2 -2
- package/dist/EventTransport-BFQjw9pB.mjs +133 -0
- package/dist/{QueryCache-DOBNHBE0.d.mts → QueryCache-D41bfdBB.d.mts} +1 -1
- package/dist/{actionPermissions-C8YYU92K.mjs → actionPermissions-sUUKDhtP.mjs} +4 -2
- package/dist/adapters/index.d.mts +3 -3
- package/dist/adapters/index.mjs +2 -2
- package/dist/{adapters-D0tT2Tyo.mjs → adapters-DUUiiimH.mjs} +17 -2
- package/dist/audit/index.d.mts +2 -2
- package/dist/auth/index.d.mts +4 -4
- package/dist/auth/index.mjs +1 -1
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/cache/index.d.mts +3 -3
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/cli/commands/generate.d.mts +0 -2
- package/dist/cli/commands/generate.mjs +16 -16
- package/dist/cli/commands/init.mjs +149 -65
- package/dist/context/index.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +3 -3
- package/dist/{core-DXdSSFW-.mjs → core-CbcQRIch.mjs} +25 -8
- package/dist/{createActionRouter-BwaSM0No.mjs → createActionRouter-CIKOcNA7.mjs} +74 -14
- package/dist/{createApp-P1d6rjPy.mjs → createApp-C9bRrqlX.mjs} +4 -6
- package/dist/defineEvent-D1Ky9M1D.mjs +188 -0
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/{eventPlugin--5HIkdPU.mjs → eventPlugin-Cts2-Tfj.mjs} +9 -135
- package/dist/{eventPlugin-CUNjYYRY.d.mts → eventPlugin-DDJoNEPL.d.mts} +34 -7
- package/dist/events/index.d.mts +164 -5
- package/dist/events/index.mjs +138 -182
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +204 -31
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +1 -1
- package/dist/{fields-C8Y0XLAu.d.mts → fields-BRjxOAFp.d.mts} +1 -1
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +3 -3
- package/dist/idempotency/index.mjs +1 -1
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-6u4_Gg6G.d.mts → index-CXXRbnf8.d.mts} +51 -5
- package/dist/{index-DdQ3O9Pg.d.mts → index-D9t1KNaB.d.mts} +2 -2
- package/dist/{index-BbMrcvGp.d.mts → index-Rg8axYPz.d.mts} +12 -4
- package/dist/{index-BdXnTPRj.d.mts → index-m8mOOlFW.d.mts} +3 -3
- package/dist/{index-BYCqHCVu.d.mts → index-rHjXmJar.d.mts} +3 -3
- package/dist/index.d.mts +7 -7
- package/dist/index.mjs +7 -7
- package/dist/integrations/event-gateway.d.mts +2 -2
- package/dist/integrations/index.d.mts +2 -2
- 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/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/middleware/index.d.mts +1 -1
- package/dist/{openapi-C0L9ar7m.mjs → openapi-D7G1V7ex.mjs} +2 -2
- package/dist/org/index.d.mts +2 -2
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +1 -1
- package/dist/{permissions-B4vU9L0Q.mjs → permissions-gd_aUWrR.mjs} +42 -0
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +5 -5
- package/dist/plugins/index.mjs +1 -1
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +4 -4
- package/dist/presets/filesUpload.mjs +1 -1
- 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/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-k604Lj99.mjs → presets-Z7P5w4gF.mjs} +1 -1
- package/dist/{queryCachePlugin-BUXBSm4F.d.mts → queryCachePlugin-CqMdLI2-.d.mts} +2 -2
- package/dist/{redis-Cm1gnRDf.d.mts → redis-DiMkdHEl.d.mts} +1 -1
- package/dist/redis-stream-xTGxB2bm.d.mts +232 -0
- package/dist/registry/index.d.mts +1 -1
- package/dist/{requestContext-CfRkaxwf.mjs → requestContext-C5XeK3VA.mjs} +15 -0
- package/dist/{resourceToTools--okX6QBr.mjs → resourceToTools-CxNmI6xF.mjs} +7 -6
- package/dist/{routerShared-DeESFp4a.mjs → routerShared-BqLRb5l7.mjs} +60 -3
- package/dist/scope/index.d.mts +2 -2
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +1 -1
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-9beEMe25.d.mts → types-BQ9TJQNy.d.mts} +1 -1
- package/dist/{types-BH7dEGvU.d.mts → types-D7KpfiL1.d.mts} +10 -10
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/index.mjs +1 -1
- package/dist/{utils-D3Yxnrwr.mjs → utils-CcYTj09l.mjs} +1 -1
- package/dist/{versioning-M9lNLhO8.d.mts → versioning-DsglKfM_.d.mts} +1 -1
- package/package.json +3 -1
- package/skills/arc/SKILL.md +409 -769
- package/skills/arc/references/events.md +489 -489
- package/dist/redis-stream-CM8TXTix.d.mts +0 -110
- /package/dist/{EventTransport-CfVEGaEl.d.mts → EventTransport-CYNUXdCJ.d.mts} +0 -0
- /package/dist/{elevation-s5ykdNHr.d.mts → elevation-BQQXZ_VR.d.mts} +0 -0
- /package/dist/{errorHandler-Co3lnVmJ.d.mts → errorHandler-DEWmGWPz.d.mts} +0 -0
- /package/dist/{externalPaths-Bapitwvd.d.mts → externalPaths-BD5nw6St.d.mts} +0 -0
- /package/dist/{interface-CkkWm5uR.d.mts → interface-DfLGcus7.d.mts} +0 -0
- /package/dist/{interface-Da0r7Lna.d.mts → interface-beEtJyWM.d.mts} +0 -0
- /package/dist/{pluralize-BneOJkpi.mjs → pluralize-CWP6MB39.mjs} +0 -0
- /package/dist/{schemaIR-BlG9bY7v.mjs → schemaIR-Dy2p4MxS.mjs} +0 -0
- /package/dist/{sessionManager-D-oNWHz3.d.mts → sessionManager-C4Le_UB3.d.mts} +0 -0
- /package/dist/{storage-BwGQXUpd.d.mts → storage-Dfzt4VTl.d.mts} +0 -0
- /package/dist/{store-helpers-BhrzxvyQ.mjs → store-helpers-Cp4uKC1U.mjs} +0 -0
- /package/dist/{tracing-DokiEsuz.d.mts → tracing-QJVprktp.d.mts} +0 -0
- /package/dist/{types-tgR4Pt8F.d.mts → types-DDyTPc6y.d.mts} +0 -0
- /package/dist/{websocket-CyJ1VIFI.d.mts → websocket-ChC2rqe1.d.mts} +0 -0
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Classytic
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Classytic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
# @classytic/arc
|
|
2
2
|
|
|
3
|
-
Database-agnostic resource framework for Fastify. One `defineResource()` call → REST + auth + permissions + events + caching + OpenAPI + MCP tools
|
|
3
|
+
Database-agnostic resource framework for Fastify. One `defineResource()` call → REST + auth + permissions + events + caching + OpenAPI + MCP tools.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Fastify 5+ · Node.js 22+ · ESM only
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install @classytic/arc fastify
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
# Security defaults createApp() loads (each opt-out via `cors: false` etc.)
|
|
11
|
+
npm install @fastify/cors @fastify/helmet @fastify/rate-limit @fastify/under-pressure @fastify/sensible
|
|
12
|
+
|
|
13
|
+
# Storage adapter — pick one
|
|
14
|
+
npm install @classytic/mongokit mongoose # MongoDB
|
|
10
15
|
# OR @classytic/sqlitekit drizzle-orm better-sqlite3 (sqlite)
|
|
11
|
-
# OR
|
|
16
|
+
# OR implement RepositoryLike from @classytic/repo-core
|
|
12
17
|
```
|
|
13
18
|
|
|
14
19
|
---
|
|
@@ -18,7 +23,7 @@ npm install @classytic/mongokit mongoose # MongoDB (most common)
|
|
|
18
23
|
| | |
|
|
19
24
|
|---|---|
|
|
20
25
|
| **One call, full REST** | `defineResource({ name, adapter, presets, permissions })` → `GET /`, `GET /:id`, `POST /`, `PATCH /:id`, `DELETE /:id` + custom routes + actions |
|
|
21
|
-
| **DB-agnostic** | Mongoose,
|
|
26
|
+
| **DB-agnostic** | Mongoose, Drizzle/sqlitekit, or any `RepositoryLike` impl — swap backends without rewriting routes. (Prisma adapter is experimental: implemented, no integration tests yet.) |
|
|
22
27
|
| **Multi-tenant by default** | Tenant-field auto-injected, scope-aware queries, per-org cache keys, elevation events. |
|
|
23
28
|
| **Tree-shakable subpaths** | `@classytic/arc/auth`, `/events`, `/cache`, `/mcp`, `/integrations/jobs` — pay only for what you import. |
|
|
24
29
|
| **MCP tools, free** | Resources auto-generate Model Context Protocol tools for AI agents. Same permissions, same field rules. |
|
|
@@ -33,12 +38,19 @@ import { createApp, loadResources } from '@classytic/arc/factory';
|
|
|
33
38
|
|
|
34
39
|
await mongoose.connect(process.env.DB_URI);
|
|
35
40
|
|
|
41
|
+
// Fail fast on missing CORS env — silent `undefined` here drops to surprising
|
|
42
|
+
// browser defaults. Browser apps: declare an explicit allowlist (below).
|
|
43
|
+
// Server-to-server / API-key services: `cors: { origin: '*', credentials: false }`
|
|
44
|
+
// or `cors: false` to disable entirely (CORS is a browser-only concern).
|
|
45
|
+
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS;
|
|
46
|
+
if (!ALLOWED_ORIGINS) throw new Error('ALLOWED_ORIGINS env is required');
|
|
47
|
+
|
|
36
48
|
const app = await createApp({
|
|
37
49
|
preset: 'production',
|
|
38
50
|
resourcePrefix: '/api/v1',
|
|
39
51
|
resources: await loadResources(import.meta.url), // auto-discover *.resource.ts
|
|
40
52
|
auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
|
|
41
|
-
cors: { origin:
|
|
53
|
+
cors: { origin: ALLOWED_ORIGINS.split(','), credentials: true },
|
|
42
54
|
});
|
|
43
55
|
|
|
44
56
|
await app.listen({ port: 8040, host: '0.0.0.0' });
|
|
@@ -60,7 +72,7 @@ resources: async () => {
|
|
|
60
72
|
},
|
|
61
73
|
```
|
|
62
74
|
|
|
63
|
-
`loadResources({ context })`
|
|
75
|
+
`loadResources({ context })` threads engine handles into resources whose default export is `(ctx) => defineResource(...)`. No parallel factory files, no `exclude: [...]` bookkeeping.
|
|
64
76
|
|
|
65
77
|
---
|
|
66
78
|
|
|
@@ -199,7 +211,7 @@ const ctx = await createTestApp({
|
|
|
199
211
|
authMode: 'jwt',
|
|
200
212
|
connectMongoose: true, // in-memory Mongo + Mongoose connect
|
|
201
213
|
});
|
|
202
|
-
ctx.auth.register('admin', { user: { id: '1',
|
|
214
|
+
ctx.auth.register('admin', { user: { id: '1', role: 'admin' }, orgId: 'org-1' });
|
|
203
215
|
|
|
204
216
|
const res = await ctx.app.inject({
|
|
205
217
|
method: 'POST',
|
|
@@ -229,19 +241,6 @@ arc doctor # diagnose env
|
|
|
229
241
|
|
|
230
242
|
---
|
|
231
243
|
|
|
232
|
-
## Highlights from recent releases
|
|
233
|
-
|
|
234
|
-
| Version | Headline |
|
|
235
|
-
|---|---|
|
|
236
|
-
| **2.11.2** | `RouteSchemaOptions['query']` types `allowedPopulate` + `allowedLookups` |
|
|
237
|
-
| **2.11.1** | `loadResources({ context })` + factory exports; `ActionDefinition.schema` widened to `unknown`; `silent` removed in favor of `arcLog` fallback |
|
|
238
|
-
| **2.11.0** | `BaseController` mixin split, testing surface rewrite (`createTestApp`, `TestAuthProvider`, `expectArc`), action-router parity, async resources factory |
|
|
239
|
-
| **2.10** | Permissions split, `RepositoryLike` plugs into outbox/audit/idempotency, plugin onSend race fix |
|
|
240
|
-
|
|
241
|
-
Full history: [`/changelog/v2.md`](changelog/v2.md).
|
|
242
|
-
|
|
243
|
-
---
|
|
244
|
-
|
|
245
244
|
## Documentation
|
|
246
245
|
|
|
247
246
|
- **Skill** for AI agents: `npx skills add classytic/arc` — wires arc into Claude Code / agentic flows.
|
package/bin/arc.js
CHANGED
|
@@ -334,7 +334,7 @@ GLOBAL OPTIONS
|
|
|
334
334
|
|
|
335
335
|
INIT OPTIONS
|
|
336
336
|
--mongokit Use MongoKit adapter (default, recommended)
|
|
337
|
-
--custom Use custom adapter
|
|
337
|
+
--custom Use custom / Drizzle-ready adapter template
|
|
338
338
|
--better-auth Use Better Auth (default, recommended)
|
|
339
339
|
--jwt Use Arc built-in JWT auth
|
|
340
340
|
--multi-tenant, --multi Multi-tenant mode (adds org scoping)
|
|
@@ -346,7 +346,7 @@ INIT OPTIONS
|
|
|
346
346
|
--skip-install Skip npm install after scaffolding
|
|
347
347
|
|
|
348
348
|
GENERATE SUBCOMMANDS
|
|
349
|
-
resource, r Generate
|
|
349
|
+
resource, r Generate resource-first scaffold (model, repo, resource, test)
|
|
350
350
|
controller, c Generate controller only
|
|
351
351
|
model, m Generate model only
|
|
352
352
|
repository, repo Generate repository only
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { h as SYSTEM_FIELDS, o as DEFAULT_TENANT_FIELD } from "./constants-BhY1OHoH.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
3
|
import { _ as isElevated, n as PUBLIC_SCOPE, o as getOrgId, v as isMember } from "./types-AOD8fxIw.mjs";
|
|
4
|
-
import { M as simpleEqualityMatcher, j as getUserId, k as ArcQueryParser } from "./utils-
|
|
4
|
+
import { M as simpleEqualityMatcher, j as getUserId, k as ArcQueryParser } from "./utils-CcYTj09l.mjs";
|
|
5
5
|
import { t as buildQueryKey } from "./keys-CARyUjiR.mjs";
|
|
6
|
-
import { M as applyFieldWritePermissions, P as resolveEffectiveRoles } from "./permissions-
|
|
6
|
+
import { M as applyFieldWritePermissions, P as resolveEffectiveRoles } from "./permissions-gd_aUWrR.mjs";
|
|
7
7
|
import { t as getUserRoles } from "./types-DV9WDfeg.mjs";
|
|
8
8
|
import { r as ForbiddenError } from "./errors-D5c-5BJL.mjs";
|
|
9
9
|
//#region src/core/AccessControl.ts
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
//#region src/events/EventTransport.ts
|
|
2
|
+
/**
|
|
3
|
+
* In-memory event transport (default)
|
|
4
|
+
* Events are delivered synchronously within the process.
|
|
5
|
+
* Not suitable for multi-instance deployments.
|
|
6
|
+
*/
|
|
7
|
+
var MemoryEventTransport = class {
|
|
8
|
+
name = "memory";
|
|
9
|
+
handlers = /* @__PURE__ */ new Map();
|
|
10
|
+
logger;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.logger = options?.logger ?? console;
|
|
13
|
+
}
|
|
14
|
+
async publish(event) {
|
|
15
|
+
const exactHandlers = this.handlers.get(event.type) ?? /* @__PURE__ */ new Set();
|
|
16
|
+
const wildcardHandlers = this.handlers.get("*") ?? /* @__PURE__ */ new Set();
|
|
17
|
+
const patternHandlers = /* @__PURE__ */ new Set();
|
|
18
|
+
for (const [pattern, handlers] of this.handlers.entries()) if (pattern.endsWith(".*")) {
|
|
19
|
+
const prefix = pattern.slice(0, -2);
|
|
20
|
+
if (event.type.startsWith(`${prefix}.`)) for (const h of handlers) patternHandlers.add(h);
|
|
21
|
+
}
|
|
22
|
+
const allHandlers = new Set([
|
|
23
|
+
...exactHandlers,
|
|
24
|
+
...wildcardHandlers,
|
|
25
|
+
...patternHandlers
|
|
26
|
+
]);
|
|
27
|
+
for (const handler of allHandlers) try {
|
|
28
|
+
await handler(event);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
this.logger.error(`[EventTransport] Handler error for ${event.type}:`, err);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Reference `publishMany` implementation — delegates to `publish()` in order.
|
|
35
|
+
*
|
|
36
|
+
* Production transports (Kafka, Redis pipeline, SQS batch) should override
|
|
37
|
+
* this with a single batched network call. Memory transport has nothing to
|
|
38
|
+
* batch, so we just loop — the loop still returns a proper result map so
|
|
39
|
+
* `EventOutbox.relay` can exercise the batched code path in tests.
|
|
40
|
+
*/
|
|
41
|
+
async publishMany(events) {
|
|
42
|
+
const results = /* @__PURE__ */ new Map();
|
|
43
|
+
for (const event of events) try {
|
|
44
|
+
await this.publish(event);
|
|
45
|
+
results.set(event.meta.id, null);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
results.set(event.meta.id, err instanceof Error ? err : new Error(String(err)));
|
|
48
|
+
}
|
|
49
|
+
return results;
|
|
50
|
+
}
|
|
51
|
+
async subscribe(pattern, handler) {
|
|
52
|
+
if (!this.handlers.has(pattern)) this.handlers.set(pattern, /* @__PURE__ */ new Set());
|
|
53
|
+
this.handlers.get(pattern)?.add(handler);
|
|
54
|
+
return () => {
|
|
55
|
+
const set = this.handlers.get(pattern);
|
|
56
|
+
if (set) {
|
|
57
|
+
set.delete(handler);
|
|
58
|
+
if (set.size === 0) this.handlers.delete(pattern);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async close() {
|
|
63
|
+
this.handlers.clear();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Create a domain event with auto-generated metadata.
|
|
68
|
+
*
|
|
69
|
+
* `id` and `timestamp` are filled in; everything else is caller-controlled.
|
|
70
|
+
* Set `schemaVersion` explicitly for any event type you plan to evolve.
|
|
71
|
+
*/
|
|
72
|
+
function createEvent(type, payload, meta) {
|
|
73
|
+
return {
|
|
74
|
+
type,
|
|
75
|
+
payload,
|
|
76
|
+
meta: {
|
|
77
|
+
id: crypto.randomUUID(),
|
|
78
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
79
|
+
...meta
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Create a child event that chains causation from a parent event.
|
|
85
|
+
*
|
|
86
|
+
* Rules:
|
|
87
|
+
* - `causationId` is set to the parent's `id` (direct cause)
|
|
88
|
+
* - `correlationId` is inherited from the parent if set, else falls back
|
|
89
|
+
* to the parent's `id` (root correlation)
|
|
90
|
+
* - `userId` / `organizationId` are inherited when not overridden so the
|
|
91
|
+
* whole chain stays scoped to the originating principal/tenant
|
|
92
|
+
*
|
|
93
|
+
* Caller-supplied `meta` wins over inherited fields — pass `{ userId: newActor }`
|
|
94
|
+
* to override when a subsystem acts on behalf of a different principal.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* const orderPlaced = createEvent('order.placed', { orderId: 'o1' }, {
|
|
99
|
+
* correlationId: req.id, userId: user.id,
|
|
100
|
+
* });
|
|
101
|
+
* await events.publish(orderPlaced);
|
|
102
|
+
*
|
|
103
|
+
* // Downstream handler emits a child event:
|
|
104
|
+
* const reserved = createChildEvent(orderPlaced, 'inventory.reserved', {
|
|
105
|
+
* orderId: 'o1', skus: ['sku-1', 'sku-2'],
|
|
106
|
+
* });
|
|
107
|
+
* // reserved.meta.causationId === orderPlaced.meta.id
|
|
108
|
+
* // reserved.meta.correlationId === orderPlaced.meta.correlationId
|
|
109
|
+
* // reserved.meta.userId === user.id (inherited)
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
function createChildEvent(parent, type, payload, meta) {
|
|
113
|
+
const inherited = {
|
|
114
|
+
correlationId: parent.meta.correlationId ?? parent.meta.id,
|
|
115
|
+
causationId: parent.meta.id
|
|
116
|
+
};
|
|
117
|
+
if (parent.meta.userId !== void 0) inherited.userId = parent.meta.userId;
|
|
118
|
+
if (parent.meta.organizationId !== void 0) inherited.organizationId = parent.meta.organizationId;
|
|
119
|
+
if (parent.meta.source !== void 0) inherited.source = parent.meta.source;
|
|
120
|
+
if (parent.meta.idempotencyKey !== void 0) inherited.idempotencyKey = parent.meta.idempotencyKey;
|
|
121
|
+
return {
|
|
122
|
+
type,
|
|
123
|
+
payload,
|
|
124
|
+
meta: {
|
|
125
|
+
id: crypto.randomUUID(),
|
|
126
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
127
|
+
...inherited,
|
|
128
|
+
...meta
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
export { createChildEvent as n, createEvent as r, MemoryEventTransport as t };
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Callers decide what "no gate" means:
|
|
7
7
|
* - HTTP: boot-time throw in `normalizeActionsToRouterConfig`.
|
|
8
|
-
* - MCP:
|
|
9
|
-
*
|
|
8
|
+
* - MCP: tool-generation throw in `resourceToTools` (mirrors HTTP — the
|
|
9
|
+
* two surfaces fail closed identically so MCP can't expose an
|
|
10
|
+
* unauthenticated mutating tool when the HTTP plugin lifecycle hasn't
|
|
11
|
+
* run).
|
|
10
12
|
* - OpenAPI: docs advertise the endpoint as unauthenticated.
|
|
11
13
|
*/
|
|
12
14
|
function resolveActionPermission(input) {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { An as
|
|
2
|
-
import { a as PrismaQueryParserOptions, c as MongooseAdapterOptions, d as DrizzleAdapterOptions, f as createDrizzleAdapter, i as PrismaQueryParser, l as createMongooseAdapter, n as PrismaAdapterOptions, o as createPrismaAdapter, r as PrismaQueryOptions, s as MongooseAdapter, t as PrismaAdapter, u as DrizzleAdapter } from "../index-
|
|
3
|
-
export { AdapterFactory, DataAdapter, DrizzleAdapter, DrizzleAdapterOptions, FieldMetadata, MongooseAdapter, MongooseAdapterOptions, PrismaAdapter, PrismaAdapterOptions, PrismaQueryOptions, PrismaQueryParser, PrismaQueryParserOptions, RelationMetadata, RepositoryLike, SchemaMetadata, ValidationResult, createDrizzleAdapter, createMongooseAdapter, createPrismaAdapter };
|
|
1
|
+
import { An as FieldMetadata, Dn as AdapterRepositoryInput, En as AdapterFactory, Fn as asRepositoryLike, Mn as RepositoryLike, Nn as SchemaMetadata, Pn as ValidationResult, jn as RelationMetadata, kn as DataAdapter } from "../index-CXXRbnf8.mjs";
|
|
2
|
+
import { a as PrismaQueryParserOptions, c as MongooseAdapterOptions, d as DrizzleAdapterOptions, f as createDrizzleAdapter, i as PrismaQueryParser, l as createMongooseAdapter, n as PrismaAdapterOptions, o as createPrismaAdapter, r as PrismaQueryOptions, s as MongooseAdapter, t as PrismaAdapter, u as DrizzleAdapter } from "../index-Rg8axYPz.mjs";
|
|
3
|
+
export { AdapterFactory, AdapterRepositoryInput, DataAdapter, DrizzleAdapter, DrizzleAdapterOptions, FieldMetadata, MongooseAdapter, MongooseAdapterOptions, PrismaAdapter, PrismaAdapterOptions, PrismaQueryOptions, PrismaQueryParser, PrismaQueryParserOptions, RelationMetadata, RepositoryLike, SchemaMetadata, ValidationResult, asRepositoryLike, createDrizzleAdapter, createMongooseAdapter, createPrismaAdapter };
|
package/dist/adapters/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as createMongooseAdapter, i as MongooseAdapter, n as PrismaQueryParser, o as
|
|
2
|
-
export { DrizzleAdapter, MongooseAdapter, PrismaAdapter, PrismaQueryParser, createDrizzleAdapter, createMongooseAdapter, createPrismaAdapter };
|
|
1
|
+
import { a as createMongooseAdapter, c as createDrizzleAdapter, i as MongooseAdapter, n as PrismaQueryParser, o as asRepositoryLike, r as createPrismaAdapter, s as DrizzleAdapter, t as PrismaAdapter } from "../adapters-DUUiiimH.mjs";
|
|
2
|
+
export { DrizzleAdapter, MongooseAdapter, PrismaAdapter, PrismaQueryParser, asRepositoryLike, createDrizzleAdapter, createMongooseAdapter, createPrismaAdapter };
|
|
@@ -264,6 +264,21 @@ function createDrizzleAdapter(options) {
|
|
|
264
264
|
return new DrizzleAdapter(options);
|
|
265
265
|
}
|
|
266
266
|
//#endregion
|
|
267
|
+
//#region src/adapters/interface.ts
|
|
268
|
+
/**
|
|
269
|
+
* Widen a permissive `AdapterRepositoryInput<TDoc>` to arc's strict
|
|
270
|
+
* `RepositoryLike<TDoc>` view. Single-source escape hatch for the
|
|
271
|
+
* filter-IR drift documented on `AdapterRepositoryInput`.
|
|
272
|
+
*
|
|
273
|
+
* Arc internals (audit / outbox / idempotency, BaseController) still see
|
|
274
|
+
* the IR-aware `RepositoryLike`; only the call paths arc exercises are
|
|
275
|
+
* shared between the two views, and those use the narrower
|
|
276
|
+
* `Record<string, unknown>` filter shape both sides agree on.
|
|
277
|
+
*/
|
|
278
|
+
function asRepositoryLike(input) {
|
|
279
|
+
return input;
|
|
280
|
+
}
|
|
281
|
+
//#endregion
|
|
267
282
|
//#region src/adapters/mongoose.ts
|
|
268
283
|
/**
|
|
269
284
|
* Mongoose data adapter with proper type safety
|
|
@@ -280,7 +295,7 @@ var MongooseAdapter = class {
|
|
|
280
295
|
if (!isMongooseModel(options.model)) throw new TypeError("MongooseAdapter: Invalid model. Expected Mongoose Model instance.\nUsage: createMongooseAdapter({ model: YourModel, repository: yourRepo })");
|
|
281
296
|
if (!isRepository(options.repository)) throw new TypeError("MongooseAdapter: Invalid repository. Expected StandardRepo instance.\nUsage: createMongooseAdapter({ model: YourModel, repository: yourRepo })");
|
|
282
297
|
this.model = options.model;
|
|
283
|
-
this.repository = options.repository;
|
|
298
|
+
this.repository = asRepositoryLike(options.repository);
|
|
284
299
|
this.schemaGenerator = options.schemaGenerator;
|
|
285
300
|
this.name = `MongooseAdapter<${options.model.modelName}>`;
|
|
286
301
|
}
|
|
@@ -946,4 +961,4 @@ function createPrismaAdapter(options) {
|
|
|
946
961
|
return new PrismaAdapter(options);
|
|
947
962
|
}
|
|
948
963
|
//#endregion
|
|
949
|
-
export { createMongooseAdapter as a, MongooseAdapter as i, PrismaQueryParser as n,
|
|
964
|
+
export { createMongooseAdapter as a, createDrizzleAdapter as c, MongooseAdapter as i, PrismaQueryParser as n, asRepositoryLike as o, createPrismaAdapter as r, DrizzleAdapter as s, PrismaAdapter as t };
|
package/dist/audit/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { d as UserBase } from "../fields-
|
|
1
|
+
import { Mn as RepositoryLike } from "../index-CXXRbnf8.mjs";
|
|
2
|
+
import { d as UserBase } from "../fields-BRjxOAFp.mjs";
|
|
3
3
|
import { FastifyPluginAsync } from "fastify";
|
|
4
4
|
|
|
5
5
|
//#region src/audit/stores/interface.d.ts
|
package/dist/auth/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Ot as AuthHelpers, kt as AuthPluginOptions } from "../index-
|
|
2
|
-
import { c as PermissionCheck } from "../fields-
|
|
3
|
-
import { t as ExternalOpenApiPaths } from "../externalPaths-
|
|
4
|
-
import { a as SessionManagerOptions, c as createSessionManager, i as SessionData, n as MemorySessionStoreOptions, o as SessionManagerResult, r as SessionCookieOptions, s as SessionStore, t as MemorySessionStore } from "../sessionManager-
|
|
1
|
+
import { Ot as AuthHelpers, kt as AuthPluginOptions } from "../index-CXXRbnf8.mjs";
|
|
2
|
+
import { c as PermissionCheck } from "../fields-BRjxOAFp.mjs";
|
|
3
|
+
import { t as ExternalOpenApiPaths } from "../externalPaths-BD5nw6St.mjs";
|
|
4
|
+
import { a as SessionManagerOptions, c as createSessionManager, i as SessionData, n as MemorySessionStoreOptions, o as SessionManagerResult, r as SessionCookieOptions, s as SessionStore, t as MemorySessionStore } from "../sessionManager-C4Le_UB3.mjs";
|
|
5
5
|
import { FastifyPluginAsync, FastifyReply as FastifyReply$1, FastifyRequest } from "fastify";
|
|
6
6
|
|
|
7
7
|
//#region src/auth/authPlugin.d.ts
|
package/dist/auth/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as requireTeamMembership, m as requireOrgRole, p as requireOrgMembership } from "../permissions-
|
|
1
|
+
import { _ as requireTeamMembership, m as requireOrgRole, p as requireOrgMembership } from "../permissions-gd_aUWrR.mjs";
|
|
2
2
|
import { n as normalizeRoles, t as getUserRoles } from "../types-DV9WDfeg.mjs";
|
|
3
3
|
import { t as ArcError } from "../errors-D5c-5BJL.mjs";
|
|
4
4
|
import { n as extractBetterAuthOpenApi } from "../betterAuthOpenApi-DwxtK3uG.mjs";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as SessionData, s as SessionStore } from "../sessionManager-
|
|
1
|
+
import { i as SessionData, s as SessionStore } from "../sessionManager-C4Le_UB3.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/auth/redis-session.d.ts
|
|
4
4
|
/** Minimal Redis client interface — compatible with ioredis */
|
package/dist/cache/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as CacheStats, r as CacheStore, t as CacheLogger } from "../interface-
|
|
2
|
-
import { a as QueryCacheConfig, i as QueryCache, n as CacheResult, r as CacheStatus, t as CacheEnvelope } from "../QueryCache-
|
|
3
|
-
import { i as queryCachePlugin, n as QueryCacheDefaults, r as QueryCachePluginOptions, t as CrossResourceRule } from "../queryCachePlugin-
|
|
1
|
+
import { n as CacheStats, r as CacheStore, t as CacheLogger } from "../interface-beEtJyWM.mjs";
|
|
2
|
+
import { a as QueryCacheConfig, i as QueryCache, n as CacheResult, r as CacheStatus, t as CacheEnvelope } from "../QueryCache-D41bfdBB.mjs";
|
|
3
|
+
import { i as queryCachePlugin, n as QueryCacheDefaults, r as QueryCachePluginOptions, t as CrossResourceRule } from "../queryCachePlugin-CqMdLI2-.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/cache/keys.d.ts
|
|
6
6
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as ResourceRegistry } from "../../ResourceRegistry-DkAeAuTX.mjs";
|
|
2
|
-
import { t as buildOpenApiSpec } from "../../openapi-
|
|
2
|
+
import { t as buildOpenApiSpec } from "../../openapi-D7G1V7ex.mjs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
@@ -6,8 +6,6 @@
|
|
|
6
6
|
* - src/resources/product/product.model.ts
|
|
7
7
|
* - src/resources/product/product.repository.ts
|
|
8
8
|
* - src/resources/product/product.resource.ts
|
|
9
|
-
* - src/resources/product/product.controller.ts
|
|
10
|
-
* - src/resources/product/product.schemas.ts
|
|
11
9
|
*
|
|
12
10
|
* Handles kebab-case names: `arc g r org-profile` generates:
|
|
13
11
|
* - Class names: OrgProfile, OrgProfileRepository
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as pluralize } from "../../pluralize-
|
|
1
|
+
import { t as pluralize } from "../../pluralize-CWP6MB39.mjs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
//#region src/cli/commands/generate.ts
|
|
@@ -9,8 +9,6 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
9
9
|
* - src/resources/product/product.model.ts
|
|
10
10
|
* - src/resources/product/product.repository.ts
|
|
11
11
|
* - src/resources/product/product.resource.ts
|
|
12
|
-
* - src/resources/product/product.controller.ts
|
|
13
|
-
* - src/resources/product/product.schemas.ts
|
|
14
12
|
*
|
|
15
13
|
* Handles kebab-case names: `arc g r org-profile` generates:
|
|
16
14
|
* - Class names: OrgProfile, OrgProfileRepository
|
|
@@ -58,7 +56,7 @@ ${ts ? `
|
|
|
58
56
|
export interface I${name} {
|
|
59
57
|
name: string;
|
|
60
58
|
description?: string;
|
|
61
|
-
isActive: boolean;
|
|
59
|
+
${isMultiTenant ? "organizationId: string;\n " : ""}isActive: boolean;
|
|
62
60
|
}
|
|
63
61
|
|
|
64
62
|
export type ${name}Document = HydratedDocument<I${name}>;
|
|
@@ -67,14 +65,14 @@ const ${camel}Schema = new Schema${ts ? `<I${name}>` : ""}(
|
|
|
67
65
|
{
|
|
68
66
|
name: { type: String, required: true, trim: true },
|
|
69
67
|
description: { type: String, trim: true },
|
|
70
|
-
isActive: { type: Boolean, default: true },
|
|
68
|
+
${isMultiTenant ? "organizationId: { type: String, required: true, index: true },\n " : ""}isActive: { type: Boolean, default: true },
|
|
71
69
|
},
|
|
72
70
|
{ timestamps: true }
|
|
73
71
|
);
|
|
74
72
|
|
|
75
73
|
// Indexes
|
|
76
74
|
${camel}Schema.index({ name: 1 });
|
|
77
|
-
${camel}Schema.index({ isActive: 1 });
|
|
75
|
+
${isMultiTenant ? `${camel}Schema.index({ organizationId: 1, isActive: 1 });\n` : ""}${camel}Schema.index({ isActive: 1 });
|
|
78
76
|
|
|
79
77
|
const ${name} = mongoose.models.${name} || mongoose.model('${name}', ${camel}Schema);
|
|
80
78
|
export default ${name};
|
|
@@ -193,20 +191,21 @@ export default crudSchemas;
|
|
|
193
191
|
*/
|
|
194
192
|
|
|
195
193
|
import { defineResource, createMongooseAdapter } from '@classytic/arc';
|
|
196
|
-
import {
|
|
194
|
+
import { allOf, requireOrgMembership, requireRoles } from '@classytic/arc/permissions';
|
|
195
|
+
import { multiTenantPreset } from '@classytic/arc/presets';
|
|
197
196
|
import ${name}${ts ? `, { type I${name} }` : ""} from './${fileName}.model.js';
|
|
198
197
|
import ${camel}Repository from './${fileName}.repository.js';${queryParserImport}
|
|
199
198
|
|
|
200
199
|
const ${camel}Resource = defineResource${ts ? `<I${name}>` : ""}({
|
|
201
200
|
name: '${fileName}',
|
|
202
201
|
adapter: createMongooseAdapter({ model: ${name}, repository: ${camel}Repository }),${queryParserConfig}
|
|
203
|
-
presets: ['softDelete'],
|
|
202
|
+
presets: ['softDelete', multiTenantPreset({ tenantField: 'organizationId' })],
|
|
204
203
|
permissions: {
|
|
205
|
-
list:
|
|
206
|
-
get:
|
|
207
|
-
create: requireRoles(['admin']),
|
|
208
|
-
update: requireRoles(['admin']),
|
|
209
|
-
delete: requireRoles(['admin']),
|
|
204
|
+
list: requireOrgMembership(),
|
|
205
|
+
get: requireOrgMembership(),
|
|
206
|
+
create: allOf(requireOrgMembership(), requireRoles(['admin'])),
|
|
207
|
+
update: allOf(requireOrgMembership(), requireRoles(['admin'])),
|
|
208
|
+
delete: allOf(requireOrgMembership(), requireRoles(['admin'])),
|
|
210
209
|
},
|
|
211
210
|
});
|
|
212
211
|
|
|
@@ -367,7 +366,7 @@ async function generate(type, args) {
|
|
|
367
366
|
}
|
|
368
367
|
}
|
|
369
368
|
/**
|
|
370
|
-
* Generate a
|
|
369
|
+
* Generate a resource-first scaffold
|
|
371
370
|
*/
|
|
372
371
|
async function generateResource(name, lowerName, resourcePath, templates, ext, includeMcp = false) {
|
|
373
372
|
console.log(`\nGenerating resource: ${name}...\n`);
|
|
@@ -417,8 +416,9 @@ Next steps:
|
|
|
417
416
|
src/resources/${lowerName}/${lowerName}.model.${ext}
|
|
418
417
|
|
|
419
418
|
3. Adjust permissions in ${lowerName}.resource.${ext}:
|
|
420
|
-
${isMultiTenant ? ` -
|
|
421
|
-
-
|
|
419
|
+
${isMultiTenant ? ` - requireOrgMembership() → member of the current organization
|
|
420
|
+
- multiTenantPreset() → injects and filters organizationId
|
|
421
|
+
- requireRoles(['admin']) → admin writes inside the org scope` : ` - requireAuth() → any authenticated user
|
|
422
422
|
- requireRoles(['admin']) → specific platform roles`}
|
|
423
423
|
|
|
424
424
|
4. Run tests:
|