@classytic/arc 2.32.0 → 2.33.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 +1 -1
- package/bin/arc.js +58 -17
- package/dist/{BaseController-Dk34dxhI.mjs → BaseController-CaQC5EXX.mjs} +293 -33
- package/dist/{EventTransport-2uVV34WU.d.mts → EventTransport-BEVP50ka.d.mts} +16 -0
- package/dist/{EventTransport-BV10D0oG.mjs → EventTransport-tq4CEB-q.mjs} +39 -2
- package/dist/{HookSystem-vimw5D7Z.mjs → HookSystem-DqXkMXkB.mjs} +2 -2
- package/dist/{ResourceRegistry-BVr7PgA0.mjs → ResourceRegistry-DoALUlOO.mjs} +3 -2
- package/dist/{actionPermissions-Dti7ebyv.mjs → actionPermissions-CUeCmpUd.mjs} +1 -1
- package/dist/audit/index.d.mts +2 -2
- package/dist/audit/index.mjs +2 -2
- package/dist/auth/audit.mjs +1 -1
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +39 -7
- package/dist/auth/redis-session.mjs +1 -1
- package/dist/{authorizationDecision-__T3EJDA.mjs → authorizationDecision-B3mP_QTy.mjs} +2 -2
- package/dist/{types-B7XeLEgK.d.mts → base-Csjc2oF-.d.mts} +74 -74
- package/dist/{buildHandler-Dn7rzUJo.mjs → buildHandler-DLE9xX-V.mjs} +4 -4
- package/dist/cache/index.mjs +4 -4
- package/dist/{caching-DFJqHyFs.mjs → caching-BcGmvnHh.mjs} +2 -2
- package/dist/cleanup/index.d.mts +17 -5
- package/dist/cleanup/index.mjs +240 -7
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/describe.mjs +3 -3
- package/dist/cli/commands/docs.mjs +3 -3
- package/dist/cli/commands/doctor.mjs +1 -1
- package/dist/cli/commands/generate.mjs +2 -2
- package/dist/cli/commands/init.mjs +2 -2
- package/dist/cli/commands/introspect.mjs +2 -2
- package/dist/cli/index.mjs +3 -3
- package/dist/{constants-J99LGTJI.mjs → constants-BxCoxmiN.mjs} +1 -1
- package/dist/context/index.mjs +3 -3
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +7 -7
- package/dist/{core-BwwmNSp7.mjs → core-dxhcHhUD.mjs} +3 -3
- package/dist/{createActionRouter-DkJZWo0m.mjs → createActionRouter-DVpoA0do.mjs} +8 -8
- package/dist/{createAggregationRouter-B1exOVD8.mjs → createAggregationRouter-B_B48JZ-.mjs} +7 -7
- package/dist/{createApp-Bv0m-Q6_.mjs → createApp-CLGegqiG.mjs} +128 -24
- package/dist/{defineAggregation-gX4No1gt.mjs → defineAggregation-Nv-DwfOf.mjs} +1 -1
- package/dist/{defineResource-XFPPUu2c.mjs → defineResource-DR9vsUYy.mjs} +297 -23
- package/dist/discovery/index.mjs +1 -1
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +3 -3
- package/dist/{elevation-DNXXxgjl.mjs → elevation-B0tlY8y1.mjs} +4 -4
- package/dist/encryption/index.mjs +3 -3
- package/dist/{errorHandler-B2AZUJUj.mjs → errorHandler-C7xBqHsI.mjs} +42 -5
- package/dist/{errorHandler-CpYUJmPA.d.mts → errorHandler-DI5AN2b5.d.mts} +14 -3
- package/dist/{errors-Bh1KK5ps.mjs → errors-Cd_yy7aW.mjs} +1 -1
- package/dist/{eventPlugin-GF5nnBIq.d.mts → eventPlugin-DSNEZWfV.d.mts} +29 -1
- package/dist/{eventPlugin-Dk_TyXeE.mjs → eventPlugin-Dar8qRaj.mjs} +65 -9
- package/dist/events/index.d.mts +5 -351
- package/dist/events/index.mjs +5 -780
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +1 -1
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/events/transports/redis.mjs +1 -1
- package/dist/factory/index.d.mts +20 -5
- package/dist/factory/index.mjs +7 -7
- package/dist/{fastify-augmentation-BppG5bMT.d.mts → fastify-augmentation-DVsTAy6X.d.mts} +3 -3
- package/dist/{fields-DEi65pGj.mjs → fields-CojeUkS4.mjs} +1 -1
- package/dist/{generate-CRSG0jTv.mjs → generate-DiB0nHPR.mjs} +2 -2
- package/dist/{headers-BXjQYbeC.mjs → headers-CveNO9gR.mjs} +2 -2
- package/dist/{health-5XnTa2uo.mjs → health-CasG2A28.mjs} +1 -1
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +2 -2
- package/dist/idempotency/index.d.mts +45 -1
- package/dist/idempotency/index.mjs +91 -9
- package/dist/idempotency/redis.mjs +1 -1
- package/dist/{index-zq6VrjaB.d.mts → index-B-DvUzsr.d.mts} +12 -2
- package/dist/{index-CpzxqePp.d.mts → index-BcR2bFZQ.d.mts} +707 -373
- package/dist/{index-DXlHwvuE.d.mts → index-Brin0ODU.d.mts} +4 -4
- package/dist/{index-ZWhLTp_s.d.mts → index-C5NnMEKF.d.mts} +1 -1
- package/dist/{index-C-481Zf2.d.mts → index-ugr1tQlW.d.mts} +17 -1
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +11 -11
- package/dist/{init-BbfP8aky.mjs → init-mdJ-v4Xp.mjs} +18 -12
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +2 -2
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/index.mjs +1 -1
- package/dist/integrations/jobs/index.d.mts +1 -1
- package/dist/integrations/jobs/index.mjs +99 -97
- package/dist/integrations/mcp/index.d.mts +12 -3
- package/dist/integrations/mcp/index.mjs +2 -2
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +2 -2
- package/dist/integrations/streamline.mjs +3 -3
- package/dist/integrations/webhooks.d.mts +124 -1
- package/dist/integrations/webhooks.mjs +180 -2
- package/dist/integrations/websocket-pushref-redis.mjs +1 -1
- package/dist/integrations/websocket-redis.mjs +1 -1
- package/dist/integrations/websocket.mjs +1 -1
- package/dist/{interface-BP4IJCBO.mjs → interface-DznJrm9R.mjs} +1 -1
- package/dist/{jsonBody-DDSR2HFl.mjs → jsonBody-CfGH-GxO.mjs} +2 -2
- package/dist/{keys-B-HBTGXl.mjs → keys-WeKvY1dE.mjs} +1 -1
- package/dist/{loadResources-DisEAmkm.mjs → loadResources-Bj-aSsXi.mjs} +2 -2
- package/dist/{loadResourcesFromEntry-D1MJp13f.mjs → loadResourcesFromEntry-DwaxNcE2.mjs} +2 -2
- package/dist/logger/index.mjs +1 -1
- package/dist/{mcpPlugin-CpfVpKHE.mjs → mcpPlugin-aJg5TqHi.mjs} +60 -21
- package/dist/{memory-DsuWnj4T.mjs → memory-qYSB7rMQ.mjs} +2 -2
- package/dist/{metrics-DdQL8syd.mjs → metrics-D7uchLjN.mjs} +2 -2
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +3 -3
- package/dist/migrations/index.mjs +2 -2
- package/dist/{module-Br81am6m.mjs → module-CwKsgZXp.mjs} +1 -1
- package/dist/{multipartBody-BnkBLN7v.mjs → multipartBody-4w-S3-PU.mjs} +1 -1
- package/dist/{openapi-DIRzvxC0.mjs → openapi-BN5cQaO2.mjs} +6 -6
- package/dist/outbox-admin/index.d.mts +2 -2
- package/dist/outbox-admin/index.mjs +4 -4
- package/dist/outbox-module-CXB79bvt.mjs +839 -0
- package/dist/outbox-module-D-VFq37T.d.mts +368 -0
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +5 -5
- package/dist/{permissions-Dl_m0NCe.mjs → permissions-BAfwgBxc.mjs} +25 -6
- package/dist/{pipe-ELHCP3zj.mjs → pipe-BzzoEhHj.mjs} +2 -2
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +2 -2
- package/dist/{pipeline-CPA-jx0M.mjs → pipeline-q69DZzgG.mjs} +6 -6
- package/dist/plugins/index.d.mts +6 -6
- package/dist/plugins/index.mjs +16 -16
- package/dist/plugins/response-cache.mjs +2 -2
- package/dist/plugins/tracing-entry.mjs +2 -2
- package/dist/{pluralize-C2ZkKbsn.mjs → pluralize-B7xf8qZF.mjs} +1 -1
- package/dist/{preloadResources-CDs87fTw.mjs → preloadResources-BkyqeTDz.mjs} +1 -1
- package/dist/{preloadResources-T1hmlTEZ.d.mts → preloadResources-g07ldtzh.d.mts} +30 -7
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/filesUpload.mjs +5 -5
- package/dist/presets/index.d.mts +2 -2
- package/dist/presets/index.mjs +2 -2
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +3 -3
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +2 -2
- package/dist/{presets-Dntd_pUO.mjs → presets-D-h0uWtP.mjs} +4 -4
- package/dist/projection-BKM0Ui-s.mjs +42 -0
- package/dist/{queryCachePlugin-Dhps-Ldt.mjs → queryCachePlugin-DhsZ0ZIc.mjs} +49 -7
- package/dist/{rateLimit-C7mmAGs3.mjs → rateLimit-D4-ASwsI.mjs} +2 -2
- package/dist/{rateLimitKey-CccEFBfk.mjs → rateLimitKey-XngqVNEV.mjs} +1 -1
- package/dist/{realtime-DVvVZZFz.mjs → realtime-CTkv3O59.mjs} +9 -9
- package/dist/{redis-stream-qN6A-RFv.d.mts → redis-stream-mwLWlfHG.d.mts} +1 -1
- package/dist/{registerRoute-B_QrjYZS.mjs → registerRoute-DNnd3vkz.mjs} +4 -4
- package/dist/registry/index.d.mts +3 -3
- package/dist/registry/index.mjs +3 -3
- package/dist/{registry-DqUmzA9W.mjs → registry-CLxCR5LF.mjs} +2 -2
- package/dist/{renewingLease-BpP9wwj7.mjs → renewingLease-BnWtbA2U.mjs} +1 -1
- package/dist/{replyHelpers-DS-P0b32.mjs → replyHelpers-BSBVCwL3.mjs} +2 -2
- package/dist/{repositoryFilter-DotlxG3L.mjs → repositoryFilter-DKny7PDn.mjs} +1 -1
- package/dist/{requestContext-B6vQF-sy.mjs → requestContext-uYE35JxI.mjs} +1 -1
- package/dist/{rolldown-runtime-BhflRrBs.mjs → rolldown-runtime-DNg-KNCk.mjs} +1 -1
- package/dist/runtimeCapabilities-Dm88DaTW.mjs +49 -0
- package/dist/{schedules-C2Q3uv2d.mjs → schedules-CAcZgia0.mjs} +3 -3
- package/dist/{schemaConverter-Crjvczi_.mjs → schemaConverter-XH1RZRyu.mjs} +1 -1
- package/dist/{schemaIR-CnyKK_1j.mjs → schemaIR-CDvclAew.mjs} +2 -2
- package/dist/schemas/index.mjs +1 -1
- package/dist/scim/index.mjs +2 -2
- package/dist/scope/index.mjs +5 -5
- package/dist/{scope-afz_Ieek.mjs → scope-BaWcqBvr.mjs} +4 -4
- package/dist/{simpleEqualityMatcher-C4KiMWUa.mjs → simpleEqualityMatcher-Se8VrsaP.mjs} +1 -1
- package/dist/{sse-B9P4Y-UI.mjs → sse-Cf0jUHsh.mjs} +5 -5
- package/dist/{sseStream-B3zb_d1h.mjs → sseStream-Bvpi-UXQ.mjs} +2 -2
- package/dist/{store-helpers-DmEMHlPQ.mjs → store-helpers-B88p99Z2.mjs} +1 -1
- package/dist/{streaming-DPZdQmDz.mjs → streaming-DsT1B5zJ.mjs} +1 -1
- package/dist/{subscribe-helpers-sp5xTDVA.mjs → subscribe-helpers-CLIOHsOK.mjs} +2 -2
- package/dist/sync/changelog.mjs +1 -1
- package/dist/testing/cleanupStoreContract.d.mts +1 -1
- package/dist/testing/cleanupStoreContract.mjs +3 -1
- package/dist/testing/index.d.mts +24 -12
- package/dist/testing/index.mjs +24 -12
- package/dist/testing/outboxStoreContract.mjs +1 -1
- package/dist/testing/storageContract.mjs +1 -1
- package/dist/{transactionContext-BvLKvOIL.mjs → transactionContext-BYtWnwAS.mjs} +1 -1
- package/dist/{typeGuards-B_5-hKjJ.mjs → typeGuards-jjKJDN13.mjs} +1 -1
- package/dist/types/index.d.mts +3 -3
- package/dist/types/index.mjs +1 -1
- package/dist/types/storage.mjs +1 -1
- package/dist/{types-CT5vu6M0.d.mts → types-BcTumAz_.d.mts} +41 -7
- package/dist/{types-CwcUQ610.d.mts → types-BfC83KsP.d.mts} +15 -1
- package/dist/{types-DXYX7dIR.mjs → types-CoqIRrkY.mjs} +4 -4
- package/dist/{types-C4QE8qnm.d.mts → types-DVLddu2q.d.mts} +144 -1
- package/dist/usage/index.d.mts +1 -1
- package/dist/usage/index.mjs +0 -0
- package/dist/{userHelpers-BNsOXzOd.mjs → userHelpers-CWF5pbZD.mjs} +1 -1
- package/dist/utils/index.d.mts +57 -5
- package/dist/utils/index.mjs +12 -11
- package/dist/{utils-BtOha_Sp.mjs → utils-h1V_sT-t.mjs} +38 -7
- package/dist/{validate-PMCPR9El.mjs → validate-CdQlz52F.mjs} +2 -2
- package/dist/{versioning-D_rhHfZm.d.mts → versioning-DEs-_hFi.d.mts} +1 -1
- package/dist/{versioning-CKv2jz_U.mjs → versioning-rZbyLVD-.mjs} +3 -3
- package/package.json +17 -16
- package/skills/arc/SKILL.md +96 -4
- package/skills/arc/references/api-reference.md +19 -0
- package/skills/arc/references/production.md +2 -2
- package/dist/projection-CjHhM5W1.mjs +0 -18
package/README.md
CHANGED
|
@@ -147,7 +147,7 @@ defineResource({
|
|
|
147
147
|
defineResource({ name: 'pipeline', adapter, defaultLimit: 200, maxLimit: 500 });
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
-
`?limit=`
|
|
150
|
+
A `?limit=` above the cap is **clamped to it** and served, not rejected (changed in 2.33). Rejecting made the documented clamp unreachable and turned a benign over-ask into a 400 that callers commonly render as an empty list. The effective cap is carried in the OpenAPI parameter description, and the response's own `limit` reports what was applied.
|
|
151
151
|
|
|
152
152
|
---
|
|
153
153
|
|
package/bin/arc.js
CHANGED
|
@@ -42,17 +42,11 @@ const VERSION = getPackageVersion();
|
|
|
42
42
|
|
|
43
43
|
const args = process.argv.slice(2);
|
|
44
44
|
|
|
45
|
-
// Version
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// Help flag or no args
|
|
52
|
-
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
53
|
-
printHelp();
|
|
54
|
-
process.exit(0);
|
|
55
|
-
}
|
|
45
|
+
// Version / help are GLOBAL only in the leading position. Scanning the whole
|
|
46
|
+
// argv made `arc init --help` print the global help and exit, which left the
|
|
47
|
+
// per-command usage in COMMAND_HELP unreachable — a user asking how to use
|
|
48
|
+
// one command got the manual for all of them.
|
|
49
|
+
const leading = args[0];
|
|
56
50
|
|
|
57
51
|
// ============================================================================
|
|
58
52
|
// Command Routing
|
|
@@ -64,7 +58,7 @@ const [command, subcommand, ...rest] = args;
|
|
|
64
58
|
// through to the command (where `--help` was mistaken for an argument).
|
|
65
59
|
const COMMAND_ALIASES = { new: 'init', g: 'generate', i: 'introspect', desc: 'describe', d: 'docs' };
|
|
66
60
|
const COMMAND_HELP = {
|
|
67
|
-
init: 'Usage: arc init [name] [--jwt|--better-auth] [--mongokit|--custom] [--api-key]\n [--multi|--single] [--js] [--edge] [--docker|--no-docker] [--skip-install] [--force]\n Scaffold a new arc project (prompts when run in a TTY without a name).',
|
|
61
|
+
init: 'Usage: arc init [name] [--jwt|--better-auth] [--mongokit|--custom] [--api-key|--no-api-key] [--session cookie|bearer|both]\n [--multi|--single] [--js] [--edge] [--docker|--no-docker] [--skip-install] [--force]\n Scaffold a new arc project (prompts when run in a TTY without a name).',
|
|
68
62
|
generate: 'Usage: arc generate <resource|controller|model|repository|schemas|mcp> <name> [--mcp]\n Aliases: g; types r/c/m/repo/s. Name must be kebab-case (letters, digits, hyphens).',
|
|
69
63
|
introspect: 'Usage: arc introspect <entry-file>\n Print a human-readable summary of the resources an entry file exports.',
|
|
70
64
|
describe: 'Usage: arc describe <entry-file> [--json] [--pretty] [--entry <file>]\n Describe resources; --json emits machine output for AI agents.',
|
|
@@ -72,8 +66,27 @@ const COMMAND_HELP = {
|
|
|
72
66
|
doctor: 'Usage: arc doctor\n Diagnose common arc setup issues (exit 1 on hard failures).',
|
|
73
67
|
};
|
|
74
68
|
|
|
69
|
+
/**
|
|
70
|
+
* `process.exitCode` + return, never `process.exit()`.
|
|
71
|
+
*
|
|
72
|
+
* `process.exit()` tears the process down immediately; on POSIX a `stdout`
|
|
73
|
+
* that is a PIPE (`arc --help | less`, `arc describe --json > out.json`)
|
|
74
|
+
* writes asynchronously, so an exit racing the flush TRUNCATES output. Arc's
|
|
75
|
+
* own `doctor` already sets `exitCode`; this file is the one that did not.
|
|
76
|
+
* Setting the code and returning lets Node drain stdout and exit on its own.
|
|
77
|
+
*/
|
|
75
78
|
async function main() {
|
|
76
79
|
try {
|
|
80
|
+
// Global help/version are the LEADING argument only — see `leading`.
|
|
81
|
+
if (leading === '--version' || leading === '-v') {
|
|
82
|
+
console.log(`Arc CLI v${VERSION}`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (args.length === 0 || leading === '--help' || leading === '-h') {
|
|
86
|
+
printHelp();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
77
90
|
// `arc <command> --help` / `-h` → command-specific usage, exit 0.
|
|
78
91
|
const canonicalCommand = COMMAND_ALIASES[command] ?? command;
|
|
79
92
|
if (
|
|
@@ -117,7 +130,8 @@ async function main() {
|
|
|
117
130
|
default:
|
|
118
131
|
console.error(`Unknown command: ${command}`);
|
|
119
132
|
console.error('Run "arc --help" for usage');
|
|
120
|
-
process.
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
return;
|
|
121
135
|
}
|
|
122
136
|
} catch (err) {
|
|
123
137
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -125,7 +139,7 @@ async function main() {
|
|
|
125
139
|
if (process.env.DEBUG) {
|
|
126
140
|
console.error(err instanceof Error ? err.stack : err);
|
|
127
141
|
}
|
|
128
|
-
process.
|
|
142
|
+
process.exitCode = 1;
|
|
129
143
|
}
|
|
130
144
|
}
|
|
131
145
|
|
|
@@ -148,7 +162,8 @@ async function handleGenerate(type, args) {
|
|
|
148
162
|
console.log(' arc generate resource product --mcp');
|
|
149
163
|
console.log(' arc generate mcp product');
|
|
150
164
|
console.log(' arc g r invoice');
|
|
151
|
-
process.
|
|
165
|
+
process.exitCode = 1;
|
|
166
|
+
return;
|
|
152
167
|
}
|
|
153
168
|
|
|
154
169
|
// Normalize type shortcuts
|
|
@@ -170,14 +185,16 @@ async function handleGenerate(type, args) {
|
|
|
170
185
|
if (!normalizedType) {
|
|
171
186
|
console.error(`Unknown type: ${type}`);
|
|
172
187
|
console.log('Available types: resource (r), controller (c), model (m), repository (repo), schemas (s)');
|
|
173
|
-
process.
|
|
188
|
+
process.exitCode = 1;
|
|
189
|
+
return;
|
|
174
190
|
}
|
|
175
191
|
|
|
176
192
|
const name = args[0];
|
|
177
193
|
if (!name) {
|
|
178
194
|
console.error('Missing name argument');
|
|
179
195
|
console.log(`\nUsage: arc generate ${normalizedType} <name>`);
|
|
180
|
-
process.
|
|
196
|
+
process.exitCode = 1;
|
|
197
|
+
return;
|
|
181
198
|
}
|
|
182
199
|
|
|
183
200
|
// Import and run
|
|
@@ -250,6 +267,11 @@ function parseInitOptions(rawArgs) {
|
|
|
250
267
|
adapter: undefined,
|
|
251
268
|
auth: undefined,
|
|
252
269
|
tenant: undefined,
|
|
270
|
+
// Better Auth-specific. `undefined` (not a default) is load-bearing:
|
|
271
|
+
// options.ts prompts ONLY when the field is undefined, so defaulting
|
|
272
|
+
// here would silently skip the interactive question.
|
|
273
|
+
apiKey: undefined,
|
|
274
|
+
session: undefined,
|
|
253
275
|
typescript: undefined,
|
|
254
276
|
edge: undefined,
|
|
255
277
|
skipInstall: false,
|
|
@@ -309,6 +331,21 @@ function parseInitOptions(rawArgs) {
|
|
|
309
331
|
opts.auth = 'jwt';
|
|
310
332
|
break;
|
|
311
333
|
|
|
334
|
+
case '--api-key':
|
|
335
|
+
opts.apiKey = true;
|
|
336
|
+
break;
|
|
337
|
+
|
|
338
|
+
case '--no-api-key':
|
|
339
|
+
opts.apiKey = false;
|
|
340
|
+
break;
|
|
341
|
+
|
|
342
|
+
case '--session':
|
|
343
|
+
// cookie | bearer | both — options.ts falls back to the documented
|
|
344
|
+
// default for anything else.
|
|
345
|
+
opts.session = next;
|
|
346
|
+
i++;
|
|
347
|
+
break;
|
|
348
|
+
|
|
312
349
|
case '--edge':
|
|
313
350
|
case '--serverless':
|
|
314
351
|
opts.edge = true;
|
|
@@ -380,6 +417,7 @@ GLOBAL OPTIONS
|
|
|
380
417
|
--help, -h Show this help
|
|
381
418
|
|
|
382
419
|
INIT OPTIONS
|
|
420
|
+
--name, -n <name> Project name (alternative to the positional argument)
|
|
383
421
|
--mongokit Use MongoKit adapter (default, recommended)
|
|
384
422
|
--custom Use custom / Drizzle-ready adapter template
|
|
385
423
|
--better-auth Use Better Auth (default, recommended)
|
|
@@ -388,6 +426,9 @@ INIT OPTIONS
|
|
|
388
426
|
--single-tenant, --single Single-tenant mode (default)
|
|
389
427
|
--ts, --typescript Generate TypeScript (default)
|
|
390
428
|
--js, --javascript Generate JavaScript
|
|
429
|
+
--api-key Enable Better Auth's apiKey plugin (machine-to-machine)
|
|
430
|
+
--no-api-key Skip it without being prompted
|
|
431
|
+
--session <mode> Better Auth session strategy: cookie | bearer | both
|
|
391
432
|
--edge, --serverless Target Edge/Serverless environments
|
|
392
433
|
--docker Emit Dockerfile + docker-compose.yml (opt-in, 2.16)
|
|
393
434
|
--no-docker Skip Docker scaffolding even in interactive mode
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { h as SYSTEM_FIELDS, o as DEFAULT_TENANT_FIELD } from "./constants-
|
|
1
|
+
/*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
|
|
2
|
+
import { h as SYSTEM_FIELDS, o as DEFAULT_TENANT_FIELD } from "./constants-BxCoxmiN.mjs";
|
|
3
3
|
import { arcLog } from "./logger/index.mjs";
|
|
4
|
-
import { a as conjoinPolicyFilters } from "./authorizationDecision-
|
|
5
|
-
import { d as createDomainError, f as createError, i as NotFoundError, p as isArcError, r as ForbiddenError } from "./errors-
|
|
6
|
-
import { n as getUserRoles, t as getUserId } from "./userHelpers-
|
|
7
|
-
import { b as isElevated, c as getOrgId, n as PUBLIC_SCOPE, x as isMember } from "./types-
|
|
8
|
-
import { t as simpleEqualityMatcher } from "./simpleEqualityMatcher-
|
|
9
|
-
import { t as toRepositoryFilter } from "./repositoryFilter-
|
|
10
|
-
import { i as resolveEffectiveRoles, n as applyFieldWritePermissions } from "./fields-
|
|
11
|
-
import { g as ArcQueryParser, n as scheduleBackground } from "./utils-
|
|
12
|
-
import { t as buildQueryKey } from "./keys-
|
|
4
|
+
import { a as conjoinPolicyFilters } from "./authorizationDecision-B3mP_QTy.mjs";
|
|
5
|
+
import { d as createDomainError, f as createError, i as NotFoundError, p as isArcError, r as ForbiddenError } from "./errors-Cd_yy7aW.mjs";
|
|
6
|
+
import { n as getUserRoles, t as getUserId } from "./userHelpers-CWF5pbZD.mjs";
|
|
7
|
+
import { b as isElevated, c as getOrgId, n as PUBLIC_SCOPE, x as isMember } from "./types-CoqIRrkY.mjs";
|
|
8
|
+
import { t as simpleEqualityMatcher } from "./simpleEqualityMatcher-Se8VrsaP.mjs";
|
|
9
|
+
import { t as toRepositoryFilter } from "./repositoryFilter-DKny7PDn.mjs";
|
|
10
|
+
import { i as resolveEffectiveRoles, n as applyFieldWritePermissions } from "./fields-CojeUkS4.mjs";
|
|
11
|
+
import { g as ArcQueryParser, n as scheduleBackground } from "./utils-h1V_sT-t.mjs";
|
|
12
|
+
import { t as buildQueryKey } from "./keys-WeKvY1dE.mjs";
|
|
13
|
+
import { retryingTransaction } from "@classytic/repo-core/repository";
|
|
14
|
+
import { isProductionEnv } from "@classytic/primitives/environment";
|
|
13
15
|
//#region src/core/AccessControl.ts
|
|
14
16
|
const log$1 = arcLog("access-control");
|
|
15
17
|
var AccessControl = class {
|
|
@@ -259,9 +261,11 @@ const DEFAULT_FIELD_WRITE_DENIAL_POLICY = "reject";
|
|
|
259
261
|
var BodySanitizer = class {
|
|
260
262
|
schemaOptions;
|
|
261
263
|
onFieldWriteDenied;
|
|
264
|
+
onImmutableWrite;
|
|
262
265
|
constructor(config) {
|
|
263
266
|
this.schemaOptions = config.schemaOptions;
|
|
264
267
|
this.onFieldWriteDenied = config.onFieldWriteDenied ?? "reject";
|
|
268
|
+
this.onImmutableWrite = config.onImmutableWrite ?? (process.env.ARC_STRICT_IMMUTABLE_WRITES === "true" ? "reject" : "strip");
|
|
265
269
|
}
|
|
266
270
|
/**
|
|
267
271
|
* Strip readonly and system-managed fields from request body.
|
|
@@ -276,11 +280,16 @@ var BodySanitizer = class {
|
|
|
276
280
|
const scopeForRules = req ? (meta ?? req.metadata)?._scope ?? PUBLIC_SCOPE : void 0;
|
|
277
281
|
const scopeIsElevated = scopeForRules ? isElevated(scopeForRules) : false;
|
|
278
282
|
const fieldRules = this.schemaOptions.fieldRules ?? {};
|
|
283
|
+
const immutableAttempts = [];
|
|
279
284
|
for (const [field, rules] of Object.entries(fieldRules)) {
|
|
280
285
|
const bypass = Boolean(rules.preserveForElevated) && scopeIsElevated;
|
|
281
286
|
if ((rules.systemManaged || rules.readonly) && !bypass) delete sanitized[field];
|
|
282
|
-
if (_operation === "update" && (rules.immutable || rules.immutableAfterCreate) && !bypass)
|
|
287
|
+
if (_operation === "update" && (rules.immutable || rules.immutableAfterCreate) && !bypass) {
|
|
288
|
+
if (Object.hasOwn(sanitized, field)) immutableAttempts.push(field);
|
|
289
|
+
delete sanitized[field];
|
|
290
|
+
}
|
|
283
291
|
}
|
|
292
|
+
if (immutableAttempts.length > 0 && this.onImmutableWrite === "reject") throw new ForbiddenError(`Cannot modify immutable field${immutableAttempts.length === 1 ? "" : "s"}: ${immutableAttempts.join(", ")}`);
|
|
284
293
|
if (req) {
|
|
285
294
|
const arcContext = meta ?? req.metadata;
|
|
286
295
|
const scope = arcContext?._scope ?? PUBLIC_SCOPE;
|
|
@@ -363,6 +372,73 @@ function buildTenantRepoOptions(req, tenantField, arcContext) {
|
|
|
363
372
|
return out;
|
|
364
373
|
}
|
|
365
374
|
/**
|
|
375
|
+
* Marks a write method ARC installed.
|
|
376
|
+
*
|
|
377
|
+
* The constructor binds `create`/`update`/`delete`, so EVERY controller owns
|
|
378
|
+
* all three and the bound function is a fresh object per instance —
|
|
379
|
+
* `Object.hasOwn` and identity comparison both say nothing about overrides.
|
|
380
|
+
* Provenance does: a class field initialises after `super()` and REPLACES the
|
|
381
|
+
* bound method, mark and all.
|
|
382
|
+
*
|
|
383
|
+
* `Symbol.for` so it still matches across two arc copies in one graph.
|
|
384
|
+
*/
|
|
385
|
+
const ARC_BOUND_WRITE = Symbol.for("arc.boundWriteMethod");
|
|
386
|
+
/**
|
|
387
|
+
* Marks a controller PROTOTYPE whose write methods dispatch declared verbs.
|
|
388
|
+
* `defineResource` refuses `writes` without it: a duck-typed `configure()`
|
|
389
|
+
* proves an options channel exists, not that anything dispatches from it.
|
|
390
|
+
* Lives on the prototype — one mark per class, zero per-instance cost.
|
|
391
|
+
*/
|
|
392
|
+
const ARC_WRITE_VERB_CAPABLE = Symbol.for("arc.writeVerbCapable");
|
|
393
|
+
/** Tag a bound write method as arc's own. Returns the same function. */
|
|
394
|
+
function markArcBoundWrite(fn) {
|
|
395
|
+
Object.defineProperty(fn, ARC_BOUND_WRITE, {
|
|
396
|
+
value: true,
|
|
397
|
+
enumerable: false,
|
|
398
|
+
configurable: true
|
|
399
|
+
});
|
|
400
|
+
return fn;
|
|
401
|
+
}
|
|
402
|
+
/** Did arc install this write method, as opposed to the host replacing it? */
|
|
403
|
+
function isArcBoundWrite(fn) {
|
|
404
|
+
return typeof fn === "function" && Reflect.get(fn, ARC_BOUND_WRITE) === true;
|
|
405
|
+
}
|
|
406
|
+
/** Tag a controller class's prototype as write-verb capable. */
|
|
407
|
+
function markWriteVerbCapable(prototype) {
|
|
408
|
+
Object.defineProperty(prototype, ARC_WRITE_VERB_CAPABLE, {
|
|
409
|
+
value: true,
|
|
410
|
+
enumerable: false,
|
|
411
|
+
configurable: true
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
/** Do this controller's write methods dispatch declared write verbs? */
|
|
415
|
+
function isWriteVerbCapable(controller) {
|
|
416
|
+
return typeof controller === "object" && controller !== null && Reflect.get(controller, ARC_WRITE_VERB_CAPABLE) === true;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Which of `ops` did the host override?
|
|
420
|
+
*
|
|
421
|
+
* THE single implementation — the boot-fatal reachability check and the
|
|
422
|
+
* field-protection warn both consume it, so they cannot disagree.
|
|
423
|
+
*
|
|
424
|
+
* Two shapes: a prototype method (found by walking to the capability-marked
|
|
425
|
+
* prototype — every layer between is host-authored) and a class field / ctor
|
|
426
|
+
* assignment (found by provenance, since arc owns an own-property for all
|
|
427
|
+
* three regardless). Arc's own mixins define no write methods; a test pins it.
|
|
428
|
+
*/
|
|
429
|
+
function detectOverriddenWriteSlots(controller, ops) {
|
|
430
|
+
const instance = controller;
|
|
431
|
+
return ops.filter((op) => {
|
|
432
|
+
if (typeof instance[op] === "function" && !isArcBoundWrite(instance[op])) return true;
|
|
433
|
+
let proto = Object.getPrototypeOf(controller);
|
|
434
|
+
while (proto && !Object.hasOwn(proto, ARC_WRITE_VERB_CAPABLE)) {
|
|
435
|
+
if (Object.hasOwn(proto, op)) return true;
|
|
436
|
+
proto = Object.getPrototypeOf(proto);
|
|
437
|
+
}
|
|
438
|
+
return false;
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
366
442
|
* Resolve the repository primary key for mutation calls.
|
|
367
443
|
*
|
|
368
444
|
* When the resource declares a custom `idField` (slug, jobId, UUID), the
|
|
@@ -634,23 +710,44 @@ const log = arcLog("query");
|
|
|
634
710
|
*/
|
|
635
711
|
let notedDeniedJoins = false;
|
|
636
712
|
function noteDeniedJoins(kind) {
|
|
637
|
-
if (notedDeniedJoins || process.env.NODE_ENV
|
|
713
|
+
if (notedDeniedJoins || isProductionEnv(process.env.NODE_ENV)) return;
|
|
638
714
|
notedDeniedJoins = true;
|
|
639
715
|
log.warn(`client requested ${kind} but no allowlist is configured — DENIED (2.24 default flip; it was allow-all). Set schemaOptions.query.allowedPopulate / allowedLookups to enable client-driven joins. (dev-only note, shown once)`);
|
|
640
716
|
}
|
|
641
717
|
var QueryResolver = class {
|
|
642
718
|
queryParser;
|
|
643
719
|
maxLimit;
|
|
720
|
+
/** Set only when the HOST passed one — it outranks any parser's cap, at any time. */
|
|
721
|
+
explicitMaxLimit;
|
|
644
722
|
defaultLimit;
|
|
645
723
|
/** `undefined` means "no default sort" (caller passed `false`). */
|
|
646
724
|
defaultSort;
|
|
725
|
+
explicitDefaultSort;
|
|
647
726
|
schemaOptions;
|
|
648
727
|
tenantField;
|
|
649
728
|
constructor(config = {}) {
|
|
650
729
|
this.queryParser = config.queryParser ?? getDefaultQueryParser();
|
|
651
|
-
|
|
730
|
+
/**
|
|
731
|
+
* Precedence: explicit config → the PARSER's own cap → framework default.
|
|
732
|
+
*
|
|
733
|
+
* The middle term is the fix. A resource that writes
|
|
734
|
+
* `new QueryParser({ maxLimit: 1000 })` has already answered "how large may a page
|
|
735
|
+
* be?", and arc previously ignored that and applied 100 anyway. Three layers each
|
|
736
|
+
* capping independently, lowest wins, no signal — a chart-of-accounts picker
|
|
737
|
+
* returned 100 of 696 rows and rendered "No accounts found", while the resource
|
|
738
|
+
* AND the repository were both configured for 1000.
|
|
739
|
+
*
|
|
740
|
+
* Note it reads `config.queryParser`, NOT `this.queryParser`. The fallback default
|
|
741
|
+
* parser declares its own generous cap, so deferring to it would silently raise the
|
|
742
|
+
* ceiling from 100 to 1000 on every endpoint that never configured one — a widening
|
|
743
|
+
* nobody asked for. Only a parser the resource EXPLICITLY supplied counts as an
|
|
744
|
+
* answer; a test pins exactly this.
|
|
745
|
+
*/
|
|
746
|
+
this.explicitMaxLimit = config.maxLimit;
|
|
747
|
+
this.maxLimit = config.maxLimit ?? config.queryParser?.maxLimit ?? 100;
|
|
652
748
|
this.defaultLimit = config.defaultLimit ?? 20;
|
|
653
749
|
this.defaultSort = config.defaultSort === false ? void 0 : config.defaultSort ?? "-createdAt";
|
|
750
|
+
this.explicitDefaultSort = config.defaultSort !== void 0 && config.defaultSort !== false;
|
|
654
751
|
this.schemaOptions = config.schemaOptions ?? {};
|
|
655
752
|
this.tenantField = config.tenantField !== void 0 ? config.tenantField : DEFAULT_TENANT_FIELD;
|
|
656
753
|
}
|
|
@@ -662,6 +759,42 @@ var QueryResolver = class {
|
|
|
662
759
|
*/
|
|
663
760
|
setParser(parser) {
|
|
664
761
|
this.queryParser = parser;
|
|
762
|
+
/**
|
|
763
|
+
* The swapped-in parser's cap applies too — swapping is how most resources supply
|
|
764
|
+
* theirs.
|
|
765
|
+
*
|
|
766
|
+
* Deferring only in the constructor was not enough: `setQueryParser()` mutates the
|
|
767
|
+
* resolver in place so captured references stay valid, so a resource that declares
|
|
768
|
+
* `new QueryParser({ maxLimit: 1000 })` arrives HERE, not through `config`. The
|
|
769
|
+
* constructor-only version of this fix looked right, passed its unit tests, and
|
|
770
|
+
* changed nothing at runtime — the chart of accounts still served 100 of 696.
|
|
771
|
+
*
|
|
772
|
+
* An explicit `config.maxLimit` still wins; that is a host decision and outranks a
|
|
773
|
+
* package default either way.
|
|
774
|
+
*/
|
|
775
|
+
if (this.explicitMaxLimit === void 0 && parser.maxLimit !== void 0) this.maxLimit = parser.maxLimit;
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* The default sort, but only when the parser permits the field.
|
|
779
|
+
*
|
|
780
|
+
* A resource declaring `allowedSortFields` has stated exactly what may be
|
|
781
|
+
* sorted on. Arc then applied its OWN default of `-createdAt` on top, and
|
|
782
|
+
* mongokit 3.31 REJECTS a sort outside the allowlist rather than dropping it —
|
|
783
|
+
* so every list call on such a resource answered 400, `Blocked sort field not
|
|
784
|
+
* in allowlist: createdAt`. REST and MCP alike; the endpoint was unusable, and
|
|
785
|
+
* nothing the caller sent could avoid it.
|
|
786
|
+
*
|
|
787
|
+
* A general default must never outrank a specific declaration, so the
|
|
788
|
+
* FRAMEWORK default withdraws. A HOST-declared `defaultSort` is left alone —
|
|
789
|
+
* that contradiction is its author's to resolve, and `defineResource` reports
|
|
790
|
+
* it as a boot diagnostic instead of arc silently picking a winner.
|
|
791
|
+
*/
|
|
792
|
+
permittedDefaultSort() {
|
|
793
|
+
const sort = this.defaultSort;
|
|
794
|
+
if (sort === void 0 || this.explicitDefaultSort) return sort;
|
|
795
|
+
const allowed = this.queryParser.allowedSortFields;
|
|
796
|
+
if (!allowed || allowed.length === 0) return sort;
|
|
797
|
+
return sort.split(",").map((s) => s.trim().replace(/^-/, "")).filter(Boolean).every((f) => allowed.includes(f)) ? sort : void 0;
|
|
665
798
|
}
|
|
666
799
|
/**
|
|
667
800
|
* Resolve a request into parsed query options -- ONE parse per request.
|
|
@@ -673,7 +806,7 @@ var QueryResolver = class {
|
|
|
673
806
|
delete parsed.filters?._policyFilters;
|
|
674
807
|
const limit = Math.min(Math.max(1, parsed.limit || this.defaultLimit), this.maxLimit);
|
|
675
808
|
const page = parsed.after ? void 0 : parsed.page ? Math.max(1, parsed.page) : 1;
|
|
676
|
-
const sortString = parsed.sort ? Object.entries(parsed.sort).map(([k, v]) => v === -1 ? `-${k}` : k).join(",") : this.
|
|
809
|
+
const sortString = parsed.sort ? Object.entries(parsed.sort).map(([k, v]) => v === -1 ? `-${k}` : k).join(",") : this.permittedDefaultSort();
|
|
677
810
|
const rawSelect = parsed.select ?? req.query?.select;
|
|
678
811
|
const policyFilters = arcContext?._policyFilters;
|
|
679
812
|
let filters = conjoinPolicyFilters(parsed.filters, policyFilters);
|
|
@@ -838,11 +971,38 @@ var BaseCrudController = class {
|
|
|
838
971
|
*/
|
|
839
972
|
_defaultSort;
|
|
840
973
|
_onFieldWriteDenied;
|
|
974
|
+
_onImmutableWrite;
|
|
975
|
+
/**
|
|
976
|
+
* Domain commands bound to the write slots (`ResourceWrites`). Consulted at
|
|
977
|
+
* the PERSISTENCE step only — everything upstream of it (sanitizer, tenant
|
|
978
|
+
* injection, actor stamp, hook sandwich) runs exactly as it does without
|
|
979
|
+
* one, which is the entire point of the seam.
|
|
980
|
+
*/
|
|
981
|
+
_writes;
|
|
982
|
+
/** Transactional write envelope — see `ResourceConfig.transactional`. */
|
|
983
|
+
_transactional = false;
|
|
841
984
|
constructor(repository, options = {}) {
|
|
842
985
|
this.repository = repository;
|
|
843
986
|
this.schemaOptions = options.schemaOptions ?? {};
|
|
844
987
|
this.queryParser = options.queryParser ?? getDefaultQueryParser();
|
|
845
|
-
|
|
988
|
+
/**
|
|
989
|
+
* The parser's cap is consulted BEFORE the framework default.
|
|
990
|
+
*
|
|
991
|
+
* A resource that hands us a parser has already stated how large a page it
|
|
992
|
+
* can serve — a bounded catalog like a chart of accounts declares 1000 so
|
|
993
|
+
* pickers can read it whole. Defaulting straight to 100 here silently
|
|
994
|
+
* overrode that: the resource asked for 1000, `QueryResolver` received a
|
|
995
|
+
* `maxLimit` of 100 that was indistinguishable from a host deliberately
|
|
996
|
+
* choosing 100, and the list came back truncated with a `200`. be-prod
|
|
997
|
+
* served 100 of 696 accounts that way, and every account picker in the
|
|
998
|
+
* product was short by 85% with nothing anywhere to report it.
|
|
999
|
+
*
|
|
1000
|
+
* Order matters and is the whole point: an explicit `options.maxLimit` from
|
|
1001
|
+
* the host still wins over the parser, and the parser wins over this
|
|
1002
|
+
* framework floor. A general default must only ever fill a gap — never
|
|
1003
|
+
* outrank something specific that was actually stated.
|
|
1004
|
+
*/
|
|
1005
|
+
this.maxLimit = options.maxLimit ?? this.queryParser?.maxLimit ?? 100;
|
|
846
1006
|
this.defaultLimit = options.defaultLimit ?? 20;
|
|
847
1007
|
this.resourceName = options.resourceName;
|
|
848
1008
|
this.tenantField = options.tenantField !== void 0 ? options.tenantField : DEFAULT_TENANT_FIELD;
|
|
@@ -852,6 +1012,9 @@ var BaseCrudController = class {
|
|
|
852
1012
|
if (options.presetFields) this._presetFields = options.presetFields;
|
|
853
1013
|
this._defaultSort = options.defaultSort;
|
|
854
1014
|
this._onFieldWriteDenied = options.onFieldWriteDenied;
|
|
1015
|
+
this._onImmutableWrite = options.onImmutableWrite;
|
|
1016
|
+
this._writes = options.writes;
|
|
1017
|
+
this._transactional = options.transactional === true;
|
|
855
1018
|
this.accessControl = new AccessControl({
|
|
856
1019
|
tenantField: this.tenantField,
|
|
857
1020
|
idField: this.idField,
|
|
@@ -859,7 +1022,8 @@ var BaseCrudController = class {
|
|
|
859
1022
|
});
|
|
860
1023
|
this.bodySanitizer = new BodySanitizer({
|
|
861
1024
|
schemaOptions: this.schemaOptions,
|
|
862
|
-
onFieldWriteDenied: this._onFieldWriteDenied
|
|
1025
|
+
onFieldWriteDenied: this._onFieldWriteDenied,
|
|
1026
|
+
onImmutableWrite: this._onImmutableWrite
|
|
863
1027
|
});
|
|
864
1028
|
this.queryResolver = new QueryResolver({
|
|
865
1029
|
queryParser: this.queryParser,
|
|
@@ -871,9 +1035,21 @@ var BaseCrudController = class {
|
|
|
871
1035
|
});
|
|
872
1036
|
this.list = this.list.bind(this);
|
|
873
1037
|
this.get = this.get.bind(this);
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1038
|
+
/**
|
|
1039
|
+
* The write methods are bound here, which means EVERY instance carries an
|
|
1040
|
+
* own `create`/`update`/`delete` property — so "does it have an own
|
|
1041
|
+
* property" cannot distinguish arc's binding from a host's override.
|
|
1042
|
+
*
|
|
1043
|
+
* Marking what arc installed makes the difference observable. A class-field
|
|
1044
|
+
* override (`update = async (req) => …`) initialises AFTER `super()`
|
|
1045
|
+
* returns and therefore REPLACES this bound method outright — the mark goes
|
|
1046
|
+
* with it, which is exactly the signal `warnOnWriteMethodOverride` needs.
|
|
1047
|
+
* A prototype-method override binds through here and stays marked; that
|
|
1048
|
+
* shape is caught by the prototype comparison instead.
|
|
1049
|
+
*/
|
|
1050
|
+
this.create = markArcBoundWrite(this.create.bind(this));
|
|
1051
|
+
this.update = markArcBoundWrite(this.update.bind(this));
|
|
1052
|
+
this.delete = markArcBoundWrite(this.delete.bind(this));
|
|
877
1053
|
}
|
|
878
1054
|
/**
|
|
879
1055
|
* Swap the controller's query parser. Mutates the existing `QueryResolver`
|
|
@@ -939,6 +1115,10 @@ var BaseCrudController = class {
|
|
|
939
1115
|
rebuildBodySanitizer = true;
|
|
940
1116
|
rebuildQueryResolver = true;
|
|
941
1117
|
}
|
|
1118
|
+
if (options.onImmutableWrite !== void 0) {
|
|
1119
|
+
this._onImmutableWrite = options.onImmutableWrite;
|
|
1120
|
+
rebuildBodySanitizer = true;
|
|
1121
|
+
}
|
|
942
1122
|
if (options.onFieldWriteDenied !== void 0) {
|
|
943
1123
|
this._onFieldWriteDenied = options.onFieldWriteDenied;
|
|
944
1124
|
rebuildBodySanitizer = true;
|
|
@@ -958,6 +1138,8 @@ var BaseCrudController = class {
|
|
|
958
1138
|
}
|
|
959
1139
|
if (options.cache !== void 0) this._cacheConfig = options.cache;
|
|
960
1140
|
if (options.presetFields !== void 0) this._presetFields = options.presetFields;
|
|
1141
|
+
if (options.writes !== void 0) this._writes = options.writes;
|
|
1142
|
+
if (options.transactional !== void 0) this._transactional = options.transactional === true;
|
|
961
1143
|
if (rebuildAccessControl) this.accessControl = new AccessControl({
|
|
962
1144
|
tenantField: this.tenantField,
|
|
963
1145
|
idField: this.idField,
|
|
@@ -965,7 +1147,8 @@ var BaseCrudController = class {
|
|
|
965
1147
|
});
|
|
966
1148
|
if (rebuildBodySanitizer) this.bodySanitizer = new BodySanitizer({
|
|
967
1149
|
schemaOptions: this.schemaOptions,
|
|
968
|
-
onFieldWriteDenied: this._onFieldWriteDenied
|
|
1150
|
+
onFieldWriteDenied: this._onFieldWriteDenied,
|
|
1151
|
+
onImmutableWrite: this._onImmutableWrite
|
|
969
1152
|
});
|
|
970
1153
|
if (rebuildQueryResolver) this.queryResolver = new QueryResolver({
|
|
971
1154
|
queryParser: this.queryParser,
|
|
@@ -1105,6 +1288,52 @@ var BaseCrudController = class {
|
|
|
1105
1288
|
return executeHookedOp(this.hookOpContext(req), args, executor);
|
|
1106
1289
|
}
|
|
1107
1290
|
/**
|
|
1291
|
+
* Run a write's PERSISTENCE step, transactionally when the resource asked.
|
|
1292
|
+
*
|
|
1293
|
+
* Plain mode hands `fn` the live repository. Transactional mode runs it
|
|
1294
|
+
* through repo-core's `retryingTransaction`: the callback receives the
|
|
1295
|
+
* TX-BOUND repository (so declared verbs and repository calls join the same
|
|
1296
|
+
* transaction), transient conflicts re-run `fn` with jittered bounded
|
|
1297
|
+
* backoff, everything else — including `VersionConflictError` — surfaces
|
|
1298
|
+
* once. Hooks stay OUTSIDE: before/around ran already and must not re-run;
|
|
1299
|
+
* after-hooks run post-commit, which they already do by running after this.
|
|
1300
|
+
*/
|
|
1301
|
+
runWritePersistence(fn) {
|
|
1302
|
+
if (!this._transactional) return fn(this.repository);
|
|
1303
|
+
return retryingTransaction(this.repository, (txRepo, uow) => fn(txRepo, uow));
|
|
1304
|
+
}
|
|
1305
|
+
/** Assemble the `WriteContext` a `create` verb receives. */
|
|
1306
|
+
writeContext(req, repo = this.repository, uow) {
|
|
1307
|
+
return {
|
|
1308
|
+
req,
|
|
1309
|
+
repository: repo,
|
|
1310
|
+
...uow !== void 0 ? { uow } : {}
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Assemble the `MutationWriteContext` an `update` / `delete` verb receives.
|
|
1315
|
+
*
|
|
1316
|
+
* `existing` is the document arc ALREADY loaded to run its permission and
|
|
1317
|
+
* tenant checks — `loadMutableTarget` throws before any verb runs when it
|
|
1318
|
+
* cannot produce one, which is why the context types it required. Handing
|
|
1319
|
+
* it over rather than letting the verb re-fetch is deliberate: a second
|
|
1320
|
+
* read can observe a different document than the one the request was
|
|
1321
|
+
* authorised against.
|
|
1322
|
+
*
|
|
1323
|
+
* `id` is the REPOSITORY PRIMARY KEY (`repoId`), never the raw route param —
|
|
1324
|
+
* see the note on `MutationWriteContext.id`. Callers pass `resolveRepoId`'s
|
|
1325
|
+
* output.
|
|
1326
|
+
*/
|
|
1327
|
+
mutationWriteContext(req, id, existing, repo = this.repository, uow) {
|
|
1328
|
+
return {
|
|
1329
|
+
req,
|
|
1330
|
+
repository: repo,
|
|
1331
|
+
id,
|
|
1332
|
+
existing,
|
|
1333
|
+
...uow !== void 0 ? { uow } : {}
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1108
1337
|
* Run `executeAfter` for the given op + data. No-op when hooks aren't
|
|
1109
1338
|
* wired or `resourceName` isn't set. Caller passes the data shape it
|
|
1110
1339
|
* wants downstream after-handlers to receive — typically the result for
|
|
@@ -1340,11 +1569,21 @@ var BaseCrudController = class {
|
|
|
1340
1569
|
const item = await this.runHookedOpUntilResult(req, {
|
|
1341
1570
|
op: "create",
|
|
1342
1571
|
input: data
|
|
1343
|
-
}, async (processed) => this.
|
|
1572
|
+
}, async (processed) => this.runWritePersistence((repo, uow) => this._writes?.create ? this._writes.create(processed, this.writeContext(req, repo, uow)) : repo.create(processed, {
|
|
1344
1573
|
user,
|
|
1345
1574
|
context: arcContext,
|
|
1346
1575
|
...this.tenantRepoOptions(req)
|
|
1347
|
-
}));
|
|
1576
|
+
})));
|
|
1577
|
+
/**
|
|
1578
|
+
* A create COMMAND must return the created document — enforced, because
|
|
1579
|
+
* the alternative is quietly worse than any failure: a `201` carrying
|
|
1580
|
+
* `undefined` data, after-hooks (audit, events, cache invalidation) fed a
|
|
1581
|
+
* non-document, and a client that believes the write happened with no way
|
|
1582
|
+
* to address what it created. TypeScript types the contract, but arc is a
|
|
1583
|
+
* JavaScript runtime framework and a JS command returning nothing is one
|
|
1584
|
+
* missing `return` away.
|
|
1585
|
+
*/
|
|
1586
|
+
if (this._writes?.create && item == null) throw createError(500, `writes.create for resource "${this.resourceName ?? "unknown"}" returned ${item === null ? "null" : "undefined"} — a create command must return the created document. Signal failure by throwing a typed error; a nullish return would otherwise answer 201 with no document.`, { code: "WRITE_VERB_CONTRACT_VIOLATION" });
|
|
1348
1587
|
await this.runAfterHook(req, "create", item);
|
|
1349
1588
|
return {
|
|
1350
1589
|
data: item,
|
|
@@ -1367,15 +1606,27 @@ var BaseCrudController = class {
|
|
|
1367
1606
|
op: "update",
|
|
1368
1607
|
input: data,
|
|
1369
1608
|
meta: hookMeta
|
|
1370
|
-
}, async (processed) => this.
|
|
1609
|
+
}, async (processed) => this.runWritePersistence((repo, uow) => this._writes?.update ? this._writes.update(repoId, processed, this.mutationWriteContext(req, repoId, existing, repo, uow)) : repo.update(repoId, processed, {
|
|
1371
1610
|
user,
|
|
1372
1611
|
context: arcContext,
|
|
1373
1612
|
...this.tenantRepoOptions(req)
|
|
1374
|
-
}));
|
|
1375
|
-
|
|
1376
|
-
|
|
1613
|
+
})));
|
|
1614
|
+
/**
|
|
1615
|
+
* A declared verb signals "not found" by THROWING, so reaching this line
|
|
1616
|
+
* means the command succeeded. Translating its `void` return into a 404
|
|
1617
|
+
* would report failure for a write that happened — the repository's
|
|
1618
|
+
* `null`-means-miss contract is the repository's, not a domain command's.
|
|
1619
|
+
* Re-read so the response still carries the document.
|
|
1620
|
+
*/
|
|
1621
|
+
const resolved = this._writes?.update ? item ?? await this.repository.getById(repoId, {
|
|
1622
|
+
user,
|
|
1623
|
+
context: arcContext,
|
|
1624
|
+
...this.tenantRepoOptions(req)
|
|
1625
|
+
}) : item;
|
|
1626
|
+
if (!resolved) this.throwNotFound("NOT_FOUND");
|
|
1627
|
+
await this.runAfterHook(req, "update", resolved, hookMeta);
|
|
1377
1628
|
return {
|
|
1378
|
-
data:
|
|
1629
|
+
data: resolved,
|
|
1379
1630
|
status: 200,
|
|
1380
1631
|
meta: { message: "Updated successfully" }
|
|
1381
1632
|
};
|
|
@@ -1391,15 +1642,15 @@ var BaseCrudController = class {
|
|
|
1391
1642
|
input: existing,
|
|
1392
1643
|
meta: hookMeta,
|
|
1393
1644
|
pipeProcessedData: false
|
|
1394
|
-
}, async () => this.
|
|
1645
|
+
}, async () => this.runWritePersistence((repo, uow) => this._writes?.delete ? this._writes.delete(repoId, this.mutationWriteContext(req, repoId, existing, repo, uow)) : repo.delete(repoId, {
|
|
1395
1646
|
user,
|
|
1396
1647
|
context: arcContext,
|
|
1397
1648
|
...this.tenantRepoOptions(req),
|
|
1398
1649
|
...deleteMode ? { mode: deleteMode } : {}
|
|
1399
|
-
}));
|
|
1400
|
-
if (!result) this.throwNotFound("NOT_FOUND");
|
|
1650
|
+
})));
|
|
1651
|
+
if (!result && !this._writes?.delete) this.throwNotFound("NOT_FOUND");
|
|
1401
1652
|
await this.runAfterHook(req, "delete", existing, hookMeta);
|
|
1402
|
-
const deleteResult = result;
|
|
1653
|
+
const deleteResult = result ?? {};
|
|
1403
1654
|
return {
|
|
1404
1655
|
data: {
|
|
1405
1656
|
message: deleteResult.message || "Deleted successfully",
|
|
@@ -1410,6 +1661,15 @@ var BaseCrudController = class {
|
|
|
1410
1661
|
};
|
|
1411
1662
|
}
|
|
1412
1663
|
};
|
|
1664
|
+
/**
|
|
1665
|
+
* This class's write methods dispatch declared write verbs (`_writes`), so
|
|
1666
|
+
* `defineResource` may accept a `writes` declaration for controllers built on
|
|
1667
|
+
* it. One mark on the prototype — inherited by every subclass and mixin
|
|
1668
|
+
* composition, zero per-instance cost. A duck-typed `configure()` is NOT this
|
|
1669
|
+
* proof: it shows an options channel exists, not that anything dispatches
|
|
1670
|
+
* `writes` out of it.
|
|
1671
|
+
*/
|
|
1672
|
+
markWriteVerbCapable(BaseCrudController.prototype);
|
|
1413
1673
|
//#endregion
|
|
1414
1674
|
//#region src/core/mixins/bulk.ts
|
|
1415
1675
|
/**
|
|
@@ -1752,4 +2012,4 @@ function TreeMixin(Base) {
|
|
|
1752
2012
|
*/
|
|
1753
2013
|
var BaseController = class extends SoftDeleteMixin(TreeMixin(SlugMixin(BulkMixin(BaseCrudController)))) {};
|
|
1754
2014
|
//#endregion
|
|
1755
|
-
export { BulkMixin as a, collectReadBlockedFields as c,
|
|
2015
|
+
export { BulkMixin as a, collectReadBlockedFields as c, isWriteVerbCapable as d, BodySanitizer as f, SlugMixin as i, isFieldReadable as l, AccessControl as m, TreeMixin as n, BaseCrudController as o, DEFAULT_FIELD_WRITE_DENIAL_POLICY as p, SoftDeleteMixin as r, QueryResolver as s, BaseController as t, detectOverriddenWriteSlots as u };
|