@classytic/arc 2.14.2 → 2.15.3
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 +44 -0
- package/dist/{BaseController-Dv60tU83.mjs → BaseController-dx3m2J8V.mjs} +102 -2
- package/dist/auth/index.d.mts +1 -1
- package/dist/{buildHandler-jSZ6Fdvi.mjs → buildHandler-CcFOpJLh.mjs} +2 -19
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +2 -2
- package/dist/{core-D29kkRL5.mjs → core-CvmOqEms.mjs} +70 -13
- package/dist/{createAggregationRouter-DhR-Ofiz.mjs → createAggregationRouter-B0bPDf5b.mjs} +7 -5
- package/dist/{createApp-BarYhXCZ.mjs → createApp-PFegs47-.mjs} +64 -4
- package/dist/docs/index.d.mts +1 -1
- package/dist/factory/index.d.mts +2 -2
- package/dist/factory/index.mjs +1 -1
- package/dist/hooks/index.d.mts +1 -1
- package/dist/{index-D1-Kp_dP.d.mts → index-BstGxcc3.d.mts} +1 -1
- package/dist/{index-Dwc0orNd.d.mts → index-BswOSJCE.d.mts} +88 -17
- package/dist/{index-Bt0F3nJj.d.mts → index-bRjYu21O.d.mts} +1 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +3 -3
- 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/middleware/index.d.mts +1 -1
- package/dist/org/index.d.mts +1 -1
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +2 -35
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +1 -1
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +2 -1
- package/dist/presets/search.d.mts +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/{resourceToTools-BM686jB4.mjs → resourceToTools-tFYUNmM0.mjs} +2 -2
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +1 -1
- package/dist/types/index.d.mts +1 -1
- package/dist/{types-C6ONJ_Z2.d.mts → types-BQsjgQzS.d.mts} +1 -1
- package/dist/{types-NGtx3uxV.d.mts → types-DrBaUwyV.d.mts} +40 -4
- package/dist/utils/index.d.mts +1 -1
- package/dist/{versioning-DTTvc80y.d.mts → versioning-hmkPcDlX.d.mts} +34 -1
- package/package.json +5 -5
- package/skills/arc/SKILL.md +77 -0
- package/skills/arc-code-review/references/anti-patterns.md +44 -0
- package/skills/arc-code-review/references/arc-cheatsheet.md +27 -0
package/README.md
CHANGED
|
@@ -155,6 +155,50 @@ Custom checks return `{ granted, reason?, filters?, scope? }` — `filters` prop
|
|
|
155
155
|
|
|
156
156
|
---
|
|
157
157
|
|
|
158
|
+
## Aggregations
|
|
159
|
+
|
|
160
|
+
Add `aggregations: { … }` to a resource and arc registers `GET /:prefix/aggregations/:name` per entry. Each runs a portable `$match → $group → $project → $sort → $limit` pipeline against the kit's `repo.aggregate(req, options)` — same shape across mongokit / sqlitekit / prismakit, so dashboards work unchanged across backends.
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import { defineResource, defineAggregation } from '@classytic/arc';
|
|
164
|
+
|
|
165
|
+
defineResource({
|
|
166
|
+
name: 'transaction',
|
|
167
|
+
adapter,
|
|
168
|
+
presets: [multiTenantPreset({ tenantField: 'organizationId' })],
|
|
169
|
+
permissions: { list: canViewRevenue() },
|
|
170
|
+
|
|
171
|
+
aggregations: {
|
|
172
|
+
byPaymentMethod: defineAggregation({
|
|
173
|
+
groupBy: 'method',
|
|
174
|
+
measures: { total: 'sum:amount', count: 'count' },
|
|
175
|
+
sort: { total: -1 },
|
|
176
|
+
cache: { staleTime: 60, swr: true, tags: ['revenue'] },
|
|
177
|
+
permissions: canViewRevenue(),
|
|
178
|
+
}),
|
|
179
|
+
byDay: defineAggregation({
|
|
180
|
+
dateBuckets: { day: { field: 'createdAt', interval: 'day' } },
|
|
181
|
+
groupBy: 'flow',
|
|
182
|
+
measures: { total: 'sum:amount', count: 'count' },
|
|
183
|
+
requireDateRange: { field: 'createdAt', maxRangeDays: 365 },
|
|
184
|
+
cache: { staleTime: 60, swr: true, tags: ['revenue'] },
|
|
185
|
+
permissions: canViewRevenue(),
|
|
186
|
+
}),
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Caller filters via query string compose with the declaration:
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
GET /api/transactions/aggregations/byPaymentMethod?status=verified
|
|
195
|
+
GET /api/transactions/aggregations/byDay?createdAt[gte]=2026-01-01&createdAt[lt]=2026-02-01
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Tenant scope flows through `repo.aggregate(req, options)` — the kit's multi-tenant plugin handles type-coercion (string → ObjectId for mongokit `fieldType: 'objectId'`, UUID/text for sqlitekit, etc.). Arc itself stays out of the filter slot because it's DB-agnostic. Safety guards on the declaration: `requireFilters`, `requireDateRange { maxRangeDays }`, `maxGroups`. SWR cache + tag invalidation tie aggregations to CRUD writes. Every aggregation auto-exports as an MCP tool with the same permissions and filter validation.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
158
202
|
## Authentication
|
|
159
203
|
|
|
160
204
|
Discriminated union on `type`:
|
|
@@ -481,9 +481,20 @@ var BaseCrudController = class {
|
|
|
481
481
|
resourceName;
|
|
482
482
|
tenantField;
|
|
483
483
|
idField = "_id";
|
|
484
|
-
/**
|
|
484
|
+
/**
|
|
485
|
+
* Composable access control (ID filtering, policy checks, org scope, ownership).
|
|
486
|
+
*
|
|
487
|
+
* Not `readonly` since 2.15.0 — `configure()` rebuilds it when the host
|
|
488
|
+
* supplies tenant/idField/matchesFilter post-construction. Same model as
|
|
489
|
+
* `queryResolver` after `setQueryParser` shipped in 2.10.9.
|
|
490
|
+
*/
|
|
485
491
|
accessControl;
|
|
486
|
-
/**
|
|
492
|
+
/**
|
|
493
|
+
* Composable body sanitization (field permissions, system fields).
|
|
494
|
+
*
|
|
495
|
+
* Not `readonly` since 2.15.0 — `configure()` rebuilds it when the host
|
|
496
|
+
* supplies schemaOptions/onFieldWriteDenied post-construction.
|
|
497
|
+
*/
|
|
487
498
|
bodySanitizer;
|
|
488
499
|
/**
|
|
489
500
|
* Composable query resolution (parsing, pagination, sort, select/populate).
|
|
@@ -548,6 +559,94 @@ var BaseCrudController = class {
|
|
|
548
559
|
this.queryResolver.setParser(queryParser);
|
|
549
560
|
}
|
|
550
561
|
/**
|
|
562
|
+
* Apply resource-level options to a custom controller AFTER construction.
|
|
563
|
+
*
|
|
564
|
+
* Closes the pre-2.15 footgun where `defineResource({ controller, tenantField,
|
|
565
|
+
* schemaOptions, ... })` warned that the options were "dropped" because the
|
|
566
|
+
* user-supplied controller never received them. Hosts had to remember to
|
|
567
|
+
* forward each one through `super(repo, { ... })` — easy to miss, silently
|
|
568
|
+
* mis-scopes queries when missed.
|
|
569
|
+
*
|
|
570
|
+
* `defineResource()` now calls `controller.configure(resolvedOpts)` after
|
|
571
|
+
* `resolveOrAutoCreateController()` runs. Configure-aware controllers receive
|
|
572
|
+
* the resolved values; arc skips the dropped-options warn for them.
|
|
573
|
+
*
|
|
574
|
+
* Only the keys that affect cross-cutting state (tenant scope, schema/field
|
|
575
|
+
* rules, sort/limit policy, cache, write-denial policy) are honoured —
|
|
576
|
+
* `repository` / `resourceName` are constructor-only because they participate
|
|
577
|
+
* in mixin composition. Each known key rebuilds the affected sub-component
|
|
578
|
+
* (AccessControl / BodySanitizer / QueryResolver) so referentially-stable
|
|
579
|
+
* consumers don't see stale state.
|
|
580
|
+
*
|
|
581
|
+
* Idempotent: safe to call zero, one, or many times before first request;
|
|
582
|
+
* arc calls it exactly once.
|
|
583
|
+
*
|
|
584
|
+
* Type narrowed to `ControllerConfigurableOptions` — `resourceName` is
|
|
585
|
+
* construction-only and intentionally excluded so accidental "rename
|
|
586
|
+
* the resource at runtime" calls fail to typecheck.
|
|
587
|
+
*/
|
|
588
|
+
configure(options) {
|
|
589
|
+
let rebuildAccessControl = false;
|
|
590
|
+
let rebuildBodySanitizer = false;
|
|
591
|
+
let rebuildQueryResolver = false;
|
|
592
|
+
let bodySanitizerOnFieldWriteDenied;
|
|
593
|
+
let resolverDefaultSort;
|
|
594
|
+
if (options.tenantField !== void 0) {
|
|
595
|
+
this.tenantField = options.tenantField;
|
|
596
|
+
rebuildAccessControl = true;
|
|
597
|
+
rebuildQueryResolver = true;
|
|
598
|
+
}
|
|
599
|
+
if (options.idField !== void 0) {
|
|
600
|
+
this.idField = options.idField;
|
|
601
|
+
rebuildAccessControl = true;
|
|
602
|
+
}
|
|
603
|
+
if (options.matchesFilter !== void 0) {
|
|
604
|
+
this._matchesFilter = options.matchesFilter;
|
|
605
|
+
rebuildAccessControl = true;
|
|
606
|
+
}
|
|
607
|
+
if (options.schemaOptions !== void 0) {
|
|
608
|
+
this.schemaOptions = options.schemaOptions;
|
|
609
|
+
rebuildBodySanitizer = true;
|
|
610
|
+
rebuildQueryResolver = true;
|
|
611
|
+
}
|
|
612
|
+
if (options.onFieldWriteDenied !== void 0) {
|
|
613
|
+
bodySanitizerOnFieldWriteDenied = options.onFieldWriteDenied;
|
|
614
|
+
rebuildBodySanitizer = true;
|
|
615
|
+
}
|
|
616
|
+
if (options.queryParser !== void 0) this.setQueryParser(options.queryParser);
|
|
617
|
+
if (options.maxLimit !== void 0) {
|
|
618
|
+
this.maxLimit = options.maxLimit;
|
|
619
|
+
rebuildQueryResolver = true;
|
|
620
|
+
}
|
|
621
|
+
if (options.defaultLimit !== void 0) {
|
|
622
|
+
this.defaultLimit = options.defaultLimit;
|
|
623
|
+
rebuildQueryResolver = true;
|
|
624
|
+
}
|
|
625
|
+
if (options.defaultSort !== void 0) {
|
|
626
|
+
resolverDefaultSort = options.defaultSort;
|
|
627
|
+
rebuildQueryResolver = true;
|
|
628
|
+
}
|
|
629
|
+
if (options.cache !== void 0) this._cacheConfig = options.cache;
|
|
630
|
+
if (options.presetFields !== void 0) this._presetFields = options.presetFields;
|
|
631
|
+
if (rebuildAccessControl) this.accessControl = new AccessControl({
|
|
632
|
+
tenantField: this.tenantField,
|
|
633
|
+
idField: this.idField,
|
|
634
|
+
matchesFilter: this._matchesFilter
|
|
635
|
+
});
|
|
636
|
+
if (rebuildBodySanitizer) this.bodySanitizer = new BodySanitizer({
|
|
637
|
+
schemaOptions: this.schemaOptions,
|
|
638
|
+
onFieldWriteDenied: bodySanitizerOnFieldWriteDenied
|
|
639
|
+
});
|
|
640
|
+
if (rebuildQueryResolver) this.queryResolver = new QueryResolver({
|
|
641
|
+
queryParser: this.queryParser,
|
|
642
|
+
maxLimit: this.maxLimit,
|
|
643
|
+
defaultLimit: this.defaultLimit,
|
|
644
|
+
defaultSort: resolverDefaultSort,
|
|
645
|
+
schemaOptions: this.schemaOptions,
|
|
646
|
+
tenantField: this.tenantField
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
551
650
|
* Get the tenant field name if multi-tenant scoping is enabled.
|
|
552
651
|
* Returns `undefined` when `tenantField` is `false`.
|
|
553
652
|
*/
|
|
@@ -602,6 +701,7 @@ var BaseCrudController = class {
|
|
|
602
701
|
if (presetFields && typeof presetFields === "object") {
|
|
603
702
|
for (const [key, value] of Object.entries(presetFields)) if (value != null && out[key] == null) out[key] = value;
|
|
604
703
|
}
|
|
704
|
+
if (this.tenantField && out[this.tenantField] == null && scope && isElevated(scope)) out.bypassTenant = true;
|
|
605
705
|
const userId = getUserId(req.user);
|
|
606
706
|
if (userId) out.userId = userId;
|
|
607
707
|
if (req.user) out.user = req.user;
|
package/dist/auth/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Rt as AuthHelpers, zt as AuthPluginOptions } from "../index-
|
|
1
|
+
import { Rt as AuthHelpers, zt as AuthPluginOptions } from "../index-BswOSJCE.mjs";
|
|
2
2
|
import { c as PermissionCheck } from "../fields-COhcH3fk.mjs";
|
|
3
3
|
import { t as ExternalOpenApiPaths } from "../externalPaths-BD5nw6St.mjs";
|
|
4
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";
|
|
@@ -78,10 +78,8 @@ function adapterSupportsAggregate(repo) {
|
|
|
78
78
|
}
|
|
79
79
|
/** Compile to canonical `AggRequest` for `repo.aggregate()` at request time. */
|
|
80
80
|
function compileAggRequest(normalized, callerFilter, tenantOptions) {
|
|
81
|
-
const baseFilter = normalized.compiled.filter ?? {};
|
|
82
81
|
const filter = {
|
|
83
|
-
...
|
|
84
|
-
...baseFilter,
|
|
82
|
+
...normalized.compiled.filter ?? {},
|
|
85
83
|
...callerFilter
|
|
86
84
|
};
|
|
87
85
|
const executionHints = buildExecutionHints(normalized.base);
|
|
@@ -374,21 +372,6 @@ function assertFieldAllowed(context, ref, input) {
|
|
|
374
372
|
}
|
|
375
373
|
if (blockedFields.has(ref)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" references field "${ref}" in ${context}, but the field is blocked from aggregation (\`hidden: true\` or \`aggregable: false\` in schemaOptions.fieldRules). Aggregating hidden fields would leak cardinality information.`);
|
|
376
374
|
}
|
|
377
|
-
function extractTenantFilter(tenantOptions) {
|
|
378
|
-
const out = {};
|
|
379
|
-
const optionOnlyKeys = new Set([
|
|
380
|
-
"userId",
|
|
381
|
-
"user",
|
|
382
|
-
"session",
|
|
383
|
-
"requestId"
|
|
384
|
-
]);
|
|
385
|
-
for (const [key, value] of Object.entries(tenantOptions)) {
|
|
386
|
-
if (optionOnlyKeys.has(key)) continue;
|
|
387
|
-
if (value === void 0 || value === null) continue;
|
|
388
|
-
out[key] = value;
|
|
389
|
-
}
|
|
390
|
-
return out;
|
|
391
|
-
}
|
|
392
375
|
//#endregion
|
|
393
376
|
//#region src/core/aggregation/buildHandler.ts
|
|
394
377
|
/**
|
|
@@ -444,7 +427,7 @@ async function executeAggregation(normalized, deps, ctx) {
|
|
|
444
427
|
};
|
|
445
428
|
let result;
|
|
446
429
|
try {
|
|
447
|
-
result = await repo.aggregate(aggReq);
|
|
430
|
+
result = await repo.aggregate(aggReq, tenantOptions);
|
|
448
431
|
} catch (err) {
|
|
449
432
|
return mapAggregateError(err, aggregationName);
|
|
450
433
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { V as ResourceDefinition, ft as RouteSchemaOptions, rt as RateLimitConfig } from "../../index-
|
|
1
|
+
import { V as ResourceDefinition, ft as RouteSchemaOptions, rt as RateLimitConfig } from "../../index-BswOSJCE.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/cli/commands/describe.d.ts
|
|
4
4
|
interface DescribedResource {
|
package/dist/core/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { $t as SoftDeleteMixin, B as defineResource, Ct as AggregationConfig, Dt as AggregationMaterializedResult, Et as AggregationMaterializedContext, Ot as AggregationRateLimit, Qt as SoftDeleteExt, St as AggregationCacheConfig, Tt as AggregationIndexHint, V as ResourceDefinition, Zt as BaseController, _n as
|
|
2
|
-
import { A as MAX_SEARCH_LENGTH, C as DEFAULT_UPDATE_METHOD, D as HookPhase, E as HookOperation, M as MutationOperation, N as RESERVED_QUERY_PARAMS, O as MAX_FILTER_DEPTH, P as SYSTEM_FIELDS, S as DEFAULT_TENANT_FIELD, T as HOOK_PHASES, _ as CrudOperation, a as createRequestContext, b as DEFAULT_MAX_LIMIT, c as sendControllerResponse, d as getEntityQuery, f as defineResourceVariants, g as CRUD_OPERATIONS, h as defineAggregation, i as createFastifyHandler, j as MUTATION_OPERATIONS, k as MAX_REGEX_LENGTH, l as getEntityId, m as createPermissionMiddleware, n as isFieldReadable, o as getControllerContext, p as createCrudRouter, r as createCrudHandlers, s as getControllerScope, t as collectReadBlockedFields, u as getEntityIdField, v as DEFAULT_ID_FIELD, w as HOOK_OPERATIONS, x as DEFAULT_SORT, y as DEFAULT_LIMIT } from "../index-
|
|
3
|
-
export { AccessControl, AccessControlConfig, AggMeasureInput, AggMeasureShorthand, AggregationCacheConfig, AggregationConfig, AggregationDateRangeRequirement, AggregationIndexHint, AggregationMaterializedContext, AggregationMaterializedResult, AggregationRateLimit, AggregationsMap, BaseController, BaseControllerOptions, BaseCrudController, BodySanitizer, BodySanitizerConfig, BulkExt, BulkMixin, CRUD_OPERATIONS, CrudOperation, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, HookOperation, HookPhase, ListResult, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MutationOperation, QueryResolver, QueryResolverConfig, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, SlugExt, SlugMixin, SoftDeleteExt, SoftDeleteMixin, TreeExt, TreeMixin, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAggregation, defineResource, defineResourceVariants, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, sendControllerResponse };
|
|
1
|
+
import { $t as SoftDeleteMixin, B as defineResource, Ct as AggregationConfig, Dt as AggregationMaterializedResult, Et as AggregationMaterializedContext, Ot as AggregationRateLimit, Qt as SoftDeleteExt, St as AggregationCacheConfig, Tt as AggregationIndexHint, V as ResourceDefinition, Zt as BaseController, _n as ListResult, an as BulkMixin, bn as AccessControl, bt as AggMeasureInput, cn as ControllerConfigurableOptions, dn as QueryResolverConfig, en as TreeExt, in as BulkExt, kt as AggregationsMap, ln as ControllerConstructionOptions, nn as SlugExt, on as BaseControllerOptions, rn as SlugMixin, sn as BaseCrudController, tn as TreeMixin, un as QueryResolver, vn as BodySanitizer, wt as AggregationDateRangeRequirement, xn as AccessControlConfig, xt as AggMeasureShorthand, yn as BodySanitizerConfig } from "../index-BswOSJCE.mjs";
|
|
2
|
+
import { A as MAX_SEARCH_LENGTH, C as DEFAULT_UPDATE_METHOD, D as HookPhase, E as HookOperation, M as MutationOperation, N as RESERVED_QUERY_PARAMS, O as MAX_FILTER_DEPTH, P as SYSTEM_FIELDS, S as DEFAULT_TENANT_FIELD, T as HOOK_PHASES, _ as CrudOperation, a as createRequestContext, b as DEFAULT_MAX_LIMIT, c as sendControllerResponse, d as getEntityQuery, f as defineResourceVariants, g as CRUD_OPERATIONS, h as defineAggregation, i as createFastifyHandler, j as MUTATION_OPERATIONS, k as MAX_REGEX_LENGTH, l as getEntityId, m as createPermissionMiddleware, n as isFieldReadable, o as getControllerContext, p as createCrudRouter, r as createCrudHandlers, s as getControllerScope, t as collectReadBlockedFields, u as getEntityIdField, v as DEFAULT_ID_FIELD, w as HOOK_OPERATIONS, x as DEFAULT_SORT, y as DEFAULT_LIMIT } from "../index-BstGxcc3.mjs";
|
|
3
|
+
export { AccessControl, AccessControlConfig, AggMeasureInput, AggMeasureShorthand, AggregationCacheConfig, AggregationConfig, AggregationDateRangeRequirement, AggregationIndexHint, AggregationMaterializedContext, AggregationMaterializedResult, AggregationRateLimit, AggregationsMap, BaseController, BaseControllerOptions, BaseCrudController, BodySanitizer, BodySanitizerConfig, BulkExt, BulkMixin, CRUD_OPERATIONS, ControllerConfigurableOptions, ControllerConstructionOptions, CrudOperation, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, HookOperation, HookPhase, ListResult, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MutationOperation, QueryResolver, QueryResolverConfig, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, SlugExt, SlugMixin, SoftDeleteExt, SoftDeleteMixin, TreeExt, TreeMixin, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAggregation, defineResource, defineResourceVariants, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, sendControllerResponse };
|
package/dist/core/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as DEFAULT_SORT, c as HOOK_OPERATIONS, d as MAX_REGEX_LENGTH, f as MAX_SEARCH_LENGTH, h as SYSTEM_FIELDS, i as DEFAULT_MAX_LIMIT, l as HOOK_PHASES, m as RESERVED_QUERY_PARAMS, n as DEFAULT_ID_FIELD, o as DEFAULT_TENANT_FIELD, p as MUTATION_OPERATIONS, r as DEFAULT_LIMIT, s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS, u as MAX_FILTER_DEPTH } from "../constants-Cxde4rpC.mjs";
|
|
2
|
-
import { a as BulkMixin, c as collectReadBlockedFields, d as AccessControl, i as SlugMixin, l as isFieldReadable, n as TreeMixin, o as BaseCrudController, r as SoftDeleteMixin, s as QueryResolver, t as BaseController, u as BodySanitizer } from "../BaseController-
|
|
2
|
+
import { a as BulkMixin, c as collectReadBlockedFields, d as AccessControl, i as SlugMixin, l as isFieldReadable, n as TreeMixin, o as BaseCrudController, r as SoftDeleteMixin, s as QueryResolver, t as BaseController, u as BodySanitizer } from "../BaseController-dx3m2J8V.mjs";
|
|
3
3
|
import { _ as getControllerContext, g as createRequestContext, h as createFastifyHandler, m as createCrudHandlers, v as getControllerScope, y as sendControllerResponse } from "../routerShared-DrOa-26E.mjs";
|
|
4
|
-
import { a as defineResource, c as createPermissionMiddleware, i as defineResourceVariants, l as defineAggregation, n as getEntityIdField, o as ResourceDefinition, r as getEntityQuery, s as createCrudRouter, t as getEntityId } from "../core-
|
|
4
|
+
import { a as defineResource, c as createPermissionMiddleware, i as defineResourceVariants, l as defineAggregation, n as getEntityIdField, o as ResourceDefinition, r as getEntityQuery, s as createCrudRouter, t as getEntityId } from "../core-CvmOqEms.mjs";
|
|
5
5
|
export { AccessControl, BaseController, BaseCrudController, BodySanitizer, BulkMixin, CRUD_OPERATIONS, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, QueryResolver, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, SlugMixin, SoftDeleteMixin, TreeMixin, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAggregation, defineResource, defineResourceVariants, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, sendControllerResponse };
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-Cxde4rpC.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
3
|
import { A as assertValidConfig, l as getDefaultCrudSchemas } from "./utils-_h9B3c57.mjs";
|
|
4
|
-
import { t as BaseController } from "./BaseController-
|
|
4
|
+
import { t as BaseController } from "./BaseController-dx3m2J8V.mjs";
|
|
5
5
|
import { t as applyPresets } from "./presets-BbkjdPeH.mjs";
|
|
6
6
|
import { n as convertRouteSchema, t as convertOpenApiSchemas } from "./schemaConverter-De34B1ZG.mjs";
|
|
7
7
|
import { t as hasEvents } from "./typeGuards-BzkXkvVv.mjs";
|
|
8
|
-
import { b as buildRequestScopeProjection, c as buildPreHandlerChain, d as resolveRoutePreHandlers, f as resolveRouterPluginMw, h as createFastifyHandler, i as buildAuthMiddleware, l as buildRateLimitConfig, m as createCrudHandlers, o as buildCrudPermissionMw, p as selectPluginMw, r as buildArcDecorator, s as buildPipelineHandler, u as resolvePipelineSteps } from "./routerShared-DrOa-26E.mjs";
|
|
8
|
+
import { b as buildRequestScopeProjection, c as buildPreHandlerChain, d as resolveRoutePreHandlers, f as resolveRouterPluginMw, g as createRequestContext, h as createFastifyHandler, i as buildAuthMiddleware, l as buildRateLimitConfig, m as createCrudHandlers, o as buildCrudPermissionMw, p as selectPluginMw, r as buildArcDecorator, s as buildPipelineHandler, u as resolvePipelineSteps } from "./routerShared-DrOa-26E.mjs";
|
|
9
9
|
import { t as resolveActionPermission } from "./actionPermissions-CyUkQu6O.mjs";
|
|
10
10
|
//#region src/core/aggregation/defineAggregation.ts
|
|
11
11
|
/**
|
|
@@ -283,6 +283,7 @@ function resolveOrAutoCreateController(resolvedConfig, adapter, repository, hasC
|
|
|
283
283
|
const userController = resolvedConfig.controller;
|
|
284
284
|
if (userController) {
|
|
285
285
|
threadQueryParser(userController, resolvedConfig);
|
|
286
|
+
threadConfigureLifecycle(userController, resolvedConfig, adapter);
|
|
286
287
|
warnOnDroppedAuthorOptions(resolvedConfig);
|
|
287
288
|
warnOnDroppedPresetOptions(resolvedConfig);
|
|
288
289
|
return userController;
|
|
@@ -291,11 +292,57 @@ function resolveOrAutoCreateController(resolvedConfig, adapter, repository, hasC
|
|
|
291
292
|
return buildBaseController(resolvedConfig, adapter, repository);
|
|
292
293
|
}
|
|
293
294
|
/**
|
|
295
|
+
* Forward resource-level options into a user-supplied controller via
|
|
296
|
+
* the duck-typed `configure(opts)` lifecycle hook. Closes the pre-2.15
|
|
297
|
+
* gap where `tenantField` / `schemaOptions` / `idField` / `defaultSort`
|
|
298
|
+
* / `cache` / `onFieldWriteDenied` had no path into a host controller
|
|
299
|
+
* unless the host remembered to forward them through `super(repo,
|
|
300
|
+
* { ... })`. `BaseController` / `BaseCrudController` ship `configure()`;
|
|
301
|
+
* custom controllers can opt in by adding the method.
|
|
302
|
+
*
|
|
303
|
+
* **Gate by `_declaredKeys`**: only forward keys the user literally
|
|
304
|
+
* passed at the resource level. Without this gate, arc-inferred values
|
|
305
|
+
* (e.g. `inferTenantFieldFromAdapter` setting `tenantField: false`
|
|
306
|
+
* because the model's organizationId path doesn't exist) would clobber
|
|
307
|
+
* the matching value the host already set in the controller's
|
|
308
|
+
* constructor (e.g. `new BaseController(repo, { tenantField:
|
|
309
|
+
* "companyId" })`). Resource-level explicit values still win — the
|
|
310
|
+
* snapshot captures what the user typed before any inference runs.
|
|
311
|
+
*/
|
|
312
|
+
function threadConfigureLifecycle(controller, resolvedConfig, adapter) {
|
|
313
|
+
const ctrl = controller;
|
|
314
|
+
if (typeof ctrl.configure !== "function") return;
|
|
315
|
+
const declared = resolvedConfig._declaredKeys;
|
|
316
|
+
const wasDeclared = (key) => declared ? declared.has(key) : true;
|
|
317
|
+
const opts = {};
|
|
318
|
+
if (resolvedConfig.tenantField !== void 0 && wasDeclared("tenantField")) opts.tenantField = resolvedConfig.tenantField;
|
|
319
|
+
if (resolvedConfig.schemaOptions !== void 0 && wasDeclared("schemaOptions")) opts.schemaOptions = resolvedConfig.schemaOptions;
|
|
320
|
+
if (resolvedConfig.idField !== void 0 && wasDeclared("idField")) opts.idField = resolvedConfig.idField;
|
|
321
|
+
if (resolvedConfig.defaultSort !== void 0 && wasDeclared("defaultSort")) opts.defaultSort = resolvedConfig.defaultSort;
|
|
322
|
+
if (resolvedConfig.cache !== void 0 && wasDeclared("cache")) opts.cache = resolvedConfig.cache;
|
|
323
|
+
if (resolvedConfig.onFieldWriteDenied !== void 0 && wasDeclared("onFieldWriteDenied")) opts.onFieldWriteDenied = resolvedConfig.onFieldWriteDenied;
|
|
324
|
+
if (resolvedConfig.queryParser !== void 0 && wasDeclared("queryParser")) opts.queryParser = resolvedConfig.queryParser;
|
|
325
|
+
if (adapter?.matchesFilter !== void 0) opts.matchesFilter = adapter.matchesFilter;
|
|
326
|
+
if (resolvedConfig._controllerOptions) opts.presetFields = {
|
|
327
|
+
slugField: resolvedConfig._controllerOptions.slugField,
|
|
328
|
+
parentField: resolvedConfig._controllerOptions.parentField
|
|
329
|
+
};
|
|
330
|
+
if (Object.keys(opts).length === 0) return;
|
|
331
|
+
ctrl.configure(opts);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
294
334
|
* Forward a resource-level `queryParser` into a user-supplied
|
|
295
335
|
* controller via duck-typed `setQueryParser`. Without this the
|
|
296
336
|
* controller's internal default would silently override the
|
|
297
337
|
* resource's parser, drifting `[contains]` / `[like]` semantics
|
|
298
338
|
* away from what the OpenAPI schema advertises.
|
|
339
|
+
*
|
|
340
|
+
* **Fail-loud (2.15.0):** older versions warned and continued —
|
|
341
|
+
* letting the resource register with a silently-shadowed parser. The
|
|
342
|
+
* "ship and pray they read the log" path produced 90-minute "why
|
|
343
|
+
* doesn't `[contains]` work" debugs in production. Now throws at
|
|
344
|
+
* registration so the misconfig surfaces immediately, with the same
|
|
345
|
+
* fix-it message in the error.
|
|
299
346
|
*/
|
|
300
347
|
function threadQueryParser(controller, resolvedConfig) {
|
|
301
348
|
if (!resolvedConfig.queryParser) return;
|
|
@@ -304,7 +351,7 @@ function threadQueryParser(controller, resolvedConfig) {
|
|
|
304
351
|
ctrl.setQueryParser(resolvedConfig.queryParser);
|
|
305
352
|
return;
|
|
306
353
|
}
|
|
307
|
-
|
|
354
|
+
throw new Error(`Resource "${resolvedConfig.name}" declares a custom \`queryParser\` but its controller does not expose \`setQueryParser(qp)\`. The parser would be silently dropped, drifting \`[contains]\` / \`[like]\` semantics away from the OpenAPI schema. Extend \`BaseController\` / \`BaseCrudController\` (both implement \`setQueryParser\`) OR add a \`setQueryParser(qp)\` method to your custom controller that actually wires the parser into the controller's query resolution path. (arc 2.15.0 hardened this to a registration-time throw — pre-2.15 it was a warn.)`);
|
|
308
355
|
}
|
|
309
356
|
/**
|
|
310
357
|
* Warn when the user supplies their own controller AND declares
|
|
@@ -314,15 +361,18 @@ function threadQueryParser(controller, resolvedConfig) {
|
|
|
314
361
|
* fix.
|
|
315
362
|
*/
|
|
316
363
|
function warnOnDroppedAuthorOptions(resolvedConfig) {
|
|
364
|
+
if (typeof resolvedConfig.controller?.configure === "function") return;
|
|
365
|
+
const declared = resolvedConfig._declaredKeys;
|
|
366
|
+
const isDeclared = (key) => declared ? declared.has(key) : true;
|
|
317
367
|
const dropped = [];
|
|
318
|
-
if (resolvedConfig.tenantField !== void 0) dropped.push("tenantField");
|
|
319
|
-
if (resolvedConfig.schemaOptions !== void 0 && Object.keys(resolvedConfig.schemaOptions).length > 0) dropped.push("schemaOptions");
|
|
320
|
-
if (resolvedConfig.idField !== void 0) dropped.push("idField");
|
|
321
|
-
if (resolvedConfig.defaultSort !== void 0) dropped.push("defaultSort");
|
|
322
|
-
if (resolvedConfig.cache !== void 0) dropped.push("cache");
|
|
323
|
-
if (resolvedConfig.onFieldWriteDenied !== void 0) dropped.push("onFieldWriteDenied");
|
|
368
|
+
if (resolvedConfig.tenantField !== void 0 && isDeclared("tenantField")) dropped.push("tenantField");
|
|
369
|
+
if (resolvedConfig.schemaOptions !== void 0 && Object.keys(resolvedConfig.schemaOptions).length > 0 && isDeclared("schemaOptions")) dropped.push("schemaOptions");
|
|
370
|
+
if (resolvedConfig.idField !== void 0 && isDeclared("idField")) dropped.push("idField");
|
|
371
|
+
if (resolvedConfig.defaultSort !== void 0 && isDeclared("defaultSort")) dropped.push("defaultSort");
|
|
372
|
+
if (resolvedConfig.cache !== void 0 && isDeclared("cache")) dropped.push("cache");
|
|
373
|
+
if (resolvedConfig.onFieldWriteDenied !== void 0 && isDeclared("onFieldWriteDenied")) dropped.push("onFieldWriteDenied");
|
|
324
374
|
if (dropped.length === 0) return;
|
|
325
|
-
arcLog("defineResource").warn(`Resource "${resolvedConfig.name}" declares a custom controller AND resource-level option(s) [${dropped.join(", ")}]. Arc only threads these when it auto-builds the controller — when you pass your own, they are dropped silently and the controller falls back to its own defaults (e.g. tenantField → 'organizationId').
|
|
375
|
+
arcLog("defineResource").warn(`Resource "${resolvedConfig.name}" declares a custom controller AND resource-level option(s) [${dropped.join(", ")}]. Arc only threads these when it auto-builds the controller — when you pass your own, they are dropped silently and the controller falls back to its own defaults (e.g. tenantField → 'organizationId'). Either implement \`configure(opts)\` on the controller (arc 2.15+ canonical) or forward them via \`super(repo, { ... })\`. Same root cause as the \`queryParser\` warn above.`);
|
|
326
376
|
}
|
|
327
377
|
/**
|
|
328
378
|
* Warn when a preset injected `_controllerOptions` (slugLookup,
|
|
@@ -636,8 +686,10 @@ function stripSystemManagedFromBodyRequired(schemas, schemaOptions) {
|
|
|
636
686
|
*/
|
|
637
687
|
function applyPresetsAndAutoInject(config) {
|
|
638
688
|
const originalPresets = (config.presets ?? []).map((p) => typeof p === "string" ? p : p.name);
|
|
689
|
+
const declaredKeys = new Set(Object.keys(config).filter((k) => config[k] !== void 0));
|
|
639
690
|
const resolvedConfig = config.presets?.length ? applyPresets(config, config.presets) : { ...config };
|
|
640
691
|
resolvedConfig._appliedPresets = originalPresets;
|
|
692
|
+
resolvedConfig._declaredKeys = declaredKeys;
|
|
641
693
|
inferTenantFieldFromAdapter(resolvedConfig);
|
|
642
694
|
resolvedConfig.schemaOptions = autoInjectTenantFieldRules(resolvedConfig.schemaOptions, resolvedConfig.tenantField);
|
|
643
695
|
return resolvedConfig;
|
|
@@ -788,6 +840,7 @@ function normalizeListQuerySchema(listQuerySchema) {
|
|
|
788
840
|
for (const key of Object.keys(normalizedProps)) normalizedProps[key] = NORMALIZED_PROPS[key] ?? {};
|
|
789
841
|
}
|
|
790
842
|
return {
|
|
843
|
+
type: "object",
|
|
791
844
|
...listQuerySchema,
|
|
792
845
|
...normalizedProps ? { properties: normalizedProps } : {},
|
|
793
846
|
additionalProperties: listQuerySchema.additionalProperties ?? true
|
|
@@ -945,10 +998,13 @@ function buildResourcePlugin(resource) {
|
|
|
945
998
|
});
|
|
946
999
|
}
|
|
947
1000
|
if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
|
|
948
|
-
const { createAggregationRouter } = await import("./createAggregationRouter-
|
|
1001
|
+
const { createAggregationRouter } = await import("./createAggregationRouter-B0bPDf5b.mjs");
|
|
949
1002
|
const repoForAgg = resource.controller?.repository;
|
|
950
1003
|
const buildOptions = (req) => {
|
|
951
|
-
|
|
1004
|
+
const ctrl = resource.controller;
|
|
1005
|
+
if (!ctrl?.tenantRepoOptions) return {};
|
|
1006
|
+
const ctx = createRequestContext(req);
|
|
1007
|
+
return ctrl.tenantRepoOptions(ctx);
|
|
952
1008
|
};
|
|
953
1009
|
createAggregationRouter(typedInstance, {
|
|
954
1010
|
tag: resource.tag,
|
|
@@ -959,7 +1015,8 @@ function buildResourcePlugin(resource) {
|
|
|
959
1015
|
permissions: resource.permissions,
|
|
960
1016
|
routeGuards: resource.routeGuards,
|
|
961
1017
|
repository: repoForAgg,
|
|
962
|
-
buildOptions
|
|
1018
|
+
buildOptions,
|
|
1019
|
+
middlewares: resource.middlewares?.aggregations
|
|
963
1020
|
});
|
|
964
1021
|
}
|
|
965
1022
|
if (resource.events && Object.keys(resource.events).length > 0) typedInstance.log?.debug?.(`Resource '${resource.name}' defined ${Object.keys(resource.events).length} events`);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { f as createError, l as UnauthorizedError, r as ForbiddenError } from "./errors-j4aJm1Wg.mjs";
|
|
2
2
|
import { c as buildPreHandlerChain, f as resolveRouterPluginMw, i as buildAuthMiddleware, l as buildRateLimitConfig, p as selectPluginMw, r as buildArcDecorator } from "./routerShared-DrOa-26E.mjs";
|
|
3
|
-
import { r as validateAggregations, t as buildAggregationHandler } from "./buildHandler-
|
|
3
|
+
import { r as validateAggregations, t as buildAggregationHandler } from "./buildHandler-CcFOpJLh.mjs";
|
|
4
4
|
//#region src/core/aggregation/createAggregationRouter.ts
|
|
5
5
|
/**
|
|
6
6
|
* Register one Fastify route per aggregation. No-op when the map is
|
|
7
7
|
* empty — same convention `createActionRouter` follows.
|
|
8
8
|
*/
|
|
9
9
|
function createAggregationRouter(fastify, config) {
|
|
10
|
-
const { tag, resourceName, aggregations, fields: fieldPermissions, schemaOptions, permissions: resourcePermissions, routeGuards = [], repository, buildOptions } = config;
|
|
10
|
+
const { tag, resourceName, aggregations, fields: fieldPermissions, schemaOptions, permissions: resourcePermissions, routeGuards = [], repository, buildOptions, middlewares = [] } = config;
|
|
11
11
|
if (!aggregations || Object.keys(aggregations).length === 0) return;
|
|
12
12
|
const normalized = validateAggregations(resourceName, aggregations, schemaOptions);
|
|
13
13
|
const arcDecorator = buildArcDecorator({
|
|
@@ -23,7 +23,8 @@ function createAggregationRouter(fastify, config) {
|
|
|
23
23
|
arcDecorator,
|
|
24
24
|
routeGuards,
|
|
25
25
|
repository,
|
|
26
|
-
buildOptions
|
|
26
|
+
buildOptions,
|
|
27
|
+
middlewares
|
|
27
28
|
});
|
|
28
29
|
fastify.log?.debug?.({
|
|
29
30
|
aggregations: normalized.map((a) => a.name),
|
|
@@ -31,7 +32,7 @@ function createAggregationRouter(fastify, config) {
|
|
|
31
32
|
}, `[createAggregationRouter] registered ${normalized.length} aggregation route(s)`);
|
|
32
33
|
}
|
|
33
34
|
function registerOne(fastify, normalized, ctx) {
|
|
34
|
-
const { tag, arcDecorator, routeGuards, repository, buildOptions } = ctx;
|
|
35
|
+
const { tag, arcDecorator, routeGuards, repository, buildOptions, middlewares } = ctx;
|
|
35
36
|
const { name } = normalized;
|
|
36
37
|
const config = normalized.base;
|
|
37
38
|
const authMw = buildAuthMiddleware(fastify, config.permissions);
|
|
@@ -50,7 +51,8 @@ function registerOne(fastify, normalized, ctx) {
|
|
|
50
51
|
authMw,
|
|
51
52
|
permissionMw,
|
|
52
53
|
pluginMw: selectPluginMw("GET", resolveRouterPluginMw(fastify, false)),
|
|
53
|
-
routeGuards
|
|
54
|
+
routeGuards,
|
|
55
|
+
customMws: middlewares
|
|
54
56
|
});
|
|
55
57
|
const rateLimitConfig = buildRateLimitConfig(config.rateLimit ? {
|
|
56
58
|
max: config.rateLimit.max,
|
|
@@ -230,8 +230,9 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules) {
|
|
|
230
230
|
trackPlugin("arc-request-id");
|
|
231
231
|
}
|
|
232
232
|
if (config.arcPlugins?.health !== false) {
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
const healthOpts = typeof config.arcPlugins?.health === "object" && config.arcPlugins.health !== null ? config.arcPlugins.health : {};
|
|
234
|
+
await fastify.register(healthPlugin, healthOpts);
|
|
235
|
+
trackPlugin("arc-health", healthOpts);
|
|
235
236
|
}
|
|
236
237
|
if (config.arcPlugins?.gracefulShutdown !== false) {
|
|
237
238
|
await fastify.register(gracefulShutdownPlugin);
|
|
@@ -707,9 +708,65 @@ async function registerUtilityPlugins(fastify, config) {
|
|
|
707
708
|
*/
|
|
708
709
|
var createApp_exports = /* @__PURE__ */ __exportAll({
|
|
709
710
|
ArcFactory: () => ArcFactory,
|
|
710
|
-
|
|
711
|
+
DEFAULT_LOGGER_REDACT_PATHS: () => DEFAULT_LOGGER_REDACT_PATHS,
|
|
712
|
+
createApp: () => createApp,
|
|
713
|
+
resolveLoggerConfig: () => resolveLoggerConfig
|
|
711
714
|
});
|
|
712
715
|
const MEMORY_STORE_NAMES = new Set(["memory", "memory-cache"]);
|
|
716
|
+
/**
|
|
717
|
+
* Default redact paths layered into Fastify's pino logger when the host
|
|
718
|
+
* doesn't supply a `logger.redact` of their own. Covers the common token /
|
|
719
|
+
* cookie / password leak surfaces — `Authorization` and `Cookie` headers
|
|
720
|
+
* (and their case-variants because Node lower-cases incoming headers but
|
|
721
|
+
* outgoing logs may carry either), API-key headers, and any nested
|
|
722
|
+
* `password` / `token` / `secret` / `apiKey` fields anywhere in the log
|
|
723
|
+
* tree.
|
|
724
|
+
*
|
|
725
|
+
* Hosts that need NO redaction (test fixtures, security audits) opt out
|
|
726
|
+
* with `logger: { redact: [] }`. Hosts that need MORE redaction supply
|
|
727
|
+
* their own `redact` and arc steps aside — this default never overrides
|
|
728
|
+
* an explicit setting. (2.15.1)
|
|
729
|
+
*/
|
|
730
|
+
const DEFAULT_LOGGER_REDACT_PATHS = [
|
|
731
|
+
"req.headers.authorization",
|
|
732
|
+
"req.headers[\"x-api-key\"]",
|
|
733
|
+
"req.headers[\"x-internal-api-key\"]",
|
|
734
|
+
"req.headers.cookie",
|
|
735
|
+
"req.headers[\"set-cookie\"]",
|
|
736
|
+
"res.headers[\"set-cookie\"]",
|
|
737
|
+
"*.password",
|
|
738
|
+
"*.passwordHash",
|
|
739
|
+
"*.token",
|
|
740
|
+
"*.accessToken",
|
|
741
|
+
"*.refreshToken",
|
|
742
|
+
"*.secret",
|
|
743
|
+
"*.apiKey"
|
|
744
|
+
];
|
|
745
|
+
/**
|
|
746
|
+
* Resolve the host's `logger` option, layering safe redact defaults
|
|
747
|
+
* when the host hasn't supplied any. Returns the value Fastify expects
|
|
748
|
+
* for its `logger` server option.
|
|
749
|
+
*
|
|
750
|
+
* Three branches:
|
|
751
|
+
* - `logger === false` → pass through (no logger).
|
|
752
|
+
* - `logger === undefined` → enable with default redact paths.
|
|
753
|
+
* - `logger === true` → enable with default redact paths.
|
|
754
|
+
* - `logger` is an object → respect explicit `redact` (host wins);
|
|
755
|
+
* otherwise inject defaults.
|
|
756
|
+
*/
|
|
757
|
+
function resolveLoggerConfig(logger) {
|
|
758
|
+
if (logger === false) return false;
|
|
759
|
+
if (logger === true || logger === void 0) return { redact: [...DEFAULT_LOGGER_REDACT_PATHS] };
|
|
760
|
+
if (typeof logger === "object" && logger !== null) {
|
|
761
|
+
const objLogger = logger;
|
|
762
|
+
if (objLogger.redact !== void 0) return logger;
|
|
763
|
+
return {
|
|
764
|
+
...objLogger,
|
|
765
|
+
redact: [...DEFAULT_LOGGER_REDACT_PATHS]
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
return logger;
|
|
769
|
+
}
|
|
713
770
|
function validateAuthOptions(options) {
|
|
714
771
|
const authConfig = options.auth;
|
|
715
772
|
if (authConfig === false || !authConfig) return;
|
|
@@ -766,14 +823,17 @@ async function createApp(options) {
|
|
|
766
823
|
...options
|
|
767
824
|
};
|
|
768
825
|
const fastify = Fastify({
|
|
769
|
-
logger: config.logger
|
|
826
|
+
logger: resolveLoggerConfig(config.logger),
|
|
770
827
|
trustProxy: config.trustProxy ?? false,
|
|
771
828
|
pluginTimeout: config.pluginTimeout ?? 1e4,
|
|
829
|
+
...config.bodyLimit !== void 0 ? { bodyLimit: config.bodyLimit } : {},
|
|
830
|
+
allowErrorHandlerOverride: true,
|
|
772
831
|
routerOptions: { querystringParser: (str) => qs.parse(str) },
|
|
773
832
|
ajv: { customOptions: {
|
|
774
833
|
coerceTypes: true,
|
|
775
834
|
useDefaults: true,
|
|
776
835
|
removeAdditional: false,
|
|
836
|
+
strictTypes: false,
|
|
777
837
|
keywords: ["example", ...config.ajv?.keywords ?? []]
|
|
778
838
|
} }
|
|
779
839
|
});
|
package/dist/docs/index.d.mts
CHANGED
package/dist/factory/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as CustomPluginAuthOption, c as RawBodyOptions, d as ResourceLike, f as ResourceModule, i as CustomAuthenticatorOption, l as UnderPressureOptions, n as BetterAuthOption, o as JwtAuthOption, p as loadResources, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as LoadResourcesOptions } from "../types-
|
|
2
|
-
import { FastifyInstance } from "fastify";
|
|
1
|
+
import { a as CustomPluginAuthOption, c as RawBodyOptions, d as ResourceLike, f as ResourceModule, i as CustomAuthenticatorOption, l as UnderPressureOptions, n as BetterAuthOption, o as JwtAuthOption, p as loadResources, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as LoadResourcesOptions } from "../types-DrBaUwyV.mjs";
|
|
2
|
+
import { FastifyInstance, FastifyServerOptions } from "fastify";
|
|
3
3
|
|
|
4
4
|
//#region src/factory/createApp.d.ts
|
|
5
5
|
/**
|
package/dist/factory/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as edgePreset, c as testingPreset, i as developmentPreset, n as createApp, o as getPreset, s as productionPreset, t as ArcFactory } from "../createApp-
|
|
1
|
+
import { a as edgePreset, c as testingPreset, i as developmentPreset, n as createApp, o as getPreset, s as productionPreset, t as ArcFactory } from "../createApp-PFegs47-.mjs";
|
|
2
2
|
import { t as loadResources } from "../loadResources-DBMQg_Aj.mjs";
|
|
3
3
|
//#region src/factory/edge.ts
|
|
4
4
|
/**
|
package/dist/hooks/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { An as
|
|
1
|
+
import { An as afterCreate, Cn as HookContext, Dn as HookRegistration, En as HookPhase, Fn as beforeUpdate, In as createHookSystem, Ln as defineHook, Mn as afterUpdate, Nn as beforeCreate, On as HookSystem, Pn as beforeDelete, Sn as DefineHookOptions, Tn as HookOperation, jn as afterDelete, kn as HookSystemOptions, wn as HookHandler } from "../index-BswOSJCE.mjs";
|
|
2
2
|
export { type DefineHookOptions, type HookContext, type HookHandler, type HookOperation, type HookPhase, type HookRegistration, HookSystem, type HookSystemOptions, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as RequestContext, Ct as AggregationConfig, F as FastifyWithDecorators, K as ArcFieldRule, L as RequestWithExtras, T as CrudRouterOptions, V as ResourceDefinition, Wt as AnyRecord, _t as IControllerResponse, at as ResourceConfig, ft as RouteSchemaOptions, gt as IController, q as CrudController, vt as IRequestContext } from "./index-
|
|
1
|
+
import { C as RequestContext, Ct as AggregationConfig, F as FastifyWithDecorators, K as ArcFieldRule, L as RequestWithExtras, T as CrudRouterOptions, V as ResourceDefinition, Wt as AnyRecord, _t as IControllerResponse, at as ResourceConfig, ft as RouteSchemaOptions, gt as IController, q as CrudController, vt as IRequestContext } from "./index-BswOSJCE.mjs";
|
|
2
2
|
import { i as RequestScope } from "./types-CTYvcwHe.mjs";
|
|
3
3
|
import { c as PermissionCheck } from "./fields-COhcH3fk.mjs";
|
|
4
4
|
import { FastifyReply, FastifyRequest, RouteHandlerMethod } from "fastify";
|