@entifix/service-shell 0.7.2 → 0.9.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 CHANGED
@@ -16,17 +16,17 @@ pnpm add @opentelemetry/api @effect/opentelemetry @opentelemetry/sdk-trace-base
16
16
 
17
17
  ## Entry points
18
18
 
19
- | Import | What it holds |
20
- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
21
- | `@entifix/service-shell` | What a route set is written against: `makeEntityRoutes`, `requirePrincipal`, `requirePermission`, `requireOrganization`, `requireServiceCrossing`, `requireSharedToken`, `entityMetadataRoute`, health and SSE routes |
22
- | `@entifix/service-shell/runtime` | Running a service: `makeService`, `makeServerLayer`, `loadRemoteConfiguration`. Needs `@effect/platform-node` |
23
- | `@entifix/service-shell/observability` | `makeObservabilityLayer` and the OpenTelemetry wiring. Needs `@effect/opentelemetry` and the OpenTelemetry SDK packages |
24
- | `@entifix/service-shell/testing` | `serveTestService`, a service on an ephemeral port for a spec |
19
+ | Import | What it holds |
20
+ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
21
+ | `@entifix/service-shell` | What a route set is written against: `makeEntityRoutes`, `makeEntityFacetRoutes` (`GET <entity>/$facets`, over the host's `EntityFacetCounterFactoryTag`), `requirePrincipal`, `requirePermission`, `requireOrganization`, `requireServiceCrossing`, `requireSharedToken`, `entityMetadataRoute`, health and SSE routes |
22
+ | `@entifix/service-shell/runtime` | Running a service: `makeService`, `makeServerLayer`, `loadRemoteConfiguration`. Needs `@effect/platform-node` |
23
+ | `@entifix/service-shell/observability` | `makeObservabilityLayer` and the OpenTelemetry wiring. Needs `@effect/opentelemetry` and the OpenTelemetry SDK packages |
24
+ | `@entifix/service-shell/testing` | `serveTestService`, a service on an ephemeral port for a spec |
25
25
 
26
26
  The runtime and observability halves are optional peers: a package that only declares routes installs neither. The configuration service's URL and credential, and the header and secret that gate `GET /api/$service`, are values the host passes — `loadRemoteConfiguration({ url, headers })` and `makeService({ description: { header, token } })`.
27
27
 
28
28
  ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
29
- | `@entifix/service-shell` | `makeService`, `makeServerLayer`, `makeEntityRoutes`, `requirePrincipal`, `requirePermission`, `requireOrganization`, `entityMetadataRoute`, `loadRemoteConfiguration`, `makeObservabilityLayer`, `serveTestService` |
29
+ | `@entifix/service-shell` | `makeService`, `makeServerLayer`, `makeEntityRoutes`, `makeEntityFacetRoutes`, `requirePrincipal`, `requirePermission`, `requireOrganization`, `entityMetadataRoute`, `loadRemoteConfiguration`, `makeObservabilityLayer`, `serveTestService` |
30
30
 
31
31
  ---
32
32
 
@@ -29,5 +29,5 @@ import { type RequestPrincipal } from './require-principal.js';
29
29
  * the other path honours a caller-named organization only *after* the caller has
30
30
  * proved it is the fleet.
31
31
  */
32
- export declare const requireOrganization: (permission: Permission) => <A, E, R>(use: (organizationId: string, principal: RequestPrincipal) => Effect.Effect<A, E, R>) => Effect.Effect<HttpServerResponse.HttpServerResponse | A, import("effect/http/HttpBody").HttpBodyError | E, import("@entifix/authz").PolicyDecisionTag | import("@entifix/business").TokenServiceTag | import("effect/http/HttpServerRequest").HttpServerRequest | R>;
32
+ export declare const requireOrganization: (permission: Permission) => <A, E, R>(use: (organizationId: string, principal: RequestPrincipal) => Effect.Effect<A, E, R>) => Effect.Effect<HttpServerResponse.HttpServerResponse | A, import("effect/http/HttpBody").HttpBodyError | E, import("@entifix/business").TokenServiceTag | import("@entifix/authz").PolicyDecisionTag | import("effect/http/HttpServerRequest").HttpServerRequest | R>;
33
33
  //# sourceMappingURL=require-organization.d.ts.map
@@ -66,5 +66,5 @@ export declare const requirePrincipal: <A, E, R>(use: (principal: RequestPrincip
66
66
  * **This is the authorization boundary.** Filtering a nav menu or hiding a page
67
67
  * is presentation; a route that skips this guard is open regardless.
68
68
  */
69
- export declare const requirePermission: (permission: Permission) => <A, E, R>(use: (principal: RequestPrincipal) => Effect.Effect<A, E, R>) => Effect.Effect<HttpServerResponse.HttpServerResponse | A, import("effect/http/HttpBody").HttpBodyError | E, PolicyDecisionTag | TokenServiceTag | HttpServerRequest.HttpServerRequest | R>;
69
+ export declare const requirePermission: (permission: Permission) => <A, E, R>(use: (principal: RequestPrincipal) => Effect.Effect<A, E, R>) => Effect.Effect<HttpServerResponse.HttpServerResponse | A, import("effect/http/HttpBody").HttpBodyError | E, TokenServiceTag | PolicyDecisionTag | HttpServerRequest.HttpServerRequest | R>;
70
70
  //# sourceMappingURL=require-principal.d.ts.map
@@ -0,0 +1,35 @@
1
+ import { EntityFacetCounterFactoryTag } from '@entifix/business';
2
+ import { type Entity, type EntityConstructor } from '@entifix/core';
3
+ import { Effect } from 'effect';
4
+ import { HttpRouter } from 'effect/http';
5
+ import { type EntityReadScope, type EntityRouteContext, type EntityRouteGuard } from './entity-route-guard.js';
6
+ export interface EntityFacetRoutesOptions<T extends Entity, R, G extends EntityRouteGuard = EntityRouteGuard> {
7
+ /** Who may count: the guard the entity's `list` has, as a rule. */
8
+ readonly guard: G;
9
+ /** Defaults to `/api/<entity key>`; the route is `<basePath>/$facets`. */
10
+ readonly basePath?: `/${string}`;
11
+ /**
12
+ * Narrows the counts to the records this caller may see — the same scope
13
+ * the entity's `list` takes, so a count never reveals a record the list
14
+ * would not.
15
+ */
16
+ readonly scope?: (context: EntityRouteContext) => Effect.Effect<EntityReadScope<T>, never, R>;
17
+ }
18
+ /**
19
+ * `GET <base>/$facets` — how many of the entity's records hold each value of
20
+ * each `facet` member, each under its own `rsql.<member>` filtering — bound to
21
+ * storage through the host's {@link EntityFacetCounterFactoryTag}.
22
+ *
23
+ * A route of its own beside `makeEntityRoutes` rather than a sixth verb, so it
24
+ * is the one route set that requires a counter: a host that mounts it without
25
+ * binding one fails to compile, and a host that never counts binds nothing.
26
+ *
27
+ * The query is parsed against the entity's metadata, so only a `filterable`
28
+ * member is counted and every malformed request is a `400`. A scope is
29
+ * **conjoined into every facet's filtering**, never substituted, so no query
30
+ * string can count past it; `'none'` answers every member empty.
31
+ */
32
+ export declare const makeEntityFacetRoutes: <T extends Entity, R = never, const G extends EntityRouteGuard = never>(entityConstructor: EntityConstructor<T>, options: EntityFacetRoutesOptions<T, R, G>) => import("effect/Layer").Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Requires", EntityFacetCounterFactoryTag> | HttpRouter.Request<"Requires", import("@entifix/business").TokenServiceTag> | HttpRouter.Request<"Requires", import("@entifix/authz").PolicyDecisionTag> | HttpRouter.Request.From<"Requires", Exclude<Exclude<R, HttpRouter.Provided>, HttpRouter.Provided>> | HttpRouter.Request.From<"Requires", Exclude<Exclude<G extends {
33
+ readonly kind: "crossing";
34
+ } ? import("@entifix/authz").ServiceCrossingPolicyTag | import("./index.js").ServiceCrossingTokenTag : never, HttpRouter.Provided>, HttpRouter.Provided>> | HttpRouter.Request<"Error", unknown>>;
35
+ //# sourceMappingURL=entity-facet-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-facet-routes.d.ts","sourceRoot":"","sources":["../src/entity-facet-routes.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAG7B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAEL,KAAK,MAAM,EACX,KAAK,iBAAiB,EAMvB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,UAAU,EAAyC,MAAM,aAAa,CAAC;AAEhF,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EAGtB,MAAM,yBAAyB,CAAC;AAGjC,MAAM,WAAW,wBAAwB,CACvC,CAAC,SAAS,MAAM,EAChB,CAAC,EACD,CAAC,SAAS,gBAAgB,GAAG,gBAAgB;IAE7C,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAClB,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,CAAC,EAAE,IAAI,MAAM,EAAE,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,CACf,OAAO,EAAE,kBAAkB,KACxB,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;CAClD;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GAChC,CAAC,SAAS,MAAM,EAChB,CAAC,GAAG,KAAK,EAGT,KAAK,CAAC,CAAC,SAAS,gBAAgB,GAAG,KAAK,EAExC,mBAAmB,iBAAiB,CAAC,CAAC,CAAC,EACvC,SAAS,wBAAwB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;;iMA2E3C,CAAC"}
@@ -0,0 +1,66 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { EntityFacetCounterFactoryTag, EntityFacetCounterTag, facetCountsUCFactory } from '@entifix/business';
3
+ import { envelopeEntityName, makeEntityFacetsEnvelope, parseFacetQueryParams } from '@entifix/core';
4
+ import { Effect } from 'effect';
5
+ import { HttpRouter, HttpServerRequest, HttpServerResponse } from 'effect/http';
6
+ import { guardRoute } from './entity-route-guard.js';
7
+ import { invalid } from './route-responses.js';
8
+ /**
9
+ * `GET <base>/$facets` — how many of the entity's records hold each value of
10
+ * each `facet` member, each under its own `rsql.<member>` filtering — bound to
11
+ * storage through the host's {@link EntityFacetCounterFactoryTag}.
12
+ *
13
+ * A route of its own beside `makeEntityRoutes` rather than a sixth verb, so it
14
+ * is the one route set that requires a counter: a host that mounts it without
15
+ * binding one fails to compile, and a host that never counts binds nothing.
16
+ *
17
+ * The query is parsed against the entity's metadata, so only a `filterable`
18
+ * member is counted and every malformed request is a `400`. A scope is
19
+ * **conjoined into every facet's filtering**, never substituted, so no query
20
+ * string can count past it; `'none'` answers every member empty.
21
+ */ export const makeEntityFacetRoutes = (entityConstructor, options)=>{
22
+ var _options_basePath;
23
+ const base = (_options_basePath = options.basePath) != null ? _options_basePath : `/api/${envelopeEntityName(entityConstructor)}`;
24
+ const readQueries = Effect.gen(function*() {
25
+ const request = yield* HttpServerRequest.HttpServerRequest;
26
+ const search = new URL(request.url, 'http://localhost').searchParams;
27
+ return yield* Effect.try({
28
+ try: ()=>parseFacetQueryParams(entityConstructor, search),
29
+ catch: (error)=>error
30
+ });
31
+ });
32
+ const scoped = (queries, scope)=>scope === 'all' || scope === 'none' ? queries : queries.map((query)=>{
33
+ var _query_filtering;
34
+ return _extends({}, query, {
35
+ filtering: [
36
+ ...(_query_filtering = query.filtering) != null ? _query_filtering : [],
37
+ scope.filter
38
+ ]
39
+ });
40
+ });
41
+ const count = (context)=>Effect.gen(function*() {
42
+ const queries = yield* readQueries;
43
+ const scope = options.scope ? yield* options.scope(context) : 'all';
44
+ if (scope === 'none') {
45
+ // Still parsed: whether a query is well-formed must not depend on who
46
+ // asked.
47
+ const empty = Object.fromEntries(queries.map(({ member })=>[
48
+ member,
49
+ []
50
+ ]));
51
+ return yield* HttpServerResponse.json(makeEntityFacetsEnvelope(entityConstructor, empty));
52
+ }
53
+ const factory = yield* EntityFacetCounterFactoryTag;
54
+ const counter = yield* factory.for(entityConstructor, _extends({}, context.organizationId === undefined ? {} : {
55
+ organizationId: context.organizationId
56
+ }));
57
+ const counts = yield* facetCountsUCFactory(scoped(queries, scope)).pipe(Effect.provideService(EntityFacetCounterTag, counter));
58
+ return yield* HttpServerResponse.json(makeEntityFacetsEnvelope(entityConstructor, counts));
59
+ }).pipe(Effect.catch((error)=>invalid(error, 'invalid query', 'invalidQuery')));
60
+ const handler = guardRoute(entityConstructor, options.guard, 'read', count);
61
+ return HttpRouter.addAll([
62
+ HttpRouter.route('GET', `${base}/$facets`, handler)
63
+ ]);
64
+ };
65
+
66
+ //# sourceMappingURL=entity-facet-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/entity-facet-routes.ts"],"sourcesContent":["import {\n EntityFacetCounterFactoryTag,\n EntityFacetCounterTag,\n facetCountsUCFactory,\n} from '@entifix/business';\nimport {\n EntifixBuildError,\n type Entity,\n type EntityConstructor,\n type EntityFacetCounts,\n type EntityFacetQuery,\n envelopeEntityName,\n makeEntityFacetsEnvelope,\n parseFacetQueryParams,\n} from '@entifix/core';\nimport { Effect } from 'effect';\nimport { HttpRouter, HttpServerRequest, HttpServerResponse } from 'effect/http';\n\nimport {\n type EntityReadScope,\n type EntityRouteContext,\n type EntityRouteGuard,\n guardRoute,\n type GuardServices,\n} from './entity-route-guard.js';\nimport { invalid } from './route-responses.js';\n\nexport interface EntityFacetRoutesOptions<\n T extends Entity,\n R,\n G extends EntityRouteGuard = EntityRouteGuard,\n> {\n /** Who may count: the guard the entity's `list` has, as a rule. */\n readonly guard: G;\n /** Defaults to `/api/<entity key>`; the route is `<basePath>/$facets`. */\n readonly basePath?: `/${string}`;\n /**\n * Narrows the counts to the records this caller may see — the same scope\n * the entity's `list` takes, so a count never reveals a record the list\n * would not.\n */\n readonly scope?: (\n context: EntityRouteContext,\n ) => Effect.Effect<EntityReadScope<T>, never, R>;\n}\n\n/**\n * `GET <base>/$facets` — how many of the entity's records hold each value of\n * each `facet` member, each under its own `rsql.<member>` filtering — bound to\n * storage through the host's {@link EntityFacetCounterFactoryTag}.\n *\n * A route of its own beside `makeEntityRoutes` rather than a sixth verb, so it\n * is the one route set that requires a counter: a host that mounts it without\n * binding one fails to compile, and a host that never counts binds nothing.\n *\n * The query is parsed against the entity's metadata, so only a `filterable`\n * member is counted and every malformed request is a `400`. A scope is\n * **conjoined into every facet's filtering**, never substituted, so no query\n * string can count past it; `'none'` answers every member empty.\n */\nexport const makeEntityFacetRoutes = <\n T extends Entity,\n R = never,\n // `never` for the reason `makeEntityRoutes` gives: a caller naming `R`\n // explicitly must not widen the guard to every kind there is.\n const G extends EntityRouteGuard = never,\n>(\n entityConstructor: EntityConstructor<T>,\n options: EntityFacetRoutesOptions<T, R, G>,\n) => {\n const base: `/${string}` =\n options.basePath ?? `/api/${envelopeEntityName(entityConstructor)}`;\n\n const readQueries = Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n const search = new URL(request.url, 'http://localhost').searchParams;\n return yield* Effect.try({\n try: () => parseFacetQueryParams(entityConstructor, search),\n catch: error => error as EntifixBuildError,\n });\n });\n\n const scoped = (\n queries: EntityFacetQuery<T>[],\n scope: EntityReadScope<T>,\n ): EntityFacetQuery<T>[] =>\n scope === 'all' || scope === 'none'\n ? queries\n : queries.map(query => ({\n ...query,\n filtering: [...(query.filtering ?? []), scope.filter],\n }));\n\n const count = (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const queries = yield* readQueries;\n const scope = options.scope\n ? yield* options.scope(context)\n : ('all' as const);\n if (scope === 'none') {\n // Still parsed: whether a query is well-formed must not depend on who\n // asked.\n const empty: EntityFacetCounts = Object.fromEntries(\n queries.map(({ member }) => [member, []]),\n );\n return yield* HttpServerResponse.json(\n makeEntityFacetsEnvelope(entityConstructor, empty),\n );\n }\n const factory = yield* EntityFacetCounterFactoryTag;\n const counter = yield* factory.for(entityConstructor, {\n ...(context.organizationId === undefined\n ? {}\n : { organizationId: context.organizationId }),\n });\n const counts = yield* facetCountsUCFactory<T>(\n scoped(queries, scope),\n ).pipe(Effect.provideService(EntityFacetCounterTag, counter));\n return yield* HttpServerResponse.json(\n makeEntityFacetsEnvelope(entityConstructor, counts),\n );\n }).pipe(\n Effect.catch(error => invalid(error, 'invalid query', 'invalidQuery')),\n );\n\n const handler = guardRoute(\n entityConstructor,\n options.guard,\n 'read',\n count,\n ) as Effect.Effect<\n HttpServerResponse.HttpServerResponse,\n unknown,\n | R\n | EntityFacetCounterFactoryTag\n | GuardServices<G>\n | HttpServerRequest.HttpServerRequest\n | HttpRouter.RouteContext\n >;\n\n return HttpRouter.addAll([\n HttpRouter.route('GET', `${base}/$facets`, handler),\n ]);\n};\n"],"names":["EntityFacetCounterFactoryTag","EntityFacetCounterTag","facetCountsUCFactory","envelopeEntityName","makeEntityFacetsEnvelope","parseFacetQueryParams","Effect","HttpRouter","HttpServerRequest","HttpServerResponse","guardRoute","invalid","makeEntityFacetRoutes","entityConstructor","options","base","basePath","readQueries","gen","request","search","URL","url","searchParams","try","catch","error","scoped","queries","scope","map","query","filtering","filter","count","context","empty","Object","fromEntries","member","json","factory","counter","for","organizationId","undefined","counts","pipe","provideService","handler","guard","addAll","route"],"mappings":";AAAA,SACEA,4BAA4B,EAC5BC,qBAAqB,EACrBC,oBAAoB,QACf,oBAAoB;AAC3B,SAMEC,kBAAkB,EAClBC,wBAAwB,EACxBC,qBAAqB,QAChB,gBAAgB;AACvB,SAASC,MAAM,QAAQ,SAAS;AAChC,SAASC,UAAU,EAAEC,iBAAiB,EAAEC,kBAAkB,QAAQ,cAAc;AAEhF,SAIEC,UAAU,QAEL,0BAA0B;AACjC,SAASC,OAAO,QAAQ,uBAAuB;AAqB/C;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMC,wBAAwB,CAOnCC,mBACAC;QAGEA;IADF,MAAMC,QACJD,oBAAAA,QAAQE,QAAQ,YAAhBF,oBAAoB,CAAC,KAAK,EAAEX,mBAAmBU,oBAAoB;IAErE,MAAMI,cAAcX,OAAOY,GAAG,CAAC;QAC7B,MAAMC,UAAU,OAAOX,kBAAkBA,iBAAiB;QAC1D,MAAMY,SAAS,IAAIC,IAAIF,QAAQG,GAAG,EAAE,oBAAoBC,YAAY;QACpE,OAAO,OAAOjB,OAAOkB,GAAG,CAAC;YACvBA,KAAK,IAAMnB,sBAAsBQ,mBAAmBO;YACpDK,OAAOC,CAAAA,QAASA;QAClB;IACF;IAEA,MAAMC,SAAS,CACbC,SACAC,QAEAA,UAAU,SAASA,UAAU,SACzBD,UACAA,QAAQE,GAAG,CAACC,CAAAA;gBAEMA;mBAFI,aACjBA;gBACHC,WAAW;wBAAKD,mBAAAA,MAAMC,SAAS,YAAfD,mBAAmB,EAAE;oBAAGF,MAAMI,MAAM;iBAAC;;;IAG7D,MAAMC,QAAQ,CAACC,UACb7B,OAAOY,GAAG,CAAC;YACT,MAAMU,UAAU,OAAOX;YACvB,MAAMY,QAAQf,QAAQe,KAAK,GACvB,OAAOf,QAAQe,KAAK,CAACM,WACpB;YACL,IAAIN,UAAU,QAAQ;gBACpB,sEAAsE;gBACtE,SAAS;gBACT,MAAMO,QAA2BC,OAAOC,WAAW,CACjDV,QAAQE,GAAG,CAAC,CAAC,EAAES,MAAM,EAAE,GAAK;wBAACA;wBAAQ,EAAE;qBAAC;gBAE1C,OAAO,OAAO9B,mBAAmB+B,IAAI,CACnCpC,yBAAyBS,mBAAmBuB;YAEhD;YACA,MAAMK,UAAU,OAAOzC;YACvB,MAAM0C,UAAU,OAAOD,QAAQE,GAAG,CAAC9B,mBAAmB,aAChDsB,QAAQS,cAAc,KAAKC,YAC3B,CAAC,IACD;gBAAED,gBAAgBT,QAAQS,cAAc;YAAC;YAE/C,MAAME,SAAS,OAAO5C,qBACpByB,OAAOC,SAASC,QAChBkB,IAAI,CAACzC,OAAO0C,cAAc,CAAC/C,uBAAuByC;YACpD,OAAO,OAAOjC,mBAAmB+B,IAAI,CACnCpC,yBAAyBS,mBAAmBiC;QAEhD,GAAGC,IAAI,CACLzC,OAAOmB,KAAK,CAACC,CAAAA,QAASf,QAAQe,OAAO,iBAAiB;IAG1D,MAAMuB,UAAUvC,WACdG,mBACAC,QAAQoC,KAAK,EACb,QACAhB;IAWF,OAAO3B,WAAW4C,MAAM,CAAC;QACvB5C,WAAW6C,KAAK,CAAC,OAAO,GAAGrC,KAAK,QAAQ,CAAC,EAAEkC;KAC5C;AACH,EAAE"}
@@ -20,5 +20,5 @@ import { HttpServerRequest, HttpServerResponse } from 'effect/http';
20
20
  * database handle here would leave a vendor with no active organization unable
21
21
  * to see their own affordances.
22
22
  */
23
- export declare const entityMetadataRoute: <TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>) => Effect.Effect<HttpServerResponse.HttpServerResponse, import("effect/http/HttpBody").HttpBodyError, PolicyDecisionTag | import("@entifix/business").TokenServiceTag | HttpServerRequest.HttpServerRequest>;
23
+ export declare const entityMetadataRoute: <TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>) => Effect.Effect<HttpServerResponse.HttpServerResponse, import("effect/http/HttpBody").HttpBodyError, import("@entifix/business").TokenServiceTag | PolicyDecisionTag | HttpServerRequest.HttpServerRequest>;
24
24
  //# sourceMappingURL=entity-metadata-route.d.ts.map
@@ -0,0 +1,60 @@
1
+ import { type Action, type PolicyDecisionTag, type ServiceCrossingPolicyTag } from '@entifix/authz';
2
+ import type { RepositoryScope, TokenServiceTag } from '@entifix/business';
3
+ import type { Entity, EntityConstructor, FilterGroup } from '@entifix/core';
4
+ import type { Effect } from 'effect';
5
+ import { type RequestPrincipal, type ServiceCrossingTokenTag } from './auth/index.js';
6
+ /**
7
+ * Who may reach a route, and where the organization a per-organization store
8
+ * needs comes from.
9
+ *
10
+ * - `public` — anybody; nothing is checked and no organization is known.
11
+ * - `permission` — a verified session holding the permission the entity
12
+ * derives for the verb.
13
+ * - `organization` — the same, plus the session's active organization, which
14
+ * is what a per-organization store opens.
15
+ * - `crossing` — the fleet's crossing token and no session at all. With
16
+ * `organization: true` the caller names the organization in a header, which
17
+ * only a caller that has proved it is the fleet may do.
18
+ */
19
+ export type EntityRouteGuard = {
20
+ readonly kind: 'public';
21
+ } | {
22
+ readonly kind: 'permission';
23
+ } | {
24
+ readonly kind: 'organization';
25
+ } | {
26
+ readonly kind: 'crossing';
27
+ readonly organization: boolean;
28
+ };
29
+ /** What a route learned about its caller from its guard. */
30
+ export interface EntityRouteContext extends RepositoryScope {
31
+ readonly principal?: RequestPrincipal;
32
+ }
33
+ /**
34
+ * Which records of an entity a caller may see.
35
+ *
36
+ * `filter` narrows a list and `includes` decides a by-id read — both are
37
+ * needed, because `id` is the one member no query can name. `'none'` is a
38
+ * caller who holds the read grant and owns nothing: an empty page and a `404`,
39
+ * never a `403`, because owning no records is not an error.
40
+ */
41
+ export type EntityReadScope<T extends Entity> = 'all' | 'none' | {
42
+ readonly filter: FilterGroup<T>;
43
+ readonly includes: (entity: T) => boolean;
44
+ };
45
+ /**
46
+ * What a route needs from the host for its guard: a session's services always
47
+ * (a `$metadata` read and a session guard both read the caller), the fleet's
48
+ * crossing services only when a guard is a crossing.
49
+ */
50
+ export type GuardServices<G extends EntityRouteGuard> = TokenServiceTag | PolicyDecisionTag | (G extends {
51
+ readonly kind: 'crossing';
52
+ } ? ServiceCrossingTokenTag | ServiceCrossingPolicyTag : never);
53
+ /**
54
+ * Runs `use` behind `guard`, checking the permission the entity derives for
55
+ * `action` — the one place a derived route turns a stated guard into the
56
+ * check that enforces it, so the entity routes and the facet route cannot
57
+ * enforce the same guard differently.
58
+ */
59
+ export declare const guardRoute: <T extends Entity, A, E, R>(entityConstructor: EntityConstructor<T>, guard: EntityRouteGuard, action: Action, use: (context: EntityRouteContext) => Effect.Effect<A, E, R>) => Effect.Effect<import("effect/http/HttpServerResponse").HttpServerResponse | A, import("effect/http/HttpBody").HttpBodyError | E, TokenServiceTag | PolicyDecisionTag | import("effect/http/HttpServerRequest").HttpServerRequest | R> | Effect.Effect<import("effect/http/HttpServerResponse").HttpServerResponse | A, import("effect/http/HttpBody").HttpBodyError | E, ServiceCrossingPolicyTag | import("effect/http/HttpServerRequest").HttpServerRequest | ServiceCrossingTokenTag | R>;
60
+ //# sourceMappingURL=entity-route-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-route-guard.d.ts","sourceRoot":"","sources":["../src/entity-route-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,MAAM,EAEX,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC9B,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC1E,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,OAAO,EACL,KAAK,gBAAgB,EAKrB,KAAK,uBAAuB,EAC7B,MAAM,iBAAiB,CAAC;AAEzB;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAC3B;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;CAAE,GAC/B;IAAE,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;CAAE,GACjC;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAA;CAAE,CAAC;AAElE,4DAA4D;AAC5D,MAAM,WAAW,kBAAmB,SAAQ,eAAe;IACzD,QAAQ,CAAC,SAAS,CAAC,EAAE,gBAAgB,CAAC;CACvC;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,MAAM,IACxC,KAAK,GACL,MAAM,GACN;IACE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC;CAC3C,CAAC;AAEN;;;;GAIG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,gBAAgB,IAChD,eAAe,GACf,iBAAiB,GACjB,CAAC,CAAC,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GACpC,uBAAuB,GAAG,wBAAwB,GAClD,KAAK,CAAC,CAAC;AAEf;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAClD,mBAAmB,iBAAiB,CAAC,CAAC,CAAC,EACvC,OAAO,gBAAgB,EACvB,QAAQ,MAAM,EACd,KAAK,CAAC,OAAO,EAAE,kBAAkB,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,ieAmB7D,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { permissionForEntity } from '@entifix/authz';
2
+ import { requireCrossing, requireOrganization, requirePermission, requireServiceCrossing } from './auth/index.js';
3
+ /**
4
+ * Runs `use` behind `guard`, checking the permission the entity derives for
5
+ * `action` — the one place a derived route turns a stated guard into the
6
+ * check that enforces it, so the entity routes and the facet route cannot
7
+ * enforce the same guard differently.
8
+ */ export const guardRoute = (entityConstructor, guard, action, use)=>{
9
+ const permission = permissionForEntity(entityConstructor, action);
10
+ switch(guard.kind){
11
+ case 'public':
12
+ return use({});
13
+ case 'permission':
14
+ return requirePermission(permission)((principal)=>use({
15
+ principal
16
+ }));
17
+ case 'organization':
18
+ return requireOrganization(permission)((organizationId, principal)=>use({
19
+ organizationId,
20
+ principal
21
+ }));
22
+ case 'crossing':
23
+ return guard.organization ? requireServiceCrossing(permission)((organizationId)=>use({
24
+ organizationId
25
+ })) : requireCrossing(permission)(use({}));
26
+ }
27
+ };
28
+
29
+ //# sourceMappingURL=entity-route-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/entity-route-guard.ts"],"sourcesContent":["import {\n type Action,\n permissionForEntity,\n type PolicyDecisionTag,\n type ServiceCrossingPolicyTag,\n} from '@entifix/authz';\nimport type { RepositoryScope, TokenServiceTag } from '@entifix/business';\nimport type { Entity, EntityConstructor, FilterGroup } from '@entifix/core';\nimport type { Effect } from 'effect';\n\nimport {\n type RequestPrincipal,\n requireCrossing,\n requireOrganization,\n requirePermission,\n requireServiceCrossing,\n type ServiceCrossingTokenTag,\n} from './auth/index.js';\n\n/**\n * Who may reach a route, and where the organization a per-organization store\n * needs comes from.\n *\n * - `public` — anybody; nothing is checked and no organization is known.\n * - `permission` — a verified session holding the permission the entity\n * derives for the verb.\n * - `organization` — the same, plus the session's active organization, which\n * is what a per-organization store opens.\n * - `crossing` — the fleet's crossing token and no session at all. With\n * `organization: true` the caller names the organization in a header, which\n * only a caller that has proved it is the fleet may do.\n */\nexport type EntityRouteGuard =\n | { readonly kind: 'public' }\n | { readonly kind: 'permission' }\n | { readonly kind: 'organization' }\n | { readonly kind: 'crossing'; readonly organization: boolean };\n\n/** What a route learned about its caller from its guard. */\nexport interface EntityRouteContext extends RepositoryScope {\n readonly principal?: RequestPrincipal;\n}\n\n/**\n * Which records of an entity a caller may see.\n *\n * `filter` narrows a list and `includes` decides a by-id read — both are\n * needed, because `id` is the one member no query can name. `'none'` is a\n * caller who holds the read grant and owns nothing: an empty page and a `404`,\n * never a `403`, because owning no records is not an error.\n */\nexport type EntityReadScope<T extends Entity> =\n | 'all'\n | 'none'\n | {\n readonly filter: FilterGroup<T>;\n readonly includes: (entity: T) => boolean;\n };\n\n/**\n * What a route needs from the host for its guard: a session's services always\n * (a `$metadata` read and a session guard both read the caller), the fleet's\n * crossing services only when a guard is a crossing.\n */\nexport type GuardServices<G extends EntityRouteGuard> =\n | TokenServiceTag\n | PolicyDecisionTag\n | (G extends { readonly kind: 'crossing' }\n ? ServiceCrossingTokenTag | ServiceCrossingPolicyTag\n : never);\n\n/**\n * Runs `use` behind `guard`, checking the permission the entity derives for\n * `action` — the one place a derived route turns a stated guard into the\n * check that enforces it, so the entity routes and the facet route cannot\n * enforce the same guard differently.\n */\nexport const guardRoute = <T extends Entity, A, E, R>(\n entityConstructor: EntityConstructor<T>,\n guard: EntityRouteGuard,\n action: Action,\n use: (context: EntityRouteContext) => Effect.Effect<A, E, R>,\n) => {\n const permission = permissionForEntity(entityConstructor, action);\n switch (guard.kind) {\n case 'public':\n return use({});\n case 'permission':\n return requirePermission(permission)(principal => use({ principal }));\n case 'organization':\n return requireOrganization(permission)((organizationId, principal) =>\n use({ organizationId, principal }),\n );\n case 'crossing':\n return guard.organization\n ? requireServiceCrossing(permission)(organizationId =>\n use({ organizationId }),\n )\n : requireCrossing(permission)(use({}));\n }\n};\n"],"names":["permissionForEntity","requireCrossing","requireOrganization","requirePermission","requireServiceCrossing","guardRoute","entityConstructor","guard","action","use","permission","kind","principal","organizationId","organization"],"mappings":"AAAA,SAEEA,mBAAmB,QAGd,iBAAiB;AAKxB,SAEEC,eAAe,EACfC,mBAAmB,EACnBC,iBAAiB,EACjBC,sBAAsB,QAEjB,kBAAkB;AAsDzB;;;;;CAKC,GACD,OAAO,MAAMC,aAAa,CACxBC,mBACAC,OACAC,QACAC;IAEA,MAAMC,aAAaV,oBAAoBM,mBAAmBE;IAC1D,OAAQD,MAAMI,IAAI;QAChB,KAAK;YACH,OAAOF,IAAI,CAAC;QACd,KAAK;YACH,OAAON,kBAAkBO,YAAYE,CAAAA,YAAaH,IAAI;oBAAEG;gBAAU;QACpE,KAAK;YACH,OAAOV,oBAAoBQ,YAAY,CAACG,gBAAgBD,YACtDH,IAAI;oBAAEI;oBAAgBD;gBAAU;QAEpC,KAAK;YACH,OAAOL,MAAMO,YAAY,GACrBV,uBAAuBM,YAAYG,CAAAA,iBACjCJ,IAAI;oBAAEI;gBAAe,MAEvBZ,gBAAgBS,YAAYD,IAAI,CAAC;IACzC;AACF,EAAE"}
@@ -1,62 +1,13 @@
1
- import { type PolicyDecisionTag, type ServiceCrossingPolicyTag } from '@entifix/authz';
2
- import { EntityRepositoryFactoryTag, type RepositoryScope, type TokenServiceTag } from '@entifix/business';
3
- import { EntifixBuildError, type EntifixEnvelopeLink, type Entity, type EntityConstructor, type EntityId, type FilterGroup } from '@entifix/core';
1
+ import { EntityRepositoryFactoryTag } from '@entifix/business';
2
+ import { EntifixBuildError, type EntifixEnvelopeLink, type Entity, type EntityConstructor, type EntityId } from '@entifix/core';
4
3
  import { Effect } from 'effect';
5
4
  import { HttpRouter, HttpServerResponse } from 'effect/http';
6
- import { type RequestPrincipal, type ServiceCrossingTokenTag } from './auth/index.js';
5
+ import { type EntityReadScope, type EntityRouteContext, type EntityRouteGuard } from './entity-route-guard.js';
6
+ import { EntityRouteRefusal, routeFailure } from './route-responses.js';
7
7
  /** The five generic verbs a route set can mount. */
8
8
  export type EntityVerb = 'list' | 'get' | 'create' | 'update' | 'delete';
9
- /**
10
- * Who may reach a route, and where the organization a per-organization store
11
- * needs comes from.
12
- *
13
- * - `public` — anybody; nothing is checked and no organization is known.
14
- * - `permission` — a verified session holding the permission the entity
15
- * derives for the verb.
16
- * - `organization` — the same, plus the session's active organization, which
17
- * is what a per-organization store opens.
18
- * - `crossing` — the fleet's crossing token and no session at all. With
19
- * `organization: true` the caller names the organization in a header, which
20
- * only a caller that has proved it is the fleet may do.
21
- */
22
- export type EntityRouteGuard = {
23
- readonly kind: 'public';
24
- } | {
25
- readonly kind: 'permission';
26
- } | {
27
- readonly kind: 'organization';
28
- } | {
29
- readonly kind: 'crossing';
30
- readonly organization: boolean;
31
- };
32
- /** What a route learned about its caller from its guard. */
33
- export interface EntityRouteContext extends RepositoryScope {
34
- readonly principal?: RequestPrincipal;
35
- }
36
- /**
37
- * Which records of an entity a caller may see.
38
- *
39
- * `filter` narrows a list and `includes` decides a by-id read — both are
40
- * needed, because `id` is the one member no query can name. `'none'` is a
41
- * caller who holds the read grant and owns nothing: an empty page and a `404`,
42
- * never a `403`, because owning no records is not an error.
43
- */
44
- export type EntityReadScope<T extends Entity> = 'all' | 'none' | {
45
- readonly filter: FilterGroup<T>;
46
- readonly includes: (entity: T) => boolean;
47
- };
48
- /**
49
- * A save refused by a rule the entity's own deserializer does not make — a
50
- * closed set, a rule over the other records — answered as `status` with
51
- * `code`, which is what a client translates.
52
- */
53
- export declare class EntityRouteRefusal {
54
- readonly status: 400 | 409 | 422;
55
- readonly code: string;
56
- readonly detail?: string | undefined;
57
- readonly _tag = "EntityRouteRefusal";
58
- constructor(status: 400 | 409 | 422, code: string, detail?: string | undefined);
59
- }
9
+ export { type EntityReadScope, type EntityRouteContext, type EntityRouteGuard, } from './entity-route-guard.js';
10
+ export { EntityRouteRefusal } from './route-responses.js';
60
11
  /** A hand-written handler for one verb, mounted under the derived guard. */
61
12
  export type EntityRouteOverride<R> = (context: EntityRouteContext, links: EntityRouteLinks) => Effect.Effect<HttpServerResponse.HttpServerResponse, unknown, R>;
62
13
  /** The affordances a route set derives from the verbs it mounts. */
@@ -89,24 +40,19 @@ export interface EntityRoutesOptions<T extends Entity, R, G extends EntityRouteG
89
40
  */
90
41
  readonly override?: Partial<Record<EntityVerb, EntityRouteOverride<R>>>;
91
42
  }
92
- /** The `500` for anything a route did not anticipate. */
93
- export declare const routeFailure: (error: unknown) => Effect.Effect<HttpServerResponse.HttpServerResponse, import("effect/http/HttpBody").HttpBodyError, never>;
94
43
  /**
95
- * The generic routes for one entity — list, read, create, update, delete and
96
- * its `$metadata` — derived from the entity and bound to storage through the
97
- * host's {@link EntityRepositoryFactoryTag}.
98
- *
99
- * What a route set varies on is stated, not written: which verbs exist, who may
100
- * reach them, which records a reader sees, and what a save must pass. What it
101
- * never varies on is the wire: the load request is parsed against the entity's
102
- * own metadata (so a query can only name a member the entity declared
103
- * filterable or sortable), every answer is an envelope, and the links name
104
- * exactly the verbs mounted — never one that would `404`.
105
- *
106
- * A path that is not one of these verbs is written by hand beside it, and a
107
- * verb whose generic form does not fit is replaced with `override`.
44
+ * {@link deriveEntityRoutes}, with a curried form for a caller that has to
45
+ * name the services its `override`, `scope` or `prepare` handlers need:
46
+ * `makeEntityRoutes.for<R>()(Entity, options)` names `R` and still infers the
47
+ * entity and the guard from the value, which a single call with explicit type
48
+ * arguments cannot do.
108
49
  */
109
- export declare const makeEntityRoutes: <T extends Entity, R = never, const G extends EntityRouteGuard = EntityRouteGuard>(entityConstructor: EntityConstructor<T>, options: EntityRoutesOptions<T, R, G>) => import("effect/Layer").Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Requires", PolicyDecisionTag> | HttpRouter.Request<"Requires", EntityRepositoryFactoryTag> | HttpRouter.Request<"Requires", TokenServiceTag> | HttpRouter.Request.From<"Requires", Exclude<Exclude<R, HttpRouter.Provided>, HttpRouter.Provided>> | HttpRouter.Request.From<"Requires", Exclude<Exclude<G extends {
50
+ export { routeFailure };
51
+ export declare const makeEntityRoutes: (<T extends Entity, R = never, const G extends EntityRouteGuard = never>(entityConstructor: EntityConstructor<T>, options: EntityRoutesOptions<T, R, G>) => import("effect/Layer").Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Requires", import("@entifix/business").TokenServiceTag> | HttpRouter.Request<"Requires", import("@entifix/authz").PolicyDecisionTag> | HttpRouter.Request<"Error", unknown> | HttpRouter.Request<"Requires", EntityRepositoryFactoryTag> | HttpRouter.Request.From<"Requires", Exclude<Exclude<R, HttpRouter.Provided>, HttpRouter.Provided>> | HttpRouter.Request.From<"Requires", Exclude<Exclude<G extends {
110
52
  readonly kind: "crossing";
111
- } ? ServiceCrossingPolicyTag | ServiceCrossingTokenTag : never, HttpRouter.Provided>, HttpRouter.Provided>> | HttpRouter.Request<"Error", unknown>>;
53
+ } ? import("@entifix/authz").ServiceCrossingPolicyTag | import("./index.js").ServiceCrossingTokenTag : never, HttpRouter.Provided>, HttpRouter.Provided>>>) & {
54
+ for: <R>() => <T extends Entity, const G extends EntityRouteGuard>(entityConstructor: EntityConstructor<T>, options: EntityRoutesOptions<T, R, G>) => import("effect/Layer").Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Requires", import("@entifix/business").TokenServiceTag> | HttpRouter.Request<"Requires", import("@entifix/authz").PolicyDecisionTag> | HttpRouter.Request<"Error", unknown> | HttpRouter.Request<"Requires", EntityRepositoryFactoryTag> | HttpRouter.Request.From<"Requires", Exclude<Exclude<R, HttpRouter.Provided>, HttpRouter.Provided>> | HttpRouter.Request.From<"Requires", Exclude<Exclude<G extends {
55
+ readonly kind: "crossing";
56
+ } ? import("@entifix/authz").ServiceCrossingPolicyTag | import("./index.js").ServiceCrossingTokenTag : never, HttpRouter.Provided>, HttpRouter.Provided>>>;
57
+ };
112
58
  //# sourceMappingURL=entity-routes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"entity-routes.d.ts","sourceRoot":"","sources":["../src/entity-routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"entity-routes.d.ts","sourceRoot":"","sources":["../src/entity-routes.ts"],"names":[],"mappings":"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"}
@@ -1,23 +1,12 @@
1
1
  import { _ as _extends } from "@swc/helpers/_/_extends";
2
- import { permissionForEntity } from '@entifix/authz';
3
2
  import { deleteUCFactory, EntityIdTag, EntityLoadRequestTag, EntityRepositoryFactoryTag, EntityRepositoryTag, EntityTag, getUCFactory, loadUCFactory, saveUCFactory } from '@entifix/business';
4
- import { EntifixBuildError, envelopeEntityName, extractMetaEntity, makeEntityEnvelope, makeEntityPageEnvelope, parseLoadRequestParams, readEntityEnvelope } from '@entifix/core';
3
+ import { envelopeEntityName, extractMetaEntity, makeEntityEnvelope, makeEntityPageEnvelope, parseLoadRequestParams, readEntityEnvelope } from '@entifix/core';
5
4
  import { Effect } from 'effect';
6
5
  import { HttpRouter, HttpServerRequest, HttpServerResponse } from 'effect/http';
7
- import { requireCrossing, requireOrganization, requirePermission, requireServiceCrossing } from './auth/index.js';
8
6
  import { entityMetadataRoute } from './entity-metadata-route.js';
9
- /**
10
- * A save refused by a rule the entity's own deserializer does not make — a
11
- * closed set, a rule over the other records — answered as `status` with
12
- * `code`, which is what a client translates.
13
- */ export class EntityRouteRefusal {
14
- constructor(status, code, detail){
15
- this.status = status;
16
- this.code = code;
17
- this.detail = detail;
18
- this._tag = 'EntityRouteRefusal';
19
- }
20
- }
7
+ import { guardRoute } from './entity-route-guard.js';
8
+ import { invalid, routeFailure } from './route-responses.js';
9
+ export { EntityRouteRefusal } from './route-responses.js';
21
10
  const ACTION = {
22
11
  list: 'read',
23
12
  get: 'read',
@@ -29,34 +18,6 @@ const READS = new Set([
29
18
  'list',
30
19
  'get'
31
20
  ]);
32
- /** The `500` for anything a route did not anticipate. */ export const routeFailure = (error)=>HttpServerResponse.json({
33
- error: 'request failed',
34
- code: 'unexpected',
35
- detail: String(error)
36
- }, {
37
- status: 500
38
- });
39
- const invalid = (error, label, code)=>{
40
- var _refusalCode;
41
- return error instanceof EntifixBuildError ? HttpServerResponse.json({
42
- error: label,
43
- code: (_refusalCode = refusalCode(error)) != null ? _refusalCode : code,
44
- detail: error.message
45
- }, {
46
- status: 400
47
- }) : error instanceof EntityRouteRefusal ? HttpServerResponse.json(_extends({
48
- error: 'the record cannot be saved',
49
- code: error.code
50
- }, error.detail === undefined ? {} : {
51
- detail: error.detail
52
- }), {
53
- status: error.status
54
- }) : routeFailure(error);
55
- };
56
- /**
57
- * A domain error may carry its own code (`mediaNotHttps`), so the client can
58
- * say which rule failed rather than "invalid body".
59
- */ const refusalCode = (error)=>'code' in error && typeof error.code === 'string' ? error.code : undefined;
60
21
  /**
61
22
  * The generic routes for one entity — list, read, create, update, delete and
62
23
  * its `$metadata` — derived from the entity and bound to storage through the
@@ -71,7 +32,7 @@ const invalid = (error, label, code)=>{
71
32
  *
72
33
  * A path that is not one of these verbs is written by hand beside it, and a
73
34
  * verb whose generic form does not fit is replaced with `override`.
74
- */ export const makeEntityRoutes = (entityConstructor, options)=>{
35
+ */ const deriveEntityRoutes = (entityConstructor, options)=>{
75
36
  var _options_basePath;
76
37
  const key = envelopeEntityName(entityConstructor);
77
38
  const base = (_options_basePath = options.basePath) != null ? _options_basePath : `/api/${key}`;
@@ -131,27 +92,7 @@ const invalid = (error, label, code)=>{
131
92
  const guard = options.guard;
132
93
  return 'kind' in guard ? guard : READS.has(verb) ? guard.read : guard.write;
133
94
  };
134
- const guarded = (verb, use)=>{
135
- const guard = guardFor(verb);
136
- const permission = permissionForEntity(entityConstructor, ACTION[verb]);
137
- switch(guard.kind){
138
- case 'public':
139
- return use({});
140
- case 'permission':
141
- return requirePermission(permission)((principal)=>use({
142
- principal
143
- }));
144
- case 'organization':
145
- return requireOrganization(permission)((organizationId, principal)=>use({
146
- organizationId,
147
- principal
148
- }));
149
- case 'crossing':
150
- return guard.organization ? requireServiceCrossing(permission)((organizationId)=>use({
151
- organizationId
152
- })) : requireCrossing(permission)(use({}));
153
- }
154
- };
95
+ const guarded = (verb, use)=>guardRoute(entityConstructor, guardFor(verb), ACTION[verb], use);
155
96
  const repository = (context)=>Effect.gen(function*() {
156
97
  const factory = yield* EntityRepositoryFactoryTag;
157
98
  return yield* factory.for(entityConstructor, _extends({}, context.organizationId === undefined ? {} : {
@@ -298,5 +239,15 @@ const invalid = (error, label, code)=>{
298
239
  ...options.verbs.map((verb)=>HttpRouter.route(ROUTES[verb].method, ROUTES[verb].path, handlerFor(verb)))
299
240
  ]);
300
241
  };
242
+ /**
243
+ * {@link deriveEntityRoutes}, with a curried form for a caller that has to
244
+ * name the services its `override`, `scope` or `prepare` handlers need:
245
+ * `makeEntityRoutes.for<R>()(Entity, options)` names `R` and still infers the
246
+ * entity and the guard from the value, which a single call with explicit type
247
+ * arguments cannot do.
248
+ */ export { routeFailure };
249
+ export const makeEntityRoutes = Object.assign(deriveEntityRoutes, {
250
+ for: ()=>(entityConstructor, options)=>deriveEntityRoutes(entityConstructor, options)
251
+ });
301
252
 
302
253
  //# sourceMappingURL=entity-routes.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/entity-routes.ts"],"sourcesContent":["import {\n type Action,\n permissionForEntity,\n type PolicyDecisionTag,\n type ServiceCrossingPolicyTag,\n} from '@entifix/authz';\nimport {\n deleteUCFactory,\n EntityIdTag,\n EntityLoadRequestTag,\n EntityRepositoryFactoryTag,\n EntityRepositoryTag,\n EntityTag,\n getUCFactory,\n loadUCFactory,\n type RepositoryScope,\n saveUCFactory,\n type TokenServiceTag,\n} from '@entifix/business';\nimport {\n EntifixBuildError,\n type EntifixEnvelopeLink,\n type Entity,\n type EntityConstructor,\n type EntityId,\n type EntityLoadRequest,\n envelopeEntityName,\n extractMetaEntity,\n type FilterGroup,\n makeEntityEnvelope,\n makeEntityPageEnvelope,\n parseLoadRequestParams,\n readEntityEnvelope,\n} from '@entifix/core';\nimport { Effect } from 'effect';\nimport { HttpRouter, HttpServerRequest, HttpServerResponse } from 'effect/http';\n\nimport {\n type RequestPrincipal,\n requireCrossing,\n requireOrganization,\n requirePermission,\n requireServiceCrossing,\n type ServiceCrossingTokenTag,\n} from './auth/index.js';\nimport { entityMetadataRoute } from './entity-metadata-route.js';\n\n/** The five generic verbs a route set can mount. */\nexport type EntityVerb = 'list' | 'get' | 'create' | 'update' | 'delete';\n\n/**\n * Who may reach a route, and where the organization a per-organization store\n * needs comes from.\n *\n * - `public` — anybody; nothing is checked and no organization is known.\n * - `permission` — a verified session holding the permission the entity\n * derives for the verb.\n * - `organization` — the same, plus the session's active organization, which\n * is what a per-organization store opens.\n * - `crossing` — the fleet's crossing token and no session at all. With\n * `organization: true` the caller names the organization in a header, which\n * only a caller that has proved it is the fleet may do.\n */\nexport type EntityRouteGuard =\n | { readonly kind: 'public' }\n | { readonly kind: 'permission' }\n | { readonly kind: 'organization' }\n | { readonly kind: 'crossing'; readonly organization: boolean };\n\n/** What a route learned about its caller from its guard. */\nexport interface EntityRouteContext extends RepositoryScope {\n readonly principal?: RequestPrincipal;\n}\n\n/**\n * Which records of an entity a caller may see.\n *\n * `filter` narrows a list and `includes` decides a by-id read — both are\n * needed, because `id` is the one member no query can name. `'none'` is a\n * caller who holds the read grant and owns nothing: an empty page and a `404`,\n * never a `403`, because owning no records is not an error.\n */\nexport type EntityReadScope<T extends Entity> =\n | 'all'\n | 'none'\n | {\n readonly filter: FilterGroup<T>;\n readonly includes: (entity: T) => boolean;\n };\n\n/**\n * A save refused by a rule the entity's own deserializer does not make — a\n * closed set, a rule over the other records — answered as `status` with\n * `code`, which is what a client translates.\n */\nexport class EntityRouteRefusal {\n readonly _tag = 'EntityRouteRefusal';\n\n constructor(\n readonly status: 400 | 409 | 422,\n readonly code: string,\n readonly detail?: string,\n ) {}\n}\n\n/** A hand-written handler for one verb, mounted under the derived guard. */\nexport type EntityRouteOverride<R> = (\n context: EntityRouteContext,\n links: EntityRouteLinks,\n) => Effect.Effect<HttpServerResponse.HttpServerResponse, unknown, R>;\n\n/** The affordances a route set derives from the verbs it mounts. */\nexport interface EntityRouteLinks {\n readonly record: (id: EntityId) => EntifixEnvelopeLink[];\n readonly collection: () => EntifixEnvelopeLink[];\n}\n\nexport interface EntityRoutesOptions<\n T extends Entity,\n R,\n G extends EntityRouteGuard = EntityRouteGuard,\n> {\n readonly verbs: readonly EntityVerb[];\n /** One guard for every verb, or one for the reads and one for the writes. */\n readonly guard: G | { readonly read: G; readonly write: G };\n /** Defaults to `/api/<entity key>`. */\n readonly basePath?: `/${string}`;\n /** Narrows the reads to what this caller may see. */\n readonly scope?: (\n context: EntityRouteContext,\n ) => Effect.Effect<EntityReadScope<T>, never, R>;\n /**\n * Runs on every create and update, after the body is read and before the\n * save — and may change what is saved, or refuse it.\n */\n readonly prepare?: (\n entity: T,\n context: EntityRouteContext & { readonly verb: 'create' | 'update' },\n ) => Effect.Effect<T, EntityRouteRefusal | EntifixBuildError, R>;\n /**\n * A hand-written handler in place of the generic one for a verb. It keeps\n * the derived guard and receives the derived links, so the escape hatch\n * cannot drift from the rest of the set.\n */\n readonly override?: Partial<Record<EntityVerb, EntityRouteOverride<R>>>;\n}\n\nconst ACTION: Record<EntityVerb, Action> = {\n list: 'read',\n get: 'read',\n create: 'write',\n update: 'write',\n delete: 'delete',\n};\n\nconst READS: ReadonlySet<EntityVerb> = new Set(['list', 'get']);\n\n/**\n * What a route set needs from the host beyond the repository factory: a\n * session's services always (its `$metadata` reads the caller), the fleet's\n * crossing services only when a guard is a crossing.\n */\ntype GuardServices<G extends EntityRouteGuard> =\n | TokenServiceTag\n | PolicyDecisionTag\n | (G extends { readonly kind: 'crossing' }\n ? ServiceCrossingTokenTag | ServiceCrossingPolicyTag\n : never);\n\n/** The `500` for anything a route did not anticipate. */\nexport const routeFailure = (error: unknown) =>\n HttpServerResponse.json(\n { error: 'request failed', code: 'unexpected', detail: String(error) },\n { status: 500 },\n );\n\nconst invalid = (error: unknown, label: string, code: string) =>\n error instanceof EntifixBuildError\n ? HttpServerResponse.json(\n {\n error: label,\n code: refusalCode(error) ?? code,\n detail: error.message,\n },\n { status: 400 },\n )\n : error instanceof EntityRouteRefusal\n ? HttpServerResponse.json(\n {\n error: 'the record cannot be saved',\n code: error.code,\n ...(error.detail === undefined ? {} : { detail: error.detail }),\n },\n { status: error.status },\n )\n : routeFailure(error);\n\n/**\n * A domain error may carry its own code (`mediaNotHttps`), so the client can\n * say which rule failed rather than \"invalid body\".\n */\nconst refusalCode = (error: EntifixBuildError): string | undefined =>\n 'code' in error && typeof error.code === 'string' ? error.code : undefined;\n\n/**\n * The generic routes for one entity — list, read, create, update, delete and\n * its `$metadata` — derived from the entity and bound to storage through the\n * host's {@link EntityRepositoryFactoryTag}.\n *\n * What a route set varies on is stated, not written: which verbs exist, who may\n * reach them, which records a reader sees, and what a save must pass. What it\n * never varies on is the wire: the load request is parsed against the entity's\n * own metadata (so a query can only name a member the entity declared\n * filterable or sortable), every answer is an envelope, and the links name\n * exactly the verbs mounted — never one that would `404`.\n *\n * A path that is not one of these verbs is written by hand beside it, and a\n * verb whose generic form does not fit is replaced with `override`.\n */\nexport const makeEntityRoutes = <\n T extends Entity,\n R = never,\n const G extends EntityRouteGuard = EntityRouteGuard,\n>(\n entityConstructor: EntityConstructor<T>,\n options: EntityRoutesOptions<T, R, G>,\n) => {\n const key = envelopeEntityName(entityConstructor);\n const base: `/${string}` = options.basePath ?? `/api/${key}`;\n const mounted = new Set(options.verbs);\n\n const links: EntityRouteLinks = {\n record: id => {\n const href = `${base}/${String(id)}`;\n return [\n ...(mounted.has('get')\n ? [{ rel: 'self', href, method: 'GET' } as const]\n : []),\n ...(mounted.has('list')\n ? [{ rel: 'list', href: base, method: 'GET' } as const]\n : []),\n ...(mounted.has('update')\n ? [{ rel: 'update', href, method: 'PUT' } as const]\n : []),\n ...(mounted.has('delete')\n ? [{ rel: 'delete', href, method: 'DELETE' } as const]\n : []),\n ];\n },\n collection: () => [\n ...(mounted.has('list')\n ? [{ rel: 'self', href: base, method: 'GET' } as const]\n : []),\n ...(mounted.has('create')\n ? [{ rel: 'create', href: base, method: 'POST' } as const]\n : []),\n ],\n };\n\n const guardFor = (verb: EntityVerb): EntityRouteGuard => {\n const guard = options.guard as\n | EntityRouteGuard\n | { readonly read: EntityRouteGuard; readonly write: EntityRouteGuard };\n return 'kind' in guard ? guard : READS.has(verb) ? guard.read : guard.write;\n };\n\n const guarded = <A, E, RR>(\n verb: EntityVerb,\n use: (context: EntityRouteContext) => Effect.Effect<A, E, RR>,\n ) => {\n const guard = guardFor(verb);\n const permission = permissionForEntity(entityConstructor, ACTION[verb]);\n switch (guard.kind) {\n case 'public':\n return use({});\n case 'permission':\n return requirePermission(permission)(principal => use({ principal }));\n case 'organization':\n return requireOrganization(permission)((organizationId, principal) =>\n use({ organizationId, principal }),\n );\n case 'crossing':\n return guard.organization\n ? requireServiceCrossing(permission)(organizationId =>\n use({ organizationId }),\n )\n : requireCrossing(permission)(use({}));\n }\n };\n\n const repository = (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const factory = yield* EntityRepositoryFactoryTag;\n return yield* factory.for(entityConstructor, {\n ...(context.organizationId === undefined\n ? {}\n : { organizationId: context.organizationId }),\n });\n });\n\n const scopeOf = (context: EntityRouteContext) =>\n options.scope ? options.scope(context) : Effect.succeed('all' as const);\n\n const readLoadRequest = Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n const search = new URL(request.url, 'http://localhost').searchParams;\n return yield* Effect.try({\n try: () =>\n parseLoadRequestParams(\n entityConstructor,\n search,\n ) as unknown as EntityLoadRequest<T>,\n catch: error => error as EntifixBuildError,\n });\n });\n\n const notFound = HttpServerResponse.json(\n {\n message: 'not found',\n code: 'notFound',\n entity: extractMetaEntity(entityConstructor).key,\n },\n { status: 404 },\n );\n\n const list = (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const parsed = yield* readLoadRequest;\n const scope = yield* scopeOf(context);\n if (scope === 'none') {\n // Still parsed: whether a query is well-formed must not depend on who\n // asked.\n return yield* HttpServerResponse.json(\n makeEntityPageEnvelope(\n entityConstructor,\n { items: [], total: 0, request: parsed },\n links.collection(),\n ),\n );\n }\n // Conjoined, never substituted: the translator `$and`s every top-level\n // element, so an added element can only narrow, and no `or` in a query\n // string can escape it.\n const request: EntityLoadRequest<T> =\n scope === 'all'\n ? parsed\n : {\n ...parsed,\n filtering: [...(parsed.filtering ?? []), scope.filter],\n };\n const page = yield* loadUCFactory<T>().pipe(\n Effect.provideService(EntityRepositoryTag, yield* repository(context)),\n Effect.provideService(\n EntityLoadRequestTag,\n request as unknown as EntityLoadRequest,\n ),\n );\n return yield* HttpServerResponse.json(\n makeEntityPageEnvelope(entityConstructor, page, links.collection()),\n );\n }).pipe(\n Effect.catch(error => invalid(error, 'invalid query', 'invalidQuery')),\n );\n\n const get = (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const params = yield* HttpRouter.params;\n const scope = yield* scopeOf(context);\n if (scope === 'none') return yield* notFound;\n const found = yield* getUCFactory<T>().pipe(\n Effect.provideService(EntityRepositoryTag, yield* repository(context)),\n Effect.provideService(EntityIdTag, params.id),\n Effect.option,\n );\n // Out of scope answers exactly as absent does: a `403` would confirm the\n // record exists to somebody who may not read it.\n if (\n found._tag === 'None' ||\n (scope !== 'all' && !scope.includes(found.value))\n ) {\n return yield* notFound;\n }\n return yield* HttpServerResponse.json(\n makeEntityEnvelope(\n entityConstructor,\n found.value,\n links.record(params.id),\n ),\n );\n }).pipe(Effect.catch(routeFailure));\n\n const save = (verb: 'create' | 'update') => (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n const body = yield* request.json;\n let entity = yield* readEntityEnvelope(entityConstructor, body);\n\n // On update the URL is authoritative, so a record cannot be renamed\n // onto another one by editing its payload.\n if (verb === 'update') {\n const params = yield* HttpRouter.params;\n entity.id = params.id;\n }\n if (options.prepare) {\n entity = yield* options.prepare(entity, { ...context, verb });\n }\n\n const saved = yield* saveUCFactory<T>().pipe(\n Effect.provideService(EntityRepositoryTag, yield* repository(context)),\n Effect.provideService(EntityTag, entity),\n );\n return yield* HttpServerResponse.json(\n makeEntityEnvelope(entityConstructor, saved, links.record(saved.id)),\n );\n }).pipe(\n Effect.catch(error =>\n invalid(error, 'invalid request body', 'invalidBody'),\n ),\n );\n\n /**\n * Answers with an envelope rather than a bare `204`: every message between\n * entifix artifacts is one, and the fetch client parses every response as\n * JSON.\n */\n const remove = (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const params = yield* HttpRouter.params;\n yield* deleteUCFactory<T>().pipe(\n Effect.provideService(EntityRepositoryTag, yield* repository(context)),\n Effect.provideService(EntityIdTag, params.id),\n );\n return yield* HttpServerResponse.json({\n meta: { type: 'entity', entity: key, links: links.collection() },\n data: { id: params.id },\n });\n }).pipe(Effect.catch(routeFailure));\n\n const generic = (verb: EntityVerb, context: EntityRouteContext) => {\n switch (verb) {\n case 'list':\n return list(context);\n case 'get':\n return get(context);\n case 'create':\n return save('create')(context);\n case 'update':\n return save('update')(context);\n case 'delete':\n return remove(context);\n }\n };\n\n /**\n * Each guard asks for different services, so the branches do not unify on\n * their own; the cast states the union every guard this set can reach\n * actually needs.\n */\n const handlerFor = (verb: EntityVerb) =>\n guarded(verb, context => {\n const override = options.override?.[verb];\n return override ? override(context, links) : generic(verb, context);\n }) as Effect.Effect<\n HttpServerResponse.HttpServerResponse,\n unknown,\n | R\n | EntityRepositoryFactoryTag\n | GuardServices<G>\n | HttpServerRequest.HttpServerRequest\n | HttpRouter.RouteContext\n >;\n\n const ROUTES: Record<\n EntityVerb,\n { method: 'GET' | 'POST' | 'PUT' | 'DELETE'; path: `/${string}` }\n > = {\n list: { method: 'GET', path: base },\n get: { method: 'GET', path: `${base}/:id` },\n create: { method: 'POST', path: base },\n update: { method: 'PUT', path: `${base}/:id` },\n delete: { method: 'DELETE', path: `${base}/:id` },\n };\n\n return HttpRouter.addAll([\n // A literal, so it wins over `/:id` regardless of order.\n HttpRouter.route(\n 'GET',\n `${base}/$metadata`,\n entityMetadataRoute(entityConstructor),\n ),\n ...options.verbs.map(verb =>\n HttpRouter.route(\n ROUTES[verb].method,\n ROUTES[verb].path,\n handlerFor(verb),\n ),\n ),\n ]);\n};\n"],"names":["permissionForEntity","deleteUCFactory","EntityIdTag","EntityLoadRequestTag","EntityRepositoryFactoryTag","EntityRepositoryTag","EntityTag","getUCFactory","loadUCFactory","saveUCFactory","EntifixBuildError","envelopeEntityName","extractMetaEntity","makeEntityEnvelope","makeEntityPageEnvelope","parseLoadRequestParams","readEntityEnvelope","Effect","HttpRouter","HttpServerRequest","HttpServerResponse","requireCrossing","requireOrganization","requirePermission","requireServiceCrossing","entityMetadataRoute","EntityRouteRefusal","status","code","detail","_tag","ACTION","list","get","create","update","delete","READS","Set","routeFailure","error","json","String","invalid","label","refusalCode","message","undefined","makeEntityRoutes","entityConstructor","options","key","base","basePath","mounted","verbs","links","record","id","href","has","rel","method","collection","guardFor","verb","guard","read","write","guarded","use","permission","kind","principal","organizationId","organization","repository","context","gen","factory","for","scopeOf","scope","succeed","readLoadRequest","request","search","URL","url","searchParams","try","catch","notFound","entity","parsed","items","total","filtering","filter","page","pipe","provideService","params","found","option","includes","value","save","body","prepare","saved","remove","meta","type","data","generic","handlerFor","override","ROUTES","path","addAll","route","map"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/entity-routes.ts"],"sourcesContent":["import type { Action } from '@entifix/authz';\nimport {\n deleteUCFactory,\n EntityIdTag,\n EntityLoadRequestTag,\n EntityRepositoryFactoryTag,\n EntityRepositoryTag,\n EntityTag,\n getUCFactory,\n loadUCFactory,\n saveUCFactory,\n} from '@entifix/business';\nimport {\n EntifixBuildError,\n type EntifixEnvelopeLink,\n type Entity,\n type EntityConstructor,\n type EntityId,\n type EntityLoadRequest,\n envelopeEntityName,\n extractMetaEntity,\n makeEntityEnvelope,\n makeEntityPageEnvelope,\n parseLoadRequestParams,\n readEntityEnvelope,\n} from '@entifix/core';\nimport { Effect } from 'effect';\nimport { HttpRouter, HttpServerRequest, HttpServerResponse } from 'effect/http';\n\nimport { entityMetadataRoute } from './entity-metadata-route.js';\nimport {\n type EntityReadScope,\n type EntityRouteContext,\n type EntityRouteGuard,\n guardRoute,\n type GuardServices,\n} from './entity-route-guard.js';\nimport {\n EntityRouteRefusal,\n invalid,\n routeFailure,\n} from './route-responses.js';\n\n/** The five generic verbs a route set can mount. */\nexport type EntityVerb = 'list' | 'get' | 'create' | 'update' | 'delete';\n\nexport {\n type EntityReadScope,\n type EntityRouteContext,\n type EntityRouteGuard,\n} from './entity-route-guard.js';\nexport { EntityRouteRefusal } from './route-responses.js';\n\n/** A hand-written handler for one verb, mounted under the derived guard. */\nexport type EntityRouteOverride<R> = (\n context: EntityRouteContext,\n links: EntityRouteLinks,\n) => Effect.Effect<HttpServerResponse.HttpServerResponse, unknown, R>;\n\n/** The affordances a route set derives from the verbs it mounts. */\nexport interface EntityRouteLinks {\n readonly record: (id: EntityId) => EntifixEnvelopeLink[];\n readonly collection: () => EntifixEnvelopeLink[];\n}\n\nexport interface EntityRoutesOptions<\n T extends Entity,\n R,\n G extends EntityRouteGuard = EntityRouteGuard,\n> {\n readonly verbs: readonly EntityVerb[];\n /** One guard for every verb, or one for the reads and one for the writes. */\n readonly guard: G | { readonly read: G; readonly write: G };\n /** Defaults to `/api/<entity key>`. */\n readonly basePath?: `/${string}`;\n /** Narrows the reads to what this caller may see. */\n readonly scope?: (\n context: EntityRouteContext,\n ) => Effect.Effect<EntityReadScope<T>, never, R>;\n /**\n * Runs on every create and update, after the body is read and before the\n * save — and may change what is saved, or refuse it.\n */\n readonly prepare?: (\n entity: T,\n context: EntityRouteContext & { readonly verb: 'create' | 'update' },\n ) => Effect.Effect<T, EntityRouteRefusal | EntifixBuildError, R>;\n /**\n * A hand-written handler in place of the generic one for a verb. It keeps\n * the derived guard and receives the derived links, so the escape hatch\n * cannot drift from the rest of the set.\n */\n readonly override?: Partial<Record<EntityVerb, EntityRouteOverride<R>>>;\n}\n\nconst ACTION: Record<EntityVerb, Action> = {\n list: 'read',\n get: 'read',\n create: 'write',\n update: 'write',\n delete: 'delete',\n};\n\nconst READS: ReadonlySet<EntityVerb> = new Set(['list', 'get']);\n\n/**\n * The generic routes for one entity — list, read, create, update, delete and\n * its `$metadata` — derived from the entity and bound to storage through the\n * host's {@link EntityRepositoryFactoryTag}.\n *\n * What a route set varies on is stated, not written: which verbs exist, who may\n * reach them, which records a reader sees, and what a save must pass. What it\n * never varies on is the wire: the load request is parsed against the entity's\n * own metadata (so a query can only name a member the entity declared\n * filterable or sortable), every answer is an envelope, and the links name\n * exactly the verbs mounted — never one that would `404`.\n *\n * A path that is not one of these verbs is written by hand beside it, and a\n * verb whose generic form does not fit is replaced with `override`.\n */\nconst deriveEntityRoutes = <\n T extends Entity,\n R = never,\n // `never`, not the whole union: TypeScript infers no type argument once a\n // caller names one, so `makeEntityRoutes<T, R>(…)` used to default `G` to\n // every guard. Then the crossing tags became requirements of a route that\n // guards on a session, and the error surfaced at the host's `Layer.provide`\n // instead of here. Defaulting to `never` refuses that call at the guard\n // itself; `makeEntityRoutes.for<R>()` is the way to name `R` alone.\n const G extends EntityRouteGuard = never,\n>(\n entityConstructor: EntityConstructor<T>,\n options: EntityRoutesOptions<T, R, G>,\n) => {\n const key = envelopeEntityName(entityConstructor);\n const base: `/${string}` = options.basePath ?? `/api/${key}`;\n const mounted = new Set(options.verbs);\n\n const links: EntityRouteLinks = {\n record: id => {\n const href = `${base}/${String(id)}`;\n return [\n ...(mounted.has('get')\n ? [{ rel: 'self', href, method: 'GET' } as const]\n : []),\n ...(mounted.has('list')\n ? [{ rel: 'list', href: base, method: 'GET' } as const]\n : []),\n ...(mounted.has('update')\n ? [{ rel: 'update', href, method: 'PUT' } as const]\n : []),\n ...(mounted.has('delete')\n ? [{ rel: 'delete', href, method: 'DELETE' } as const]\n : []),\n ];\n },\n collection: () => [\n ...(mounted.has('list')\n ? [{ rel: 'self', href: base, method: 'GET' } as const]\n : []),\n ...(mounted.has('create')\n ? [{ rel: 'create', href: base, method: 'POST' } as const]\n : []),\n ],\n };\n\n const guardFor = (verb: EntityVerb): EntityRouteGuard => {\n const guard = options.guard as\n | EntityRouteGuard\n | { readonly read: EntityRouteGuard; readonly write: EntityRouteGuard };\n return 'kind' in guard ? guard : READS.has(verb) ? guard.read : guard.write;\n };\n\n const guarded = <A, E, RR>(\n verb: EntityVerb,\n use: (context: EntityRouteContext) => Effect.Effect<A, E, RR>,\n ) => guardRoute(entityConstructor, guardFor(verb), ACTION[verb], use);\n\n const repository = (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const factory = yield* EntityRepositoryFactoryTag;\n return yield* factory.for(entityConstructor, {\n ...(context.organizationId === undefined\n ? {}\n : { organizationId: context.organizationId }),\n });\n });\n\n const scopeOf = (context: EntityRouteContext) =>\n options.scope ? options.scope(context) : Effect.succeed('all' as const);\n\n const readLoadRequest = Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n const search = new URL(request.url, 'http://localhost').searchParams;\n return yield* Effect.try({\n try: () =>\n parseLoadRequestParams(\n entityConstructor,\n search,\n ) as unknown as EntityLoadRequest<T>,\n catch: error => error as EntifixBuildError,\n });\n });\n\n const notFound = HttpServerResponse.json(\n {\n message: 'not found',\n code: 'notFound',\n entity: extractMetaEntity(entityConstructor).key,\n },\n { status: 404 },\n );\n\n const list = (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const parsed = yield* readLoadRequest;\n const scope = yield* scopeOf(context);\n if (scope === 'none') {\n // Still parsed: whether a query is well-formed must not depend on who\n // asked.\n return yield* HttpServerResponse.json(\n makeEntityPageEnvelope(\n entityConstructor,\n { items: [], total: 0, request: parsed },\n links.collection(),\n ),\n );\n }\n // Conjoined, never substituted: the translator `$and`s every top-level\n // element, so an added element can only narrow, and no `or` in a query\n // string can escape it.\n const request: EntityLoadRequest<T> =\n scope === 'all'\n ? parsed\n : {\n ...parsed,\n filtering: [...(parsed.filtering ?? []), scope.filter],\n };\n const page = yield* loadUCFactory<T>().pipe(\n Effect.provideService(EntityRepositoryTag, yield* repository(context)),\n Effect.provideService(\n EntityLoadRequestTag,\n request as unknown as EntityLoadRequest,\n ),\n );\n return yield* HttpServerResponse.json(\n makeEntityPageEnvelope(entityConstructor, page, links.collection()),\n );\n }).pipe(\n Effect.catch(error => invalid(error, 'invalid query', 'invalidQuery')),\n );\n\n const get = (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const params = yield* HttpRouter.params;\n const scope = yield* scopeOf(context);\n if (scope === 'none') return yield* notFound;\n const found = yield* getUCFactory<T>().pipe(\n Effect.provideService(EntityRepositoryTag, yield* repository(context)),\n Effect.provideService(EntityIdTag, params.id),\n Effect.option,\n );\n // Out of scope answers exactly as absent does: a `403` would confirm the\n // record exists to somebody who may not read it.\n if (\n found._tag === 'None' ||\n (scope !== 'all' && !scope.includes(found.value))\n ) {\n return yield* notFound;\n }\n return yield* HttpServerResponse.json(\n makeEntityEnvelope(\n entityConstructor,\n found.value,\n links.record(params.id),\n ),\n );\n }).pipe(Effect.catch(routeFailure));\n\n const save = (verb: 'create' | 'update') => (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n const body = yield* request.json;\n let entity = yield* readEntityEnvelope(entityConstructor, body);\n\n // On update the URL is authoritative, so a record cannot be renamed\n // onto another one by editing its payload.\n if (verb === 'update') {\n const params = yield* HttpRouter.params;\n entity.id = params.id;\n }\n if (options.prepare) {\n entity = yield* options.prepare(entity, { ...context, verb });\n }\n\n const saved = yield* saveUCFactory<T>().pipe(\n Effect.provideService(EntityRepositoryTag, yield* repository(context)),\n Effect.provideService(EntityTag, entity),\n );\n return yield* HttpServerResponse.json(\n makeEntityEnvelope(entityConstructor, saved, links.record(saved.id)),\n );\n }).pipe(\n Effect.catch(error =>\n invalid(error, 'invalid request body', 'invalidBody'),\n ),\n );\n\n /**\n * Answers with an envelope rather than a bare `204`: every message between\n * entifix artifacts is one, and the fetch client parses every response as\n * JSON.\n */\n const remove = (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const params = yield* HttpRouter.params;\n yield* deleteUCFactory<T>().pipe(\n Effect.provideService(EntityRepositoryTag, yield* repository(context)),\n Effect.provideService(EntityIdTag, params.id),\n );\n return yield* HttpServerResponse.json({\n meta: { type: 'entity', entity: key, links: links.collection() },\n data: { id: params.id },\n });\n }).pipe(Effect.catch(routeFailure));\n\n const generic = (verb: EntityVerb, context: EntityRouteContext) => {\n switch (verb) {\n case 'list':\n return list(context);\n case 'get':\n return get(context);\n case 'create':\n return save('create')(context);\n case 'update':\n return save('update')(context);\n case 'delete':\n return remove(context);\n }\n };\n\n /**\n * Each guard asks for different services, so the branches do not unify on\n * their own; the cast states the union every guard this set can reach\n * actually needs.\n */\n const handlerFor = (verb: EntityVerb) =>\n guarded(verb, context => {\n const override = options.override?.[verb];\n return override ? override(context, links) : generic(verb, context);\n }) as Effect.Effect<\n HttpServerResponse.HttpServerResponse,\n unknown,\n | R\n | EntityRepositoryFactoryTag\n | GuardServices<G>\n | HttpServerRequest.HttpServerRequest\n | HttpRouter.RouteContext\n >;\n\n const ROUTES: Record<\n EntityVerb,\n { method: 'GET' | 'POST' | 'PUT' | 'DELETE'; path: `/${string}` }\n > = {\n list: { method: 'GET', path: base },\n get: { method: 'GET', path: `${base}/:id` },\n create: { method: 'POST', path: base },\n update: { method: 'PUT', path: `${base}/:id` },\n delete: { method: 'DELETE', path: `${base}/:id` },\n };\n\n return HttpRouter.addAll([\n // A literal, so it wins over `/:id` regardless of order.\n HttpRouter.route(\n 'GET',\n `${base}/$metadata`,\n entityMetadataRoute(entityConstructor),\n ),\n ...options.verbs.map(verb =>\n HttpRouter.route(\n ROUTES[verb].method,\n ROUTES[verb].path,\n handlerFor(verb),\n ),\n ),\n ]);\n};\n\n/**\n * {@link deriveEntityRoutes}, with a curried form for a caller that has to\n * name the services its `override`, `scope` or `prepare` handlers need:\n * `makeEntityRoutes.for<R>()(Entity, options)` names `R` and still infers the\n * entity and the guard from the value, which a single call with explicit type\n * arguments cannot do.\n */\nexport { routeFailure };\n\nexport const makeEntityRoutes = Object.assign(deriveEntityRoutes, {\n for:\n <R>() =>\n <T extends Entity, const G extends EntityRouteGuard>(\n entityConstructor: EntityConstructor<T>,\n options: EntityRoutesOptions<T, R, G>,\n ) =>\n deriveEntityRoutes<T, R, G>(entityConstructor, options),\n});\n"],"names":["deleteUCFactory","EntityIdTag","EntityLoadRequestTag","EntityRepositoryFactoryTag","EntityRepositoryTag","EntityTag","getUCFactory","loadUCFactory","saveUCFactory","envelopeEntityName","extractMetaEntity","makeEntityEnvelope","makeEntityPageEnvelope","parseLoadRequestParams","readEntityEnvelope","Effect","HttpRouter","HttpServerRequest","HttpServerResponse","entityMetadataRoute","guardRoute","invalid","routeFailure","EntityRouteRefusal","ACTION","list","get","create","update","delete","READS","Set","deriveEntityRoutes","entityConstructor","options","key","base","basePath","mounted","verbs","links","record","id","href","String","has","rel","method","collection","guardFor","verb","guard","read","write","guarded","use","repository","context","gen","factory","for","organizationId","undefined","scopeOf","scope","succeed","readLoadRequest","request","search","URL","url","searchParams","try","catch","error","notFound","json","message","code","entity","status","parsed","items","total","filtering","filter","page","pipe","provideService","params","found","option","_tag","includes","value","save","body","prepare","saved","remove","meta","type","data","generic","handlerFor","override","ROUTES","path","addAll","route","map","makeEntityRoutes","Object","assign"],"mappings":";AACA,SACEA,eAAe,EACfC,WAAW,EACXC,oBAAoB,EACpBC,0BAA0B,EAC1BC,mBAAmB,EACnBC,SAAS,EACTC,YAAY,EACZC,aAAa,EACbC,aAAa,QACR,oBAAoB;AAC3B,SAOEC,kBAAkB,EAClBC,iBAAiB,EACjBC,kBAAkB,EAClBC,sBAAsB,EACtBC,sBAAsB,EACtBC,kBAAkB,QACb,gBAAgB;AACvB,SAASC,MAAM,QAAQ,SAAS;AAChC,SAASC,UAAU,EAAEC,iBAAiB,EAAEC,kBAAkB,QAAQ,cAAc;AAEhF,SAASC,mBAAmB,QAAQ,6BAA6B;AACjE,SAIEC,UAAU,QAEL,0BAA0B;AACjC,SAEEC,OAAO,EACPC,YAAY,QACP,uBAAuB;AAU9B,SAASC,kBAAkB,QAAQ,uBAAuB;AA4C1D,MAAMC,SAAqC;IACzCC,MAAM;IACNC,KAAK;IACLC,QAAQ;IACRC,QAAQ;IACRC,QAAQ;AACV;AAEA,MAAMC,QAAiC,IAAIC,IAAI;IAAC;IAAQ;CAAM;AAE9D;;;;;;;;;;;;;;CAcC,GACD,MAAMC,qBAAqB,CAWzBC,mBACAC;QAG2BA;IAD3B,MAAMC,MAAM1B,mBAAmBwB;IAC/B,MAAMG,QAAqBF,oBAAAA,QAAQG,QAAQ,YAAhBH,oBAAoB,CAAC,KAAK,EAAEC,KAAK;IAC5D,MAAMG,UAAU,IAAIP,IAAIG,QAAQK,KAAK;IAErC,MAAMC,QAA0B;QAC9BC,QAAQC,CAAAA;YACN,MAAMC,OAAO,GAAGP,KAAK,CAAC,EAAEQ,OAAOF,KAAK;YACpC,OAAO;mBACDJ,QAAQO,GAAG,CAAC,SACZ;oBAAC;wBAAEC,KAAK;wBAAQH;wBAAMI,QAAQ;oBAAM;iBAAW,GAC/C,EAAE;mBACFT,QAAQO,GAAG,CAAC,UACZ;oBAAC;wBAAEC,KAAK;wBAAQH,MAAMP;wBAAMW,QAAQ;oBAAM;iBAAW,GACrD,EAAE;mBACFT,QAAQO,GAAG,CAAC,YACZ;oBAAC;wBAAEC,KAAK;wBAAUH;wBAAMI,QAAQ;oBAAM;iBAAW,GACjD,EAAE;mBACFT,QAAQO,GAAG,CAAC,YACZ;oBAAC;wBAAEC,KAAK;wBAAUH;wBAAMI,QAAQ;oBAAS;iBAAW,GACpD,EAAE;aACP;QACH;QACAC,YAAY,IAAM;mBACZV,QAAQO,GAAG,CAAC,UACZ;oBAAC;wBAAEC,KAAK;wBAAQH,MAAMP;wBAAMW,QAAQ;oBAAM;iBAAW,GACrD,EAAE;mBACFT,QAAQO,GAAG,CAAC,YACZ;oBAAC;wBAAEC,KAAK;wBAAUH,MAAMP;wBAAMW,QAAQ;oBAAO;iBAAW,GACxD,EAAE;aACP;IACH;IAEA,MAAME,WAAW,CAACC;QAChB,MAAMC,QAAQjB,QAAQiB,KAAK;QAG3B,OAAO,UAAUA,QAAQA,QAAQrB,MAAMe,GAAG,CAACK,QAAQC,MAAMC,IAAI,GAAGD,MAAME,KAAK;IAC7E;IAEA,MAAMC,UAAU,CACdJ,MACAK,MACGnC,WAAWa,mBAAmBgB,SAASC,OAAO1B,MAAM,CAAC0B,KAAK,EAAEK;IAEjE,MAAMC,aAAa,CAACC,UAClB1C,OAAO2C,GAAG,CAAC;YACT,MAAMC,UAAU,OAAOxD;YACvB,OAAO,OAAOwD,QAAQC,GAAG,CAAC3B,mBAAmB,aACvCwB,QAAQI,cAAc,KAAKC,YAC3B,CAAC,IACD;gBAAED,gBAAgBJ,QAAQI,cAAc;YAAC;QAEjD;IAEF,MAAME,UAAU,CAACN,UACfvB,QAAQ8B,KAAK,GAAG9B,QAAQ8B,KAAK,CAACP,WAAW1C,OAAOkD,OAAO,CAAC;IAE1D,MAAMC,kBAAkBnD,OAAO2C,GAAG,CAAC;QACjC,MAAMS,UAAU,OAAOlD,kBAAkBA,iBAAiB;QAC1D,MAAMmD,SAAS,IAAIC,IAAIF,QAAQG,GAAG,EAAE,oBAAoBC,YAAY;QACpE,OAAO,OAAOxD,OAAOyD,GAAG,CAAC;YACvBA,KAAK,IACH3D,uBACEoB,mBACAmC;YAEJK,OAAOC,CAAAA,QAASA;QAClB;IACF;IAEA,MAAMC,WAAWzD,mBAAmB0D,IAAI,CACtC;QACEC,SAAS;QACTC,MAAM;QACNC,QAAQrE,kBAAkBuB,mBAAmBE,GAAG;IAClD,GACA;QAAE6C,QAAQ;IAAI;IAGhB,MAAMvD,OAAO,CAACgC,UACZ1C,OAAO2C,GAAG,CAAC;gBAsBeuB;YArBxB,MAAMA,SAAS,OAAOf;YACtB,MAAMF,QAAQ,OAAOD,QAAQN;YAC7B,IAAIO,UAAU,QAAQ;gBACpB,sEAAsE;gBACtE,SAAS;gBACT,OAAO,OAAO9C,mBAAmB0D,IAAI,CACnChE,uBACEqB,mBACA;oBAAEiD,OAAO,EAAE;oBAAEC,OAAO;oBAAGhB,SAASc;gBAAO,GACvCzC,MAAMQ,UAAU;YAGtB;YACA,uEAAuE;YACvE,uEAAuE;YACvE,wBAAwB;YACxB,MAAMmB,UACJH,UAAU,QACNiB,SACA,aACKA;gBACHG,WAAW;wBAAKH,oBAAAA,OAAOG,SAAS,YAAhBH,oBAAoB,EAAE;oBAAGjB,MAAMqB,MAAM;iBAAC;;YAE9D,MAAMC,OAAO,OAAO/E,gBAAmBgF,IAAI,CACzCxE,OAAOyE,cAAc,CAACpF,qBAAqB,CAAA,OAAOoD,WAAWC,QAAO,IACpE1C,OAAOyE,cAAc,CACnBtF,sBACAiE;YAGJ,OAAO,OAAOjD,mBAAmB0D,IAAI,CACnChE,uBAAuBqB,mBAAmBqD,MAAM9C,MAAMQ,UAAU;QAEpE,GAAGuC,IAAI,CACLxE,OAAO0D,KAAK,CAACC,CAAAA,QAASrD,QAAQqD,OAAO,iBAAiB;IAG1D,MAAMhD,MAAM,CAAC+B,UACX1C,OAAO2C,GAAG,CAAC;YACT,MAAM+B,SAAS,OAAOzE,WAAWyE,MAAM;YACvC,MAAMzB,QAAQ,OAAOD,QAAQN;YAC7B,IAAIO,UAAU,QAAQ,OAAO,OAAOW;YACpC,MAAMe,QAAQ,OAAOpF,eAAkBiF,IAAI,CACzCxE,OAAOyE,cAAc,CAACpF,qBAAqB,CAAA,OAAOoD,WAAWC,QAAO,IACpE1C,OAAOyE,cAAc,CAACvF,aAAawF,OAAO/C,EAAE,GAC5C3B,OAAO4E,MAAM;YAEf,yEAAyE;YACzE,iDAAiD;YACjD,IACED,MAAME,IAAI,KAAK,UACd5B,UAAU,SAAS,CAACA,MAAM6B,QAAQ,CAACH,MAAMI,KAAK,GAC/C;gBACA,OAAO,OAAOnB;YAChB;YACA,OAAO,OAAOzD,mBAAmB0D,IAAI,CACnCjE,mBACEsB,mBACAyD,MAAMI,KAAK,EACXtD,MAAMC,MAAM,CAACgD,OAAO/C,EAAE;QAG5B,GAAG6C,IAAI,CAACxE,OAAO0D,KAAK,CAACnD;IAEvB,MAAMyE,OAAO,CAAC7C,OAA8B,CAACO,UAC3C1C,OAAO2C,GAAG,CAAC;gBACT,MAAMS,UAAU,OAAOlD,kBAAkBA,iBAAiB;gBAC1D,MAAM+E,OAAO,OAAO7B,QAAQS,IAAI;gBAChC,IAAIG,SAAS,OAAOjE,mBAAmBmB,mBAAmB+D;gBAE1D,oEAAoE;gBACpE,2CAA2C;gBAC3C,IAAI9C,SAAS,UAAU;oBACrB,MAAMuC,SAAS,OAAOzE,WAAWyE,MAAM;oBACvCV,OAAOrC,EAAE,GAAG+C,OAAO/C,EAAE;gBACvB;gBACA,IAAIR,QAAQ+D,OAAO,EAAE;oBACnBlB,SAAS,OAAO7C,QAAQ+D,OAAO,CAAClB,QAAQ,aAAKtB;wBAASP;;gBACxD;gBAEA,MAAMgD,QAAQ,OAAO1F,gBAAmB+E,IAAI,CAC1CxE,OAAOyE,cAAc,CAACpF,qBAAqB,CAAA,OAAOoD,WAAWC,QAAO,IACpE1C,OAAOyE,cAAc,CAACnF,WAAW0E;gBAEnC,OAAO,OAAO7D,mBAAmB0D,IAAI,CACnCjE,mBAAmBsB,mBAAmBiE,OAAO1D,MAAMC,MAAM,CAACyD,MAAMxD,EAAE;YAEtE,GAAG6C,IAAI,CACLxE,OAAO0D,KAAK,CAACC,CAAAA,QACXrD,QAAQqD,OAAO,wBAAwB;IAI7C;;;;GAIC,GACD,MAAMyB,SAAS,CAAC1C,UACd1C,OAAO2C,GAAG,CAAC;YACT,MAAM+B,SAAS,OAAOzE,WAAWyE,MAAM;YACvC,OAAOzF,kBAAqBuF,IAAI,CAC9BxE,OAAOyE,cAAc,CAACpF,qBAAqB,CAAA,OAAOoD,WAAWC,QAAO,IACpE1C,OAAOyE,cAAc,CAACvF,aAAawF,OAAO/C,EAAE;YAE9C,OAAO,OAAOxB,mBAAmB0D,IAAI,CAAC;gBACpCwB,MAAM;oBAAEC,MAAM;oBAAUtB,QAAQ5C;oBAAKK,OAAOA,MAAMQ,UAAU;gBAAG;gBAC/DsD,MAAM;oBAAE5D,IAAI+C,OAAO/C,EAAE;gBAAC;YACxB;QACF,GAAG6C,IAAI,CAACxE,OAAO0D,KAAK,CAACnD;IAEvB,MAAMiF,UAAU,CAACrD,MAAkBO;QACjC,OAAQP;YACN,KAAK;gBACH,OAAOzB,KAAKgC;YACd,KAAK;gBACH,OAAO/B,IAAI+B;YACb,KAAK;gBACH,OAAOsC,KAAK,UAAUtC;YACxB,KAAK;gBACH,OAAOsC,KAAK,UAAUtC;YACxB,KAAK;gBACH,OAAO0C,OAAO1C;QAClB;IACF;IAEA;;;;GAIC,GACD,MAAM+C,aAAa,CAACtD,OAClBI,QAAQJ,MAAMO,CAAAA;gBACKvB;YAAjB,MAAMuE,YAAWvE,oBAAAA,QAAQuE,QAAQ,qBAAhBvE,iBAAkB,CAACgB,KAAK;YACzC,OAAOuD,WAAWA,SAAShD,SAASjB,SAAS+D,QAAQrD,MAAMO;QAC7D;IAUF,MAAMiD,SAGF;QACFjF,MAAM;YAAEsB,QAAQ;YAAO4D,MAAMvE;QAAK;QAClCV,KAAK;YAAEqB,QAAQ;YAAO4D,MAAM,GAAGvE,KAAK,IAAI,CAAC;QAAC;QAC1CT,QAAQ;YAAEoB,QAAQ;YAAQ4D,MAAMvE;QAAK;QACrCR,QAAQ;YAAEmB,QAAQ;YAAO4D,MAAM,GAAGvE,KAAK,IAAI,CAAC;QAAC;QAC7CP,QAAQ;YAAEkB,QAAQ;YAAU4D,MAAM,GAAGvE,KAAK,IAAI,CAAC;QAAC;IAClD;IAEA,OAAOpB,WAAW4F,MAAM,CAAC;QACvB,yDAAyD;QACzD5F,WAAW6F,KAAK,CACd,OACA,GAAGzE,KAAK,UAAU,CAAC,EACnBjB,oBAAoBc;WAEnBC,QAAQK,KAAK,CAACuE,GAAG,CAAC5D,CAAAA,OACnBlC,WAAW6F,KAAK,CACdH,MAAM,CAACxD,KAAK,CAACH,MAAM,EACnB2D,MAAM,CAACxD,KAAK,CAACyD,IAAI,EACjBH,WAAWtD;KAGhB;AACH;AAEA;;;;;;CAMC,GACD,SAAS5B,YAAY,GAAG;AAExB,OAAO,MAAMyF,mBAAmBC,OAAOC,MAAM,CAACjF,oBAAoB;IAChE4B,KACE,IACA,CACE3B,mBACAC,UAEAF,mBAA4BC,mBAAmBC;AACrD,GAAG"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './auth/index.js';
2
+ export * from './entity-facet-routes.js';
2
3
  export * from './entity-metadata-route.js';
3
4
  export * from './entity-routes.js';
4
5
  export * from './health-routes.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,UAAU,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './auth/index.js';
2
+ export * from './entity-facet-routes.js';
2
3
  export * from './entity-metadata-route.js';
3
4
  export * from './entity-routes.js';
4
5
  export * from './health-routes.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './auth/index.js';\nexport * from './entity-metadata-route.js';\nexport * from './entity-routes.js';\nexport * from './health-routes.js';\nexport * from './redact-configuration.js';\nexport * from './service-description-route.js';\nexport * from './sse.js';\n"],"names":[],"mappings":"AAAA,cAAc,kBAAkB;AAChC,cAAc,6BAA6B;AAC3C,cAAc,qBAAqB;AACnC,cAAc,qBAAqB;AACnC,cAAc,4BAA4B;AAC1C,cAAc,iCAAiC;AAC/C,cAAc,WAAW"}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './auth/index.js';\nexport * from './entity-facet-routes.js';\nexport * from './entity-metadata-route.js';\nexport * from './entity-routes.js';\nexport * from './health-routes.js';\nexport * from './redact-configuration.js';\nexport * from './service-description-route.js';\nexport * from './sse.js';\n"],"names":[],"mappings":"AAAA,cAAc,kBAAkB;AAChC,cAAc,2BAA2B;AACzC,cAAc,6BAA6B;AAC3C,cAAc,qBAAqB;AACnC,cAAc,qBAAqB;AACnC,cAAc,4BAA4B;AAC1C,cAAc,iCAAiC;AAC/C,cAAc,WAAW"}
@@ -0,0 +1,22 @@
1
+ import { HttpServerResponse } from 'effect/http';
2
+ /**
3
+ * A save refused by a rule the entity's own deserializer does not make — a
4
+ * closed set, a rule over the other records — answered as `status` with
5
+ * `code`, which is what a client translates.
6
+ */
7
+ export declare class EntityRouteRefusal {
8
+ readonly status: 400 | 409 | 422;
9
+ readonly code: string;
10
+ readonly detail?: string | undefined;
11
+ readonly _tag = "EntityRouteRefusal";
12
+ constructor(status: 400 | 409 | 422, code: string, detail?: string | undefined);
13
+ }
14
+ /** The `500` for anything a route did not anticipate. */
15
+ export declare const routeFailure: (error: unknown) => import("effect/Effect").Effect<HttpServerResponse.HttpServerResponse, import("effect/http/HttpBody").HttpBodyError, never>;
16
+ /**
17
+ * The answer to a request the route refused: a `400` for what the client sent
18
+ * wrong, the refusal's own status for a rule a save broke, and a `500` for
19
+ * anything else.
20
+ */
21
+ export declare const invalid: (error: unknown, label: string, code: string) => import("effect/Effect").Effect<HttpServerResponse.HttpServerResponse, import("effect/http/HttpBody").HttpBodyError, never>;
22
+ //# sourceMappingURL=route-responses.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-responses.d.ts","sourceRoot":"","sources":["../src/route-responses.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD;;;;GAIG;AACH,qBAAa,kBAAkB;IAI3B,QAAQ,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;IAL1B,QAAQ,CAAC,IAAI,wBAAwB;gBAG1B,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,EACvB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,MAAM,YAAA;CAE3B;AAED,yDAAyD;AACzD,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,+HAIxC,CAAC;AASJ;;;;GAIG;AACH,eAAO,MAAM,OAAO,GAAI,OAAO,OAAO,EAAE,OAAO,MAAM,EAAE,MAAM,MAAM,+HAmBxC,CAAC"}
@@ -0,0 +1,49 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { EntifixBuildError } from '@entifix/core';
3
+ import { HttpServerResponse } from 'effect/http';
4
+ /**
5
+ * A save refused by a rule the entity's own deserializer does not make — a
6
+ * closed set, a rule over the other records — answered as `status` with
7
+ * `code`, which is what a client translates.
8
+ */ export class EntityRouteRefusal {
9
+ constructor(status, code, detail){
10
+ this.status = status;
11
+ this.code = code;
12
+ this.detail = detail;
13
+ this._tag = 'EntityRouteRefusal';
14
+ }
15
+ }
16
+ /** The `500` for anything a route did not anticipate. */ export const routeFailure = (error)=>HttpServerResponse.json({
17
+ error: 'request failed',
18
+ code: 'unexpected',
19
+ detail: String(error)
20
+ }, {
21
+ status: 500
22
+ });
23
+ /**
24
+ * A domain error may carry its own code (`mediaNotHttps`), so the client can
25
+ * say which rule failed rather than "invalid body".
26
+ */ const refusalCode = (error)=>'code' in error && typeof error.code === 'string' ? error.code : undefined;
27
+ /**
28
+ * The answer to a request the route refused: a `400` for what the client sent
29
+ * wrong, the refusal's own status for a rule a save broke, and a `500` for
30
+ * anything else.
31
+ */ export const invalid = (error, label, code)=>{
32
+ var _refusalCode;
33
+ return error instanceof EntifixBuildError ? HttpServerResponse.json({
34
+ error: label,
35
+ code: (_refusalCode = refusalCode(error)) != null ? _refusalCode : code,
36
+ detail: error.message
37
+ }, {
38
+ status: 400
39
+ }) : error instanceof EntityRouteRefusal ? HttpServerResponse.json(_extends({
40
+ error: 'the record cannot be saved',
41
+ code: error.code
42
+ }, error.detail === undefined ? {} : {
43
+ detail: error.detail
44
+ }), {
45
+ status: error.status
46
+ }) : routeFailure(error);
47
+ };
48
+
49
+ //# sourceMappingURL=route-responses.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/route-responses.ts"],"sourcesContent":["import { EntifixBuildError } from '@entifix/core';\nimport { HttpServerResponse } from 'effect/http';\n\n/**\n * A save refused by a rule the entity's own deserializer does not make — a\n * closed set, a rule over the other records — answered as `status` with\n * `code`, which is what a client translates.\n */\nexport class EntityRouteRefusal {\n readonly _tag = 'EntityRouteRefusal';\n\n constructor(\n readonly status: 400 | 409 | 422,\n readonly code: string,\n readonly detail?: string,\n ) {}\n}\n\n/** The `500` for anything a route did not anticipate. */\nexport const routeFailure = (error: unknown) =>\n HttpServerResponse.json(\n { error: 'request failed', code: 'unexpected', detail: String(error) },\n { status: 500 },\n );\n\n/**\n * A domain error may carry its own code (`mediaNotHttps`), so the client can\n * say which rule failed rather than \"invalid body\".\n */\nconst refusalCode = (error: EntifixBuildError): string | undefined =>\n 'code' in error && typeof error.code === 'string' ? error.code : undefined;\n\n/**\n * The answer to a request the route refused: a `400` for what the client sent\n * wrong, the refusal's own status for a rule a save broke, and a `500` for\n * anything else.\n */\nexport const invalid = (error: unknown, label: string, code: string) =>\n error instanceof EntifixBuildError\n ? HttpServerResponse.json(\n {\n error: label,\n code: refusalCode(error) ?? code,\n detail: error.message,\n },\n { status: 400 },\n )\n : error instanceof EntityRouteRefusal\n ? HttpServerResponse.json(\n {\n error: 'the record cannot be saved',\n code: error.code,\n ...(error.detail === undefined ? {} : { detail: error.detail }),\n },\n { status: error.status },\n )\n : routeFailure(error);\n"],"names":["EntifixBuildError","HttpServerResponse","EntityRouteRefusal","status","code","detail","_tag","routeFailure","error","json","String","refusalCode","undefined","invalid","label","message"],"mappings":";AAAA,SAASA,iBAAiB,QAAQ,gBAAgB;AAClD,SAASC,kBAAkB,QAAQ,cAAc;AAEjD;;;;CAIC,GACD,OAAO,MAAMC;IAGX,YACE,AAASC,MAAuB,EAChC,AAASC,IAAY,EACrB,AAASC,MAAe,CACxB;aAHSF,SAAAA;aACAC,OAAAA;aACAC,SAAAA;aALFC,OAAO;IAMb;AACL;AAEA,uDAAuD,GACvD,OAAO,MAAMC,eAAe,CAACC,QAC3BP,mBAAmBQ,IAAI,CACrB;QAAED,OAAO;QAAkBJ,MAAM;QAAcC,QAAQK,OAAOF;IAAO,GACrE;QAAEL,QAAQ;IAAI,GACd;AAEJ;;;CAGC,GACD,MAAMQ,cAAc,CAACH,QACnB,UAAUA,SAAS,OAAOA,MAAMJ,IAAI,KAAK,WAAWI,MAAMJ,IAAI,GAAGQ;AAEnE;;;;CAIC,GACD,OAAO,MAAMC,UAAU,CAACL,OAAgBM,OAAeV;QAKvCO;WAJdH,iBAAiBR,oBACbC,mBAAmBQ,IAAI,CACrB;QACED,OAAOM;QACPV,IAAI,GAAEO,eAAAA,YAAYH,kBAAZG,eAAsBP;QAC5BC,QAAQG,MAAMO,OAAO;IACvB,GACA;QAAEZ,QAAQ;IAAI,KAEhBK,iBAAiBN,qBACfD,mBAAmBQ,IAAI,CACrB;QACED,OAAO;QACPJ,MAAMI,MAAMJ,IAAI;OACZI,MAAMH,MAAM,KAAKO,YAAY,CAAC,IAAI;QAAEP,QAAQG,MAAMH,MAAM;IAAC,IAE/D;QAAEF,QAAQK,MAAML,MAAM;IAAC,KAEzBI,aAAaC;EAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entifix/service-shell",
3
- "version": "0.7.2",
3
+ "version": "0.9.0",
4
4
  "description": "Effect HTTP service skeleton: server layer, health and metadata routes, permission guards, remote configuration and OpenTelemetry.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -66,10 +66,10 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "@swc/helpers": "0.5.23",
69
- "@entifix/authz": "0.7.2",
70
- "@entifix/core": "0.7.2",
71
- "@entifix/business": "0.7.2",
72
- "@entifix/tooling": "0.7.2"
69
+ "@entifix/authz": "0.9.0",
70
+ "@entifix/business": "0.9.0",
71
+ "@entifix/core": "0.9.0",
72
+ "@entifix/tooling": "0.9.0"
73
73
  },
74
74
  "devDependencies": {
75
75
  "@effect/opentelemetry": "4.0.5",
@@ -81,7 +81,7 @@
81
81
  "@opentelemetry/sdk-metrics": "2.12.0",
82
82
  "@opentelemetry/sdk-trace-base": "2.12.0",
83
83
  "effect": "4.0.5",
84
- "@entifix/testing-unit": "0.7.2"
84
+ "@entifix/testing-unit": "0.9.0"
85
85
  },
86
86
  "peerDependencies": {
87
87
  "@effect/opentelemetry": "^4.0.5",