@entifix/business 0.9.0 → 0.10.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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/repository/entity-facet-counter.d.ts +4 -3
- package/dist/repository/entity-facet-counter.d.ts.map +1 -1
- package/dist/repository/entity-facet-counter.js.map +1 -1
- package/dist/repository/entity-link-resolver.repository.d.ts.map +1 -1
- package/dist/repository/entity-link-resolver.repository.js +2 -4
- package/dist/repository/entity-link-resolver.repository.js.map +1 -1
- package/dist/repository/entity-repository-factory.d.ts +2 -2
- package/dist/repository/entity-repository-factory.d.ts.map +1 -1
- package/dist/repository/entity-repository-factory.js.map +1 -1
- package/dist/repository/entity.repository.d.ts +12 -5
- package/dist/repository/entity.repository.d.ts.map +1 -1
- package/dist/repository/entity.repository.js.map +1 -1
- package/dist/sessions/one-time-token-store.d.ts +3 -3
- package/dist/sessions/one-time-token-store.d.ts.map +1 -1
- package/dist/sessions/one-time-token-store.js.map +1 -1
- package/dist/sessions/session-store.d.ts +8 -8
- package/dist/sessions/session-store.d.ts.map +1 -1
- package/dist/sessions/session-store.js.map +1 -1
- package/dist/shutdown/shutdown-registry.d.ts +21 -2
- package/dist/shutdown/shutdown-registry.d.ts.map +1 -1
- package/dist/shutdown/shutdown-registry.js +17 -21
- package/dist/shutdown/shutdown-registry.js.map +1 -1
- package/dist/tenancy/tenant-database-resolver.d.ts +2 -2
- package/dist/tenancy/tenant-database-resolver.d.ts.map +1 -1
- package/dist/tenancy/tenant-database-resolver.js.map +1 -1
- package/dist/tenancy/tenant-directory.d.ts +2 -2
- package/dist/tenancy/tenant-directory.d.ts.map +1 -1
- package/dist/tenancy/tenant-directory.js.map +1 -1
- package/dist/tokens/token-failures.d.ts +24 -0
- package/dist/tokens/token-failures.d.ts.map +1 -0
- package/dist/tokens/token-failures.js +28 -0
- package/dist/tokens/token-failures.js.map +1 -0
- package/dist/tokens/token-service.d.ts +4 -3
- package/dist/tokens/token-service.d.ts.map +1 -1
- package/dist/tokens/token-service.js.map +1 -1
- package/dist/use-case/delete/delete.uc.d.ts +1 -1
- package/dist/use-case/delete/delete.uc.d.ts.map +1 -1
- package/dist/use-case/facets/facet-counts.uc.d.ts +1 -1
- package/dist/use-case/facets/facet-counts.uc.d.ts.map +1 -1
- package/dist/use-case/get/get.uc.d.ts +1 -1
- package/dist/use-case/get/get.uc.d.ts.map +1 -1
- package/dist/use-case/load/load-all.uc.d.ts +1 -1
- package/dist/use-case/load/load-all.uc.d.ts.map +1 -1
- package/dist/use-case/load/load.uc.d.ts +1 -1
- package/dist/use-case/load/load.uc.d.ts.map +1 -1
- package/dist/use-case/load/resolve-links.uc.d.ts +2 -2
- package/dist/use-case/load/resolve-links.uc.d.ts.map +1 -1
- package/dist/use-case/load/resolve-links.uc.js.map +1 -1
- package/dist/use-case/save/save.uc.d.ts +1 -1
- package/dist/use-case/save/save.uc.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './sessions/one-time-token-store.js';
|
|
|
9
9
|
export * from './sessions/session-store.js';
|
|
10
10
|
export * from './shutdown/shutdown-registry.js';
|
|
11
11
|
export * from './tenancy/index.js';
|
|
12
|
+
export * from './tokens/token-failures.js';
|
|
12
13
|
export * from './tokens/token-service.js';
|
|
13
14
|
export * from './use-case/delete/index.js';
|
|
14
15
|
export * from './use-case/facets/index.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,6BAA6B,CAAC;AAC5C,cAAc,mCAAmC,CAAC;AAClD,cAAc,mCAAmC,CAAC;AAClD,cAAc,sCAAsC,CAAC;AACrD,cAAc,iDAAiD,CAAC;AAChE,cAAc,0CAA0C,CAAC;AACzD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,oCAAoC,CAAC;AACnD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iCAAiC,CAAC;AAChD,cAAc,oBAAoB,CAAC;AACnC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,6BAA6B,CAAC;AAC5C,cAAc,mCAAmC,CAAC;AAClD,cAAc,mCAAmC,CAAC;AAClD,cAAc,sCAAsC,CAAC;AACrD,cAAc,iDAAiD,CAAC;AAChE,cAAc,0CAA0C,CAAC;AACzD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,oCAAoC,CAAC;AACnD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iCAAiC,CAAC;AAChD,cAAc,oBAAoB,CAAC;AACnC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export * from './sessions/one-time-token-store.js';
|
|
|
9
9
|
export * from './sessions/session-store.js';
|
|
10
10
|
export * from './shutdown/shutdown-registry.js';
|
|
11
11
|
export * from './tenancy/index.js';
|
|
12
|
+
export * from './tokens/token-failures.js';
|
|
12
13
|
export * from './tokens/token-service.js';
|
|
13
14
|
export * from './use-case/delete/index.js';
|
|
14
15
|
export * from './use-case/facets/index.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './health/health-registry.js';\nexport * from './repository/config.repository.js';\nexport * from './repository/entity.repository.js';\nexport * from './repository/entity-facet-counter.js';\nexport * from './repository/entity-link-resolver.repository.js';\nexport * from './repository/entity-mixins.repository.js';\nexport * from './repository/entity-repository-factory.js';\nexport * from './sessions/one-time-token-store.js';\nexport * from './sessions/session-store.js';\nexport * from './shutdown/shutdown-registry.js';\nexport * from './tenancy/index.js';\nexport * from './tokens/token-service.js';\nexport * from './use-case/delete/index.js';\nexport * from './use-case/facets/index.js';\nexport * from './use-case/get/index.js';\nexport * from './use-case/load/index.js';\nexport * from './use-case/save/index.js';\nexport * from './wiring/wiring-registry.js';\n"],"names":[],"mappings":"AAAA,cAAc,8BAA8B;AAC5C,cAAc,oCAAoC;AAClD,cAAc,oCAAoC;AAClD,cAAc,uCAAuC;AACrD,cAAc,kDAAkD;AAChE,cAAc,2CAA2C;AACzD,cAAc,4CAA4C;AAC1D,cAAc,qCAAqC;AACnD,cAAc,8BAA8B;AAC5C,cAAc,kCAAkC;AAChD,cAAc,qBAAqB;AACnC,cAAc,4BAA4B;AAC1C,cAAc,6BAA6B;AAC3C,cAAc,6BAA6B;AAC3C,cAAc,0BAA0B;AACxC,cAAc,2BAA2B;AACzC,cAAc,2BAA2B;AACzC,cAAc,8BAA8B"}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './health/health-registry.js';\nexport * from './repository/config.repository.js';\nexport * from './repository/entity.repository.js';\nexport * from './repository/entity-facet-counter.js';\nexport * from './repository/entity-link-resolver.repository.js';\nexport * from './repository/entity-mixins.repository.js';\nexport * from './repository/entity-repository-factory.js';\nexport * from './sessions/one-time-token-store.js';\nexport * from './sessions/session-store.js';\nexport * from './shutdown/shutdown-registry.js';\nexport * from './tenancy/index.js';\nexport * from './tokens/token-failures.js';\nexport * from './tokens/token-service.js';\nexport * from './use-case/delete/index.js';\nexport * from './use-case/facets/index.js';\nexport * from './use-case/get/index.js';\nexport * from './use-case/load/index.js';\nexport * from './use-case/save/index.js';\nexport * from './wiring/wiring-registry.js';\n"],"names":[],"mappings":"AAAA,cAAc,8BAA8B;AAC5C,cAAc,oCAAoC;AAClD,cAAc,oCAAoC;AAClD,cAAc,uCAAuC;AACrD,cAAc,kDAAkD;AAChE,cAAc,2CAA2C;AACzD,cAAc,4CAA4C;AAC1D,cAAc,qCAAqC;AACnD,cAAc,8BAA8B;AAC5C,cAAc,kCAAkC;AAChD,cAAc,qBAAqB;AACnC,cAAc,6BAA6B;AAC3C,cAAc,4BAA4B;AAC1C,cAAc,6BAA6B;AAC3C,cAAc,6BAA6B;AAC3C,cAAc,0BAA0B;AACxC,cAAc,2BAA2B;AACzC,cAAc,2BAA2B;AACzC,cAAc,8BAA8B"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Entity, EntityConstructor, EntityFacetCounts, EntityFacetQuery, Internal, Invalid, StoreFailure } from '@entifix/core';
|
|
2
2
|
import { Context, type Effect } from 'effect';
|
|
3
|
+
import type { RepositoryFailure } from './entity.repository.js';
|
|
3
4
|
import type { RepositoryScope } from './entity-repository-factory.js';
|
|
4
5
|
/**
|
|
5
6
|
* Counts how many of one entity's records hold each value of a member — what a
|
|
@@ -14,7 +15,7 @@ import type { RepositoryScope } from './entity-repository-factory.js';
|
|
|
14
15
|
* an adapter binds what it needs when it is built.
|
|
15
16
|
*/
|
|
16
17
|
export interface EntityFacetCounter {
|
|
17
|
-
count<TEntity extends Entity>(queries: readonly EntityFacetQuery<TEntity>[]): Effect.Effect<EntityFacetCounts,
|
|
18
|
+
count<TEntity extends Entity>(queries: readonly EntityFacetQuery<TEntity>[]): Effect.Effect<EntityFacetCounts, RepositoryFailure>;
|
|
18
19
|
}
|
|
19
20
|
declare const EntityFacetCounterTag_base: Context.ServiceClass<EntityFacetCounterTag, "EntityFacetCounterTag", EntityFacetCounter>;
|
|
20
21
|
export declare class EntityFacetCounterTag extends EntityFacetCounterTag_base {
|
|
@@ -26,7 +27,7 @@ export declare class EntityFacetCounterTag extends EntityFacetCounterTag_base {
|
|
|
26
27
|
* `organizationId`.
|
|
27
28
|
*/
|
|
28
29
|
export interface EntityFacetCounterFactory {
|
|
29
|
-
readonly for: <T extends Entity>(entityConstructor: EntityConstructor<T>, scope: RepositoryScope) => Effect.Effect<EntityFacetCounter,
|
|
30
|
+
readonly for: <T extends Entity>(entityConstructor: EntityConstructor<T>, scope: RepositoryScope) => Effect.Effect<EntityFacetCounter, StoreFailure | Invalid | Internal>;
|
|
30
31
|
}
|
|
31
32
|
declare const EntityFacetCounterFactoryTag_base: Context.ServiceClass<EntityFacetCounterFactoryTag, "EntityFacetCounterFactoryTag", EntityFacetCounterFactory>;
|
|
32
33
|
export declare class EntityFacetCounterFactoryTag extends EntityFacetCounterFactoryTag_base {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-facet-counter.d.ts","sourceRoot":"","sources":["../../src/repository/entity-facet-counter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,
|
|
1
|
+
{"version":3,"file":"entity-facet-counter.d.ts","sourceRoot":"","sources":["../../src/repository/entity-facet-counter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EACN,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,QAAQ,EACR,OAAO,EACP,YAAY,EACb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAEtE;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,OAAO,SAAS,MAAM,EAC1B,OAAO,EAAE,SAAS,gBAAgB,CAAC,OAAO,CAAC,EAAE,GAC5C,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,CAAC;CACxD;;AAED,qBAAa,qBAAsB,SAAQ,0BAGf;CAAG;AAE/B;;;;;GAKG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,SAAS,MAAM,EAC7B,iBAAiB,EAAE,iBAAiB,CAAC,CAAC,CAAC,EACvC,KAAK,EAAE,eAAe,KACnB,MAAM,CAAC,MAAM,CAAC,kBAAkB,EAAE,YAAY,GAAG,OAAO,GAAG,QAAQ,CAAC,CAAC;CAC3E;;AAED,qBAAa,4BAA6B,SAAQ,iCAGf;CAAG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/repository/entity-facet-counter.ts"],"sourcesContent":["import type {\n
|
|
1
|
+
{"version":3,"sources":["../../src/repository/entity-facet-counter.ts"],"sourcesContent":["import type {\n Entity,\n EntityConstructor,\n EntityFacetCounts,\n EntityFacetQuery,\n Internal,\n Invalid,\n StoreFailure,\n} from '@entifix/core';\nimport { Context, type Effect } from 'effect';\n\nimport type { RepositoryFailure } from './entity.repository.js';\nimport type { RepositoryScope } from './entity-repository-factory.js';\n\n/**\n * Counts how many of one entity's records hold each value of a member — what a\n * facet shows beside each choice (\"Aurora (12)\").\n *\n * A port of its own rather than a fifth method on `EntityRepository`, so a\n * store that cannot count is simply not bound to it: a route or a read that\n * needs counts then fails to compile for want of the tag, instead of an\n * adapter answering an empty list nobody notices.\n *\n * ⚠️ Like the repository, every method requires nothing from its environment;\n * an adapter binds what it needs when it is built.\n */\nexport interface EntityFacetCounter {\n count<TEntity extends Entity>(\n queries: readonly EntityFacetQuery<TEntity>[],\n ): Effect.Effect<EntityFacetCounts, RepositoryFailure>;\n}\n\nexport class EntityFacetCounterTag extends Context.Service<\n EntityFacetCounterTag,\n EntityFacetCounter\n>()('EntityFacetCounterTag') {}\n\n/**\n * Opens the {@link EntityFacetCounter} for one entity, for one request — the\n * counter's `EntityRepositoryFactory`, with the same rule: an implementation\n * over a per-organization store **must refuse** a scope with no\n * `organizationId`.\n */\nexport interface EntityFacetCounterFactory {\n readonly for: <T extends Entity>(\n entityConstructor: EntityConstructor<T>,\n scope: RepositoryScope,\n ) => Effect.Effect<EntityFacetCounter, StoreFailure | Invalid | Internal>;\n}\n\nexport class EntityFacetCounterFactoryTag extends Context.Service<\n EntityFacetCounterFactoryTag,\n EntityFacetCounterFactory\n>()('EntityFacetCounterFactoryTag') {}\n"],"names":["Context","EntityFacetCounterTag","Service","EntityFacetCounterFactoryTag"],"mappings":"AASA,SAASA,OAAO,QAAqB,SAAS;AAuB9C,OAAO,MAAMC,8BAA8BD,QAAQE,OAAO,GAGtD;AAA0B;AAe9B,OAAO,MAAMC,qCAAqCH,QAAQE,OAAO,GAG7D;AAAiC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-link-resolver.repository.d.ts","sourceRoot":"","sources":["../../src/repository/entity-link-resolver.repository.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"entity-link-resolver.repository.d.ts","sourceRoot":"","sources":["../../src/repository/entity-link-resolver.repository.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,MAAM,EACX,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EAGxB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,OAAO,EAAU,MAAM,QAAQ,CAAC;AAEzC,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;;AAE7D;;;;;;GAMG;AACH,qBAAa,qBAAsB,SAAQ,0BAGf;CAAG;AAE/B,8EAA8E;AAC9E,MAAM,MAAM,sBAAsB,GAAG,SAAS;IAC5C,iBAAiB,CAAC,MAAM,CAAC;IACzB,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC;CACrC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CACtC,aAAa,EAAE,aAAa,CAAC,sBAAsB,CAAC,GACnD,OAAO,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAwBxC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Internal } from '@entifix/core';
|
|
2
2
|
import { Context, Effect } from 'effect';
|
|
3
3
|
import { EntityRepositoryTag } from './entity.repository.js';
|
|
4
4
|
/**
|
|
@@ -28,9 +28,7 @@ import { EntityRepositoryTag } from './entity.repository.js';
|
|
|
28
28
|
resolve (entityConstructor, id) {
|
|
29
29
|
const repository = repositoryByConstructor.get(entityConstructor);
|
|
30
30
|
if (!repository) {
|
|
31
|
-
return Effect.fail(new
|
|
32
|
-
entity: entityConstructor.name
|
|
33
|
-
}));
|
|
31
|
+
return Effect.fail(new Internal());
|
|
34
32
|
}
|
|
35
33
|
return repository.get(id);
|
|
36
34
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/repository/entity-link-resolver.repository.ts"],"sourcesContent":["import {\n type
|
|
1
|
+
{"version":3,"sources":["../../src/repository/entity-link-resolver.repository.ts"],"sourcesContent":["import {\n type Entity,\n type EntityConstructor,\n type EntityLinkResolver,\n Internal,\n type TransitionalFailure,\n} from '@entifix/core';\nimport { Context, Effect } from 'effect';\n\nimport { EntityRepositoryTag } from './entity.repository.js';\n\n/**\n * DI tag for the {@link EntityLinkResolver} an environment provides so entity\n * links can materialize their targets. The composition root supplies a resolver\n * backed by the appropriate adapters (REST on the web, Mongo on the backend),\n * and use-cases that follow links yield this tag rather than depending on any\n * concrete repository.\n */\nexport class EntityLinkResolverTag extends Context.Service<\n EntityLinkResolverTag,\n EntityLinkResolver\n>()('EntityLinkResolverTag') {}\n\n/** Maps a link target constructor to the repository adapter that loads it. */\nexport type EntityLinkRegistration = readonly [\n EntityConstructor<Entity>,\n Context.Context<EntityRepositoryTag>,\n];\n\n/**\n * Assembles an {@link EntityLinkResolverTag} context from repository adapters\n * that already exist. A repository requires nothing from its environment, so\n * neither does the resolver built from them.\n *\n * It lives here, beside the tag, rather than with React's `useEntityLinkResolver`\n * — which is only a `useMemo` around it — because nothing about it is React.\n * A server component that imported it from the React package would pull that\n * package's whole barrel, hooks included, and Next rejects `useState` reaching\n * a server module. Framework-free code belongs at a framework-free layer.\n */\nexport function createEntityLinkResolver(\n registrations: ReadonlyArray<EntityLinkRegistration>,\n): Context.Context<EntityLinkResolverTag> {\n const repositoryByConstructor = new Map(\n registrations.map(\n ([entityConstructor, repositoryContext]) =>\n [\n entityConstructor,\n Context.get(repositoryContext, EntityRepositoryTag),\n ] as const,\n ),\n );\n\n const resolver: EntityLinkResolver = {\n resolve(entityConstructor, id) {\n const repository = repositoryByConstructor.get(\n entityConstructor as EntityConstructor<Entity>,\n );\n if (!repository) {\n return Effect.fail(new Internal());\n }\n return repository.get(id) as Effect.Effect<never, TransitionalFailure>;\n },\n };\n\n return Context.make(EntityLinkResolverTag, resolver);\n}\n"],"names":["Internal","Context","Effect","EntityRepositoryTag","EntityLinkResolverTag","Service","createEntityLinkResolver","registrations","repositoryByConstructor","Map","map","entityConstructor","repositoryContext","get","resolver","resolve","id","repository","fail","make"],"mappings":"AAAA,SAIEA,QAAQ,QAEH,gBAAgB;AACvB,SAASC,OAAO,EAAEC,MAAM,QAAQ,SAAS;AAEzC,SAASC,mBAAmB,QAAQ,yBAAyB;AAE7D;;;;;;CAMC,GACD,OAAO,MAAMC,8BAA8BH,QAAQI,OAAO,GAGtD;AAA0B;AAQ9B;;;;;;;;;;CAUC,GACD,OAAO,SAASC,yBACdC,aAAoD;IAEpD,MAAMC,0BAA0B,IAAIC,IAClCF,cAAcG,GAAG,CACf,CAAC,CAACC,mBAAmBC,kBAAkB,GACrC;YACED;YACAV,QAAQY,GAAG,CAACD,mBAAmBT;SAChC;IAIP,MAAMW,WAA+B;QACnCC,SAAQJ,iBAAiB,EAAEK,EAAE;YAC3B,MAAMC,aAAaT,wBAAwBK,GAAG,CAC5CF;YAEF,IAAI,CAACM,YAAY;gBACf,OAAOf,OAAOgB,IAAI,CAAC,IAAIlB;YACzB;YACA,OAAOiB,WAAWJ,GAAG,CAACG;QACxB;IACF;IAEA,OAAOf,QAAQkB,IAAI,CAACf,uBAAuBU;AAC7C"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Entity, EntityConstructor, Internal, Invalid, StoreFailure } from '@entifix/core';
|
|
2
2
|
import { Context, type Effect } from 'effect';
|
|
3
3
|
import type { EntityRepository } from './entity.repository.js';
|
|
4
4
|
/**
|
|
@@ -22,7 +22,7 @@ export interface RepositoryScope {
|
|
|
22
22
|
* somebody else's data.
|
|
23
23
|
*/
|
|
24
24
|
export interface EntityRepositoryFactory {
|
|
25
|
-
readonly for: <T extends Entity>(entityConstructor: EntityConstructor<T>, scope: RepositoryScope) => Effect.Effect<EntityRepository,
|
|
25
|
+
readonly for: <T extends Entity>(entityConstructor: EntityConstructor<T>, scope: RepositoryScope) => Effect.Effect<EntityRepository, StoreFailure | Invalid | Internal>;
|
|
26
26
|
}
|
|
27
27
|
declare const EntityRepositoryFactoryTag_base: Context.ServiceClass<EntityRepositoryFactoryTag, "EntityRepositoryFactoryTag", EntityRepositoryFactory>;
|
|
28
28
|
export declare class EntityRepositoryFactoryTag extends EntityRepositoryFactoryTag_base {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-repository-factory.d.ts","sourceRoot":"","sources":["../../src/repository/entity-repository-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,
|
|
1
|
+
{"version":3,"file":"entity-repository-factory.d.ts","sourceRoot":"","sources":["../../src/repository/entity-repository-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EACN,iBAAiB,EACjB,QAAQ,EACR,OAAO,EACP,YAAY,EACb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,SAAS,MAAM,EAC7B,iBAAiB,EAAE,iBAAiB,CAAC,CAAC,CAAC,EACvC,KAAK,EAAE,eAAe,KACnB,MAAM,CAAC,MAAM,CAAC,gBAAgB,EAAE,YAAY,GAAG,OAAO,GAAG,QAAQ,CAAC,CAAC;CACzE;;AAED,qBAAa,0BAA2B,SAAQ,+BAGf;CAAG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/repository/entity-repository-factory.ts"],"sourcesContent":["import type {\n
|
|
1
|
+
{"version":3,"sources":["../../src/repository/entity-repository-factory.ts"],"sourcesContent":["import type {\n Entity,\n EntityConstructor,\n Internal,\n Invalid,\n StoreFailure,\n} from '@entifix/core';\nimport { Context, type Effect } from 'effect';\n\nimport type { EntityRepository } from './entity.repository.js';\n\n/**\n * Who a repository is being opened for. A request on a per-organization store\n * names its organization; a request on a single store names nothing.\n */\nexport interface RepositoryScope {\n readonly organizationId?: string;\n}\n\n/**\n * Opens the {@link EntityRepository} for one entity, for one request.\n *\n * What lets generic code — a route derived from an entity, a sweep — reach\n * storage without knowing which store, which engine or which database it is.\n * The host provides one at composition: a single database for a platform\n * store, a per-organization resolver for a tenant one. Calling it is cheap; it\n * hands out handles over a pool that already exists.\n *\n * An implementation over a per-organization store **must refuse** a scope with\n * no `organizationId` rather than pick a default: a default organization is\n * somebody else's data.\n */\nexport interface EntityRepositoryFactory {\n readonly for: <T extends Entity>(\n entityConstructor: EntityConstructor<T>,\n scope: RepositoryScope,\n ) => Effect.Effect<EntityRepository, StoreFailure | Invalid | Internal>;\n}\n\nexport class EntityRepositoryFactoryTag extends Context.Service<\n EntityRepositoryFactoryTag,\n EntityRepositoryFactory\n>()('EntityRepositoryFactoryTag') {}\n"],"names":["Context","EntityRepositoryFactoryTag","Service"],"mappings":"AAOA,SAASA,OAAO,QAAqB,SAAS;AAgC9C,OAAO,MAAMC,mCAAmCD,QAAQE,OAAO,GAG3D;AAA+B"}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Entity, EntityId, EntityLoadRequest, EntityPage, TransitionalFailure } from '@entifix/core';
|
|
2
2
|
import { Context } from 'effect';
|
|
3
3
|
import { Effect } from 'effect/Effect';
|
|
4
|
+
/**
|
|
5
|
+
* What a repository fails with while the adapters move to typed failures
|
|
6
|
+
* ([ADR 0013](../../../../../docs/adr/0013-failures-are-typed-values-decided-at-the-shell.md)):
|
|
7
|
+
* the old class tree, or a categorised failure — an `InvalidEnvelope` from a
|
|
8
|
+
* transport, a `DuplicateKey` from a store.
|
|
9
|
+
*/
|
|
10
|
+
export type RepositoryFailure = TransitionalFailure;
|
|
4
11
|
/**
|
|
5
12
|
* Where a use case reads and writes one entity's records.
|
|
6
13
|
*
|
|
@@ -12,10 +19,10 @@ import { Effect } from 'effect/Effect';
|
|
|
12
19
|
* to provide a value it never reads.
|
|
13
20
|
*/
|
|
14
21
|
export interface EntityRepository {
|
|
15
|
-
get<TEntity extends Entity>(id: EntityId): Effect<TEntity,
|
|
16
|
-
load<TEntity extends Entity>(request: EntityLoadRequest<TEntity>): Effect<EntityPage<TEntity>,
|
|
17
|
-
save<TEntity extends Entity>(entity: TEntity): Effect<TEntity,
|
|
18
|
-
delete<TEntity extends Entity>(entityOrId: EntityId | TEntity): Effect<void,
|
|
22
|
+
get<TEntity extends Entity>(id: EntityId): Effect<TEntity, RepositoryFailure>;
|
|
23
|
+
load<TEntity extends Entity>(request: EntityLoadRequest<TEntity>): Effect<EntityPage<TEntity>, RepositoryFailure>;
|
|
24
|
+
save<TEntity extends Entity>(entity: TEntity): Effect<TEntity, RepositoryFailure>;
|
|
25
|
+
delete<TEntity extends Entity>(entityOrId: EntityId | TEntity): Effect<void, RepositoryFailure>;
|
|
19
26
|
}
|
|
20
27
|
declare const EntityRepositoryTag_base: Context.ServiceClass<EntityRepositoryTag, "EntityRepositoryTag", EntityRepository>;
|
|
21
28
|
export declare class EntityRepositoryTag extends EntityRepositoryTag_base {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity.repository.d.ts","sourceRoot":"","sources":["../../src/repository/entity.repository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,
|
|
1
|
+
{"version":3,"file":"entity.repository.d.ts","sourceRoot":"","sources":["../../src/repository/entity.repository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EACN,QAAQ,EACR,iBAAiB,EACjB,UAAU,EACV,mBAAmB,EACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,mBAAmB,CAAC;AAEpD;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,OAAO,SAAS,MAAM,EAAE,EAAE,EAAE,QAAQ,GAAG,MAAM,CAAC,OAAO,EAAE,iBAAiB,CAAC,CAAC;IAC9E,IAAI,CAAC,OAAO,SAAS,MAAM,EACzB,OAAO,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAClC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,iBAAiB,CAAC,CAAC;IAClD,IAAI,CAAC,OAAO,SAAS,MAAM,EACzB,MAAM,EAAE,OAAO,GACd,MAAM,CAAC,OAAO,EAAE,iBAAiB,CAAC,CAAC;IACtC,MAAM,CAAC,OAAO,SAAS,MAAM,EAC3B,UAAU,EAAE,QAAQ,GAAG,OAAO,GAC7B,MAAM,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;CACpC;;AAED,qBAAa,mBAAoB,SAAQ,wBAGf;CAAG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/repository/entity.repository.ts"],"sourcesContent":["import type {\n
|
|
1
|
+
{"version":3,"sources":["../../src/repository/entity.repository.ts"],"sourcesContent":["import type {\n Entity,\n EntityId,\n EntityLoadRequest,\n EntityPage,\n TransitionalFailure,\n} from '@entifix/core';\nimport { Context } from 'effect';\nimport { Effect } from 'effect/Effect';\n\n/**\n * What a repository fails with while the adapters move to typed failures\n * ([ADR 0013](../../../../../docs/adr/0013-failures-are-typed-values-decided-at-the-shell.md)):\n * the old class tree, or a categorised failure — an `InvalidEnvelope` from a\n * transport, a `DuplicateKey` from a store.\n */\nexport type RepositoryFailure = TransitionalFailure;\n\n/**\n * Where a use case reads and writes one entity's records.\n *\n * ⚠️ **Every method requires nothing from its environment.** An adapter that\n * needs something to do its job — a REST client's base URL, a database handle —\n * binds it when the adapter is built, so the port says only what every adapter\n * has in common. A requirement here would be one adapter's mechanism leaked into\n * every use case, and every caller of a static or in-memory adapter would have\n * to provide a value it never reads.\n */\nexport interface EntityRepository {\n get<TEntity extends Entity>(id: EntityId): Effect<TEntity, RepositoryFailure>;\n load<TEntity extends Entity>(\n request: EntityLoadRequest<TEntity>,\n ): Effect<EntityPage<TEntity>, RepositoryFailure>;\n save<TEntity extends Entity>(\n entity: TEntity,\n ): Effect<TEntity, RepositoryFailure>;\n delete<TEntity extends Entity>(\n entityOrId: EntityId | TEntity,\n ): Effect<void, RepositoryFailure>;\n}\n\nexport class EntityRepositoryTag extends Context.Service<\n EntityRepositoryTag,\n EntityRepository\n>()('EntityRepositoryTag') {}\n"],"names":["Context","EntityRepositoryTag","Service"],"mappings":"AAOA,SAASA,OAAO,QAAQ,SAAS;AAkCjC,OAAO,MAAMC,4BAA4BD,QAAQE,OAAO,GAGpD;AAAwB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { NotFound, StoreFailure } from '@entifix/core';
|
|
2
2
|
import { Context } from 'effect';
|
|
3
3
|
import { Effect } from 'effect/Effect';
|
|
4
4
|
/**
|
|
@@ -22,7 +22,7 @@ export interface OneTimeTokenStore {
|
|
|
22
22
|
*
|
|
23
23
|
* The caller must send it onward immediately; there is no way to read it back.
|
|
24
24
|
*/
|
|
25
|
-
issue(purpose: TokenPurpose, subject: string, ttlSeconds: number): Effect<string,
|
|
25
|
+
issue(purpose: TokenPurpose, subject: string, ttlSeconds: number): Effect<string, StoreFailure | NotFound>;
|
|
26
26
|
/**
|
|
27
27
|
* Redeem a token, returning the subject it was minted for.
|
|
28
28
|
*
|
|
@@ -30,7 +30,7 @@ export interface OneTimeTokenStore {
|
|
|
30
30
|
* mail archive, a proxy log — is already spent. Fails when the token is
|
|
31
31
|
* unknown, expired, or already used; the caller must not distinguish those.
|
|
32
32
|
*/
|
|
33
|
-
consume(purpose: TokenPurpose, token: string): Effect<string,
|
|
33
|
+
consume(purpose: TokenPurpose, token: string): Effect<string, StoreFailure | NotFound>;
|
|
34
34
|
}
|
|
35
35
|
declare const OneTimeTokenStoreTag_base: Context.ServiceClass<OneTimeTokenStoreTag, "OneTimeTokenStoreTag", OneTimeTokenStore>;
|
|
36
36
|
/** DI tag the composition root binds to a concrete {@link OneTimeTokenStore}. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"one-time-token-store.d.ts","sourceRoot":"","sources":["../../src/sessions/one-time-token-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"one-time-token-store.d.ts","sourceRoot":"","sources":["../../src/sessions/one-time-token-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAElC;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,KAAK,CACH,OAAO,EAAE,YAAY,EACrB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,QAAQ,CAAC,CAAC;IAC3C;;;;;;OAMG;IACH,OAAO,CACL,OAAO,EAAE,YAAY,EACrB,KAAK,EAAE,MAAM,GACZ,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,QAAQ,CAAC,CAAC;CAC5C;;AAED,iFAAiF;AACjF,qBAAa,oBAAqB,SAAQ,yBAGf;CAAG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/sessions/one-time-token-store.ts"],"sourcesContent":["import type {
|
|
1
|
+
{"version":3,"sources":["../../src/sessions/one-time-token-store.ts"],"sourcesContent":["import type { NotFound, StoreFailure } from '@entifix/core';\nimport { Context } from 'effect';\nimport { Effect } from 'effect/Effect';\n\n/**\n * What a token is for. Namespacing them keeps a token minted for one purpose\n * from being redeemed as another — a password-reset token must not double as\n * an email-verification token.\n */\nexport type TokenPurpose = string;\n\n/**\n * Short-lived, single-use tokens handed to a user out of band (a link in an\n * email) and redeemed once.\n *\n * The store keeps only a HASH of the token, exactly as it would a password: the\n * plaintext exists in the response to {@link OneTimeTokenStore.issue} and in the\n * message sent to the user, and nowhere else. A store dump then yields nothing\n * an attacker can redeem.\n */\nexport interface OneTimeTokenStore {\n /**\n * Mint a token for `subject`, returning the plaintext exactly once.\n *\n * The caller must send it onward immediately; there is no way to read it back.\n */\n issue(\n purpose: TokenPurpose,\n subject: string,\n ttlSeconds: number,\n ): Effect<string, StoreFailure | NotFound>;\n /**\n * Redeem a token, returning the subject it was minted for.\n *\n * Consuming deletes it, so a link that leaks later — from a browser history, a\n * mail archive, a proxy log — is already spent. Fails when the token is\n * unknown, expired, or already used; the caller must not distinguish those.\n */\n consume(\n purpose: TokenPurpose,\n token: string,\n ): Effect<string, StoreFailure | NotFound>;\n}\n\n/** DI tag the composition root binds to a concrete {@link OneTimeTokenStore}. */\nexport class OneTimeTokenStoreTag extends Context.Service<\n OneTimeTokenStoreTag,\n OneTimeTokenStore\n>()('OneTimeTokenStoreTag') {}\n"],"names":["Context","OneTimeTokenStoreTag","Service"],"mappings":"AACA,SAASA,OAAO,QAAQ,SAAS;AA2CjC,+EAA+E,GAC/E,OAAO,MAAMC,6BAA6BD,QAAQE,OAAO,GAGrD;AAAyB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { EntityId, NotFound, StoreFailure } from '@entifix/core';
|
|
2
2
|
import { Context } from 'effect';
|
|
3
3
|
import { Effect } from 'effect/Effect';
|
|
4
4
|
/**
|
|
@@ -107,15 +107,15 @@ export interface SessionLifetimeSeconds {
|
|
|
107
107
|
*/
|
|
108
108
|
export interface SessionStore {
|
|
109
109
|
/** Persist a new session and return its freshly minted opaque id. */
|
|
110
|
-
create(data: SessionData, lifetime: SessionLifetimeSeconds): Effect<string,
|
|
110
|
+
create(data: SessionData, lifetime: SessionLifetimeSeconds): Effect<string, StoreFailure | NotFound>;
|
|
111
111
|
/** Read a live session, failing if it is unknown, revoked, or expired. */
|
|
112
|
-
read(sessionId: string): Effect<SessionRecord,
|
|
112
|
+
read(sessionId: string): Effect<SessionRecord, StoreFailure | NotFound>;
|
|
113
113
|
/**
|
|
114
114
|
* Renew the sliding window, **clamped to the session's own
|
|
115
115
|
* {@link SessionRecord.absoluteExpiresAt}**. Fails once the ceiling is passed,
|
|
116
116
|
* which is how a long-lived session finally ends.
|
|
117
117
|
*/
|
|
118
|
-
touch(sessionId: string, idleTtlSeconds: number): Effect<void,
|
|
118
|
+
touch(sessionId: string, idleTtlSeconds: number): Effect<void, StoreFailure | NotFound>;
|
|
119
119
|
/**
|
|
120
120
|
* Every live session a user holds.
|
|
121
121
|
*
|
|
@@ -123,11 +123,11 @@ export interface SessionStore {
|
|
|
123
123
|
* there is no way to show someone where they are signed in, and "sign out
|
|
124
124
|
* everywhere" is an action with no visible subject.
|
|
125
125
|
*/
|
|
126
|
-
listForUser(userId: EntityId): Effect<readonly SessionRecord[],
|
|
126
|
+
listForUser(userId: EntityId): Effect<readonly SessionRecord[], StoreFailure | NotFound>;
|
|
127
127
|
/** Revoke a single session — the next {@link read} fails immediately. */
|
|
128
|
-
revoke(sessionId: string): Effect<void,
|
|
128
|
+
revoke(sessionId: string): Effect<void, StoreFailure | NotFound>;
|
|
129
129
|
/** Revoke every session for a user (e.g. password change, admin kick). */
|
|
130
|
-
revokeAllForUser(userId: EntityId): Effect<void,
|
|
130
|
+
revokeAllForUser(userId: EntityId): Effect<void, StoreFailure | NotFound>;
|
|
131
131
|
/**
|
|
132
132
|
* Revoke every session for a user EXCEPT one.
|
|
133
133
|
*
|
|
@@ -135,7 +135,7 @@ export interface SessionStore {
|
|
|
135
135
|
* person who just changed it signed in. `revokeAllForUser` would log them out
|
|
136
136
|
* of the screen they are standing on.
|
|
137
137
|
*/
|
|
138
|
-
revokeAllForUserExcept(userId: EntityId, keepSessionId: string): Effect<void,
|
|
138
|
+
revokeAllForUserExcept(userId: EntityId, keepSessionId: string): Effect<void, StoreFailure | NotFound>;
|
|
139
139
|
}
|
|
140
140
|
declare const SessionStoreTag_base: Context.ServiceClass<SessionStoreTag, "SessionStoreTag", SessionStore>;
|
|
141
141
|
/** DI tag the composition root binds to a concrete {@link SessionStore}. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../../src/sessions/session-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../../src/sessions/session-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AACtE,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC;;;;;GAKG;AACH;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACvD;;;;;;;;OAQG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;OASG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;CACjC;AAED,mFAAmF;AACnF,MAAM,WAAW,aAAc,SAAQ,WAAW;IAChD,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,4FAA4F;IAC5F,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;CACpC;AAED,kDAAkD;AAClD,MAAM,WAAW,sBAAsB;IACrC,6CAA6C;IAC7C,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,sDAAsD;IACtD,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;CACrC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,MAAM,CACJ,IAAI,EAAE,WAAW,EACjB,QAAQ,EAAE,sBAAsB,GAC/B,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,QAAQ,CAAC,CAAC;IAC3C,0EAA0E;IAC1E,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,aAAa,EAAE,YAAY,GAAG,QAAQ,CAAC,CAAC;IACxE;;;;OAIG;IACH,KAAK,CACH,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GACrB,MAAM,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ,CAAC,CAAC;IACzC;;;;;;OAMG;IACH,WAAW,CACT,MAAM,EAAE,QAAQ,GACf,MAAM,CAAC,SAAS,aAAa,EAAE,EAAE,YAAY,GAAG,QAAQ,CAAC,CAAC;IAC7D,yEAAyE;IACzE,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ,CAAC,CAAC;IACjE,0EAA0E;IAC1E,gBAAgB,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ,CAAC,CAAC;IAC1E;;;;;;OAMG;IACH,sBAAsB,CACpB,MAAM,EAAE,QAAQ,EAChB,aAAa,EAAE,MAAM,GACpB,MAAM,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ,CAAC,CAAC;CAC1C;;AAED,4EAA4E;AAC5E,qBAAa,eAAgB,SAAQ,oBAGf;CAAG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/sessions/session-store.ts"],"sourcesContent":["import type {
|
|
1
|
+
{"version":3,"sources":["../../src/sessions/session-store.ts"],"sourcesContent":["import type { EntityId, NotFound, StoreFailure } from '@entifix/core';\nimport { Context } from 'effect';\nimport { Effect } from 'effect/Effect';\n\n/**\n * The subject state a session carries. Deliberately structural (no `Principal`\n * import — that lives in the authn layer above this one) so any domain can\n * store its authenticated subject here. A resolved `Principal` is assignable to\n * it as-is.\n */\n/**\n * What the session was opened from, for display and for notifying the owner.\n *\n * **A label, never an authorization input.** Every field is client-supplied or\n * client-derived, so a copied cookie copies the device — which is acceptable\n * precisely because nothing here decides anything. It powers \"you are signed in\n * on these devices\" and \"a new device signed in\"; it never grants or refuses.\n */\nexport interface DeviceContext {\n /** Opaque, server-minted id carried in a long-lived cookie. */\n readonly deviceId: string;\n readonly browser?: string;\n readonly os?: string;\n /** `desktop` / `mobile` / `tablet`, as the user-agent reports it. */\n readonly type?: string;\n /** Truncated to a /24 — enough for \"somewhere else\", less PII at rest. */\n readonly ip?: string;\n}\n\nexport interface SessionData {\n readonly userId: EntityId;\n /** Upstream identity subject (e.g. an IdP `sub`, or the canonical user id). */\n readonly subject: string;\n readonly roles: readonly string[];\n readonly attributes: Readonly<Record<string, unknown>>;\n /**\n * The organization this session is acting for. It lives here rather than on\n * the user because a party may belong to several organizations, so switching\n * is a session operation that re-mints the access token — modelling it on the\n * user would make someone serving two vendors keep two accounts.\n *\n * Absent for a session with no organization: a buyer, or an operator, who\n * holds no tenant scope by default.\n */\n readonly activeOrganizationId?: string;\n /**\n * Which population this session belongs to — buyer, vendor staff, or platform\n * staff. Resolved once when the session opens and carried from there into\n * every access token minted from it, so no service re-derives it per request.\n *\n * A plain string for the same reason `roles` is: the closed set lives in the\n * business layer above this one.\n */\n readonly partyRole?: string;\n /**\n * The party behind the account — the `Individual` id, not the account id.\n *\n * Resolved once when the session opens, from the only store that holds the\n * user → party mapping, and carried from here into every access token: a\n * service scoping a read to *whose records these are* asks about a party, and\n * has no way to make that hop itself.\n *\n * Absent for an account with no party record, and a consumer must read that\n * as \"no party\" rather than as \"any party\".\n */\n readonly partyId?: string;\n /**\n * The business domains {@link SessionData.activeOrganizationId} is\n * provisioned for. Resolved once when the session opens and carried from here\n * into every access token minted from it, so no service re-reads an\n * entitlement per request — the same arrangement `partyRole` already has.\n *\n * Absent for a session with no organization, which is a different statement\n * from an empty list: the ceiling does not apply rather than denying\n * everything.\n */\n readonly entitlements?: readonly string[];\n /** Absent for sessions opened by a non-browser caller. */\n readonly device?: DeviceContext;\n}\n\n/** A stored session as read back — {@link SessionData} plus lifecycle metadata. */\nexport interface SessionRecord extends SessionData {\n /** The opaque session id this record is keyed by; the revocation handle. */\n readonly sessionId: string;\n readonly createdAt: string;\n /** End of the current sliding window; moves forward on every {@link SessionStore.touch}. */\n readonly expiresAt: string;\n /**\n * The ceiling, stamped at creation and never moved. A sliding window with no\n * ceiling renews forever, so this is what guarantees every session eventually\n * ends — including one an attacker is keeping warm.\n */\n readonly absoluteExpiresAt: string;\n}\n\n/** The two durations a session is opened with. */\nexport interface SessionLifetimeSeconds {\n /** Sliding window, renewed on each touch. */\n readonly idleTtlSeconds: number;\n /** Ceiling measured from creation; never extended. */\n readonly absoluteTtlSeconds: number;\n}\n\n/**\n * The session persistence seam. An opaque, revocable, TTL-bounded store keyed by\n * a server-minted session id — the single source of truth every service can\n * read a live session from, and the handle an admin/logout revokes through.\n *\n * Framework- and store-agnostic: a Redis adapter backs it in production, a fake\n * in tests, and the use-cases that consume {@link SessionStoreTag} never change.\n */\nexport interface SessionStore {\n /** Persist a new session and return its freshly minted opaque id. */\n create(\n data: SessionData,\n lifetime: SessionLifetimeSeconds,\n ): Effect<string, StoreFailure | NotFound>;\n /** Read a live session, failing if it is unknown, revoked, or expired. */\n read(sessionId: string): Effect<SessionRecord, StoreFailure | NotFound>;\n /**\n * Renew the sliding window, **clamped to the session's own\n * {@link SessionRecord.absoluteExpiresAt}**. Fails once the ceiling is passed,\n * which is how a long-lived session finally ends.\n */\n touch(\n sessionId: string,\n idleTtlSeconds: number,\n ): Effect<void, StoreFailure | NotFound>;\n /**\n * Every live session a user holds.\n *\n * The read side of the index `revokeAllForUser` already walks. Without it\n * there is no way to show someone where they are signed in, and \"sign out\n * everywhere\" is an action with no visible subject.\n */\n listForUser(\n userId: EntityId,\n ): Effect<readonly SessionRecord[], StoreFailure | NotFound>;\n /** Revoke a single session — the next {@link read} fails immediately. */\n revoke(sessionId: string): Effect<void, StoreFailure | NotFound>;\n /** Revoke every session for a user (e.g. password change, admin kick). */\n revokeAllForUser(userId: EntityId): Effect<void, StoreFailure | NotFound>;\n /**\n * Revoke every session for a user EXCEPT one.\n *\n * What a password change should do: end every other session, but leave the\n * person who just changed it signed in. `revokeAllForUser` would log them out\n * of the screen they are standing on.\n */\n revokeAllForUserExcept(\n userId: EntityId,\n keepSessionId: string,\n ): Effect<void, StoreFailure | NotFound>;\n}\n\n/** DI tag the composition root binds to a concrete {@link SessionStore}. */\nexport class SessionStoreTag extends Context.Service<\n SessionStoreTag,\n SessionStore\n>()('SessionStoreTag') {}\n"],"names":["Context","SessionStoreTag","Service"],"mappings":"AACA,SAASA,OAAO,QAAQ,SAAS;AA2JjC,0EAA0E,GAC1E,OAAO,MAAMC,wBAAwBD,QAAQE,OAAO,GAGhD;AAAoB"}
|
|
@@ -56,7 +56,6 @@ export interface ShutdownRegistry {
|
|
|
56
56
|
*/
|
|
57
57
|
readonly drain: Effect.Effect<void>;
|
|
58
58
|
}
|
|
59
|
-
declare const ShutdownRegistryTag_base: Context.ServiceClass<ShutdownRegistryTag, "ShutdownRegistryTag", ShutdownRegistry>;
|
|
60
59
|
/**
|
|
61
60
|
* DI tag for the shutdown registry.
|
|
62
61
|
*
|
|
@@ -64,6 +63,26 @@ declare const ShutdownRegistryTag_base: Context.ServiceClass<ShutdownRegistryTag
|
|
|
64
63
|
* `entifix-ts-*-client` package can register a hook without depending on a
|
|
65
64
|
* shell.
|
|
66
65
|
*/
|
|
66
|
+
/**
|
|
67
|
+
* What a drain did, for the host to log
|
|
68
|
+
* ([ADR 0013](../../../../../docs/adr/0013-failures-are-typed-values-decided-at-the-shell.md)):
|
|
69
|
+
* the registry says it, the shell that made it decides how loud.
|
|
70
|
+
*/
|
|
71
|
+
export type ShutdownOutcome = {
|
|
72
|
+
readonly _tag: 'draining';
|
|
73
|
+
readonly hooks: number;
|
|
74
|
+
} | {
|
|
75
|
+
readonly _tag: 'hookFailed';
|
|
76
|
+
readonly hook: string;
|
|
77
|
+
readonly phase: ShutdownPhase;
|
|
78
|
+
/** `Cause.pretty`: a timeout and a failure stay distinguishable. */
|
|
79
|
+
readonly cause: string;
|
|
80
|
+
} | {
|
|
81
|
+
readonly _tag: 'drained';
|
|
82
|
+
};
|
|
83
|
+
/** Where a drain reports what it did. */
|
|
84
|
+
export type ShutdownOutcomeSink = (outcome: ShutdownOutcome) => Effect.Effect<void>;
|
|
85
|
+
declare const ShutdownRegistryTag_base: Context.ServiceClass<ShutdownRegistryTag, "ShutdownRegistryTag", ShutdownRegistry>;
|
|
67
86
|
export declare class ShutdownRegistryTag extends ShutdownRegistryTag_base {
|
|
68
87
|
}
|
|
69
88
|
/**
|
|
@@ -76,7 +95,7 @@ export declare class ShutdownRegistryTag extends ShutdownRegistryTag_base {
|
|
|
76
95
|
* ready the instant the signal lands rather than whenever the finalizers get
|
|
77
96
|
* around to running.
|
|
78
97
|
*/
|
|
79
|
-
export declare const makeShutdownRegistry: () => {
|
|
98
|
+
export declare const makeShutdownRegistry: (report?: ShutdownOutcomeSink) => {
|
|
80
99
|
registry: ShutdownRegistry;
|
|
81
100
|
begin: () => void;
|
|
82
101
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shutdown-registry.d.ts","sourceRoot":"","sources":["../../src/shutdown/shutdown-registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,mCAAoC,CAAC;AAEjE,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7D;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CACnC;AAaD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC/D;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC7C;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CACrC
|
|
1
|
+
{"version":3,"file":"shutdown-registry.d.ts","sourceRoot":"","sources":["../../src/shutdown/shutdown-registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,mCAAoC,CAAC;AAEjE,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7D;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CACnC;AAaD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC/D;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC7C;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CACrC;AAED;;;;;;GAMG;AACH;;;;GAIG;AACH,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACrD;IACE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAEjC,yCAAyC;AACzC,MAAM,MAAM,mBAAmB,GAAG,CAChC,OAAO,EAAE,eAAe,KACrB,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;;AAEzB,qBAAa,mBAAoB,SAAQ,wBAGf;CAAG;AAyB7B;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,GAC/B,SAAQ,mBAAuC,KAC9C;IACD,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,KAAK,EAAE,MAAM,IAAI,CAAC;CAoCnB,CAAC"}
|
|
@@ -22,28 +22,21 @@ import { Cause, Context, Effect } from 'effect';
|
|
|
22
22
|
* it and then loses them entirely. A hook that cannot finish in time has
|
|
23
23
|
* already failed; the difference is whether the *next* one gets to run.
|
|
24
24
|
*/ const HOOK_TIMEOUT = '10 seconds';
|
|
25
|
-
|
|
26
|
-
* DI tag for the shutdown registry.
|
|
27
|
-
*
|
|
28
|
-
* In the contract layer beside `HealthRegistryTag` so an
|
|
29
|
-
* `entifix-ts-*-client` package can register a hook without depending on a
|
|
30
|
-
* shell.
|
|
31
|
-
*/ export class ShutdownRegistryTag extends Context.Service()('ShutdownRegistryTag') {
|
|
25
|
+
export class ShutdownRegistryTag extends Context.Service()('ShutdownRegistryTag') {
|
|
32
26
|
}
|
|
33
27
|
/**
|
|
34
28
|
* Runs one hook and swallows whatever it does.
|
|
35
29
|
*
|
|
36
|
-
* A failure and a timeout are
|
|
30
|
+
* A failure and a timeout are reported the same way on purpose: both mean this
|
|
37
31
|
* hook did not do its job, both leave the ones behind it still worth running,
|
|
38
32
|
* and neither is something a terminating process can act on. `Cause.pretty`
|
|
39
|
-
* keeps them distinguishable
|
|
40
|
-
*/ const runHook = (hook)=>hook.run.pipe(Effect.timeout(HOOK_TIMEOUT), Effect.catchCause((cause)=>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}))));
|
|
33
|
+
* keeps them distinguishable for whoever reads the log the host writes.
|
|
34
|
+
*/ const runHook = (report)=>(hook)=>hook.run.pipe(Effect.timeout(HOOK_TIMEOUT), Effect.catchCause((cause)=>report({
|
|
35
|
+
_tag: 'hookFailed',
|
|
36
|
+
hook: hook.name,
|
|
37
|
+
phase: hook.phase,
|
|
38
|
+
cause: Cause.pretty(cause)
|
|
39
|
+
})));
|
|
47
40
|
/**
|
|
48
41
|
* Builds the registry and the synchronous latch that flips it.
|
|
49
42
|
*
|
|
@@ -53,7 +46,7 @@ import { Cause, Context, Effect } from 'effect';
|
|
|
53
46
|
* runtime starts interrupting — so `/api/health/ready` stops claiming to be
|
|
54
47
|
* ready the instant the signal lands rather than whenever the finalizers get
|
|
55
48
|
* around to running.
|
|
56
|
-
*/ export const makeShutdownRegistry = ()=>{
|
|
49
|
+
*/ export const makeShutdownRegistry = (report = ()=>Effect.void)=>{
|
|
57
50
|
const hooks = [];
|
|
58
51
|
let terminating = false;
|
|
59
52
|
const registry = {
|
|
@@ -63,18 +56,21 @@ import { Cause, Context, Effect } from 'effect';
|
|
|
63
56
|
terminating: Effect.sync(()=>terminating),
|
|
64
57
|
drain: Effect.gen(function*() {
|
|
65
58
|
terminating = true;
|
|
66
|
-
yield*
|
|
59
|
+
yield* report({
|
|
60
|
+
_tag: 'draining',
|
|
67
61
|
hooks: hooks.length
|
|
68
|
-
})
|
|
62
|
+
});
|
|
69
63
|
for (const phase of SHUTDOWN_PHASES){
|
|
70
64
|
// Sequential, and in registration order: `stop-intake` before `flush`
|
|
71
65
|
// is the whole contract, and within a phase a later hook may depend on
|
|
72
66
|
// an earlier one having stopped.
|
|
73
|
-
yield* Effect.forEach(hooks.filter((hook)=>hook.phase === phase), runHook, {
|
|
67
|
+
yield* Effect.forEach(hooks.filter((hook)=>hook.phase === phase), runHook(report), {
|
|
74
68
|
discard: true
|
|
75
69
|
});
|
|
76
70
|
}
|
|
77
|
-
yield*
|
|
71
|
+
yield* report({
|
|
72
|
+
_tag: 'drained'
|
|
73
|
+
});
|
|
78
74
|
})
|
|
79
75
|
};
|
|
80
76
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shutdown/shutdown-registry.ts"],"sourcesContent":["import { Cause, Context, Effect } from 'effect';\n\n/**\n * The order a shutdown runs in.\n *\n * **`stop-intake`** stops the process taking new work — cancelling the bus's\n * consumers, interrupting a polling daemon. **`flush`** carries out what is\n * already committed but not yet delivered: the outbox relay's final pass. The\n * two are separate phases rather than one list because they are ordered against\n * each other and not merely against the connections closing — flushing while\n * consumers are still folding events would race a drain against fresh work, and\n * the relay would go on finding entries the handlers keep writing.\n */\nexport const SHUTDOWN_PHASES = ['stop-intake', 'flush'] as const;\n\nexport type ShutdownPhase = (typeof SHUTDOWN_PHASES)[number];\n\n/**\n * One thing to do before this process's connections close.\n *\n * `run` is fully applied, exactly like `HealthProbe.check`: whoever\n * registers the hook has already closed over the connector, the fiber or the\n * store, so the drain can run every hook without knowing what any of them\n * touches.\n */\nexport interface ShutdownHook {\n /** Logged when the hook runs, and when it fails. Never rendered to a caller. */\n readonly name: string;\n readonly phase: ShutdownPhase;\n readonly run: Effect.Effect<void>;\n}\n\n/**\n * How long one hook may take before the drain moves on without it.\n *\n * Not belt-and-braces. Kubernetes sends SIGKILL at the end of\n * `terminationGracePeriodSeconds` whatever the process is doing, so an\n * unbounded hook does not buy time — it spends the budget of the hooks behind\n * it and then loses them entirely. A hook that cannot finish in time has\n * already failed; the difference is whether the *next* one gets to run.\n */\nconst HOOK_TIMEOUT = '10 seconds';\n\n/**\n * What a service does on its way out, registered by whoever opened the thing\n * that needs draining.\n *\n * A sibling of `HealthRegistry` and `WiringRegistry`, and for the same reason:\n * a `-service` that gains a bus gains its consumer drain by merging that\n * adapter's layer, and nothing in the service's own `main.ts` has to remember\n * to list it. A hand-written sequence per composition root drifts the first\n * time someone adds a dependency; this cannot, because the hook ships with the\n * client that needs it.\n */\nexport interface ShutdownRegistry {\n /** Add a hook. Called by an adapter layer as it constructs its client. */\n readonly register: (hook: ShutdownHook) => Effect.Effect<void>;\n /**\n * Whether this process has begun terminating.\n *\n * Read by `GET /api/health/ready`, which answers `503` from here **before**\n * it consults any probe: nothing is failing, the process is leaving.\n */\n readonly terminating: Effect.Effect<boolean>;\n /**\n * Run every hook, phase by phase, in registration order. Never fails.\n *\n * Called from a `Layer` finalizer, which is what orders it: the drain layer\n * is built after the composition root, so it is released before it, and the\n * connections a hook needs are still open while it runs.\n */\n readonly drain: Effect.Effect<void>;\n}\n\n/**\n * DI tag for the shutdown registry.\n *\n * In the contract layer beside `HealthRegistryTag` so an\n * `entifix-ts-*-client` package can register a hook without depending on a\n * shell.\n */\nexport class ShutdownRegistryTag extends Context.Service<\n ShutdownRegistryTag,\n ShutdownRegistry\n>()('ShutdownRegistryTag') {}\n\n/**\n * Runs one hook and swallows whatever it does.\n *\n * A failure and a timeout are
|
|
1
|
+
{"version":3,"sources":["../../src/shutdown/shutdown-registry.ts"],"sourcesContent":["import { Cause, Context, Effect } from 'effect';\n\n/**\n * The order a shutdown runs in.\n *\n * **`stop-intake`** stops the process taking new work — cancelling the bus's\n * consumers, interrupting a polling daemon. **`flush`** carries out what is\n * already committed but not yet delivered: the outbox relay's final pass. The\n * two are separate phases rather than one list because they are ordered against\n * each other and not merely against the connections closing — flushing while\n * consumers are still folding events would race a drain against fresh work, and\n * the relay would go on finding entries the handlers keep writing.\n */\nexport const SHUTDOWN_PHASES = ['stop-intake', 'flush'] as const;\n\nexport type ShutdownPhase = (typeof SHUTDOWN_PHASES)[number];\n\n/**\n * One thing to do before this process's connections close.\n *\n * `run` is fully applied, exactly like `HealthProbe.check`: whoever\n * registers the hook has already closed over the connector, the fiber or the\n * store, so the drain can run every hook without knowing what any of them\n * touches.\n */\nexport interface ShutdownHook {\n /** Logged when the hook runs, and when it fails. Never rendered to a caller. */\n readonly name: string;\n readonly phase: ShutdownPhase;\n readonly run: Effect.Effect<void>;\n}\n\n/**\n * How long one hook may take before the drain moves on without it.\n *\n * Not belt-and-braces. Kubernetes sends SIGKILL at the end of\n * `terminationGracePeriodSeconds` whatever the process is doing, so an\n * unbounded hook does not buy time — it spends the budget of the hooks behind\n * it and then loses them entirely. A hook that cannot finish in time has\n * already failed; the difference is whether the *next* one gets to run.\n */\nconst HOOK_TIMEOUT = '10 seconds';\n\n/**\n * What a service does on its way out, registered by whoever opened the thing\n * that needs draining.\n *\n * A sibling of `HealthRegistry` and `WiringRegistry`, and for the same reason:\n * a `-service` that gains a bus gains its consumer drain by merging that\n * adapter's layer, and nothing in the service's own `main.ts` has to remember\n * to list it. A hand-written sequence per composition root drifts the first\n * time someone adds a dependency; this cannot, because the hook ships with the\n * client that needs it.\n */\nexport interface ShutdownRegistry {\n /** Add a hook. Called by an adapter layer as it constructs its client. */\n readonly register: (hook: ShutdownHook) => Effect.Effect<void>;\n /**\n * Whether this process has begun terminating.\n *\n * Read by `GET /api/health/ready`, which answers `503` from here **before**\n * it consults any probe: nothing is failing, the process is leaving.\n */\n readonly terminating: Effect.Effect<boolean>;\n /**\n * Run every hook, phase by phase, in registration order. Never fails.\n *\n * Called from a `Layer` finalizer, which is what orders it: the drain layer\n * is built after the composition root, so it is released before it, and the\n * connections a hook needs are still open while it runs.\n */\n readonly drain: Effect.Effect<void>;\n}\n\n/**\n * DI tag for the shutdown registry.\n *\n * In the contract layer beside `HealthRegistryTag` so an\n * `entifix-ts-*-client` package can register a hook without depending on a\n * shell.\n */\n/**\n * What a drain did, for the host to log\n * ([ADR 0013](../../../../../docs/adr/0013-failures-are-typed-values-decided-at-the-shell.md)):\n * the registry says it, the shell that made it decides how loud.\n */\nexport type ShutdownOutcome =\n | { readonly _tag: 'draining'; readonly hooks: number }\n | {\n readonly _tag: 'hookFailed';\n readonly hook: string;\n readonly phase: ShutdownPhase;\n /** `Cause.pretty`: a timeout and a failure stay distinguishable. */\n readonly cause: string;\n }\n | { readonly _tag: 'drained' };\n\n/** Where a drain reports what it did. */\nexport type ShutdownOutcomeSink = (\n outcome: ShutdownOutcome,\n) => Effect.Effect<void>;\n\nexport class ShutdownRegistryTag extends Context.Service<\n ShutdownRegistryTag,\n ShutdownRegistry\n>()('ShutdownRegistryTag') {}\n\n/**\n * Runs one hook and swallows whatever it does.\n *\n * A failure and a timeout are reported the same way on purpose: both mean this\n * hook did not do its job, both leave the ones behind it still worth running,\n * and neither is something a terminating process can act on. `Cause.pretty`\n * keeps them distinguishable for whoever reads the log the host writes.\n */\nconst runHook =\n (report: ShutdownOutcomeSink) =>\n (hook: ShutdownHook): Effect.Effect<void> =>\n hook.run.pipe(\n Effect.timeout(HOOK_TIMEOUT),\n Effect.catchCause(cause =>\n report({\n _tag: 'hookFailed',\n hook: hook.name,\n phase: hook.phase,\n cause: Cause.pretty(cause),\n }),\n ),\n );\n\n/**\n * Builds the registry and the synchronous latch that flips it.\n *\n * Two halves for two callers, the `makeAmqpConnector` shape: the\n * `registry` goes into the layer graph, while `begin` is called from a\n * `process.on('SIGTERM')` handler — outside Effect, in the same tick the\n * runtime starts interrupting — so `/api/health/ready` stops claiming to be\n * ready the instant the signal lands rather than whenever the finalizers get\n * around to running.\n */\nexport const makeShutdownRegistry = (\n report: ShutdownOutcomeSink = () => Effect.void,\n): {\n registry: ShutdownRegistry;\n begin: () => void;\n} => {\n const hooks: ShutdownHook[] = [];\n let terminating = false;\n\n const registry: ShutdownRegistry = {\n register: hook =>\n Effect.sync(() => {\n hooks.push(hook);\n }),\n\n terminating: Effect.sync(() => terminating),\n\n drain: Effect.gen(function* () {\n terminating = true;\n yield* report({ _tag: 'draining', hooks: hooks.length });\n for (const phase of SHUTDOWN_PHASES) {\n // Sequential, and in registration order: `stop-intake` before `flush`\n // is the whole contract, and within a phase a later hook may depend on\n // an earlier one having stopped.\n yield* Effect.forEach(\n hooks.filter(hook => hook.phase === phase),\n runHook(report),\n { discard: true },\n );\n }\n yield* report({ _tag: 'drained' });\n }),\n };\n\n return {\n registry,\n begin: () => {\n terminating = true;\n },\n };\n};\n"],"names":["Cause","Context","Effect","SHUTDOWN_PHASES","HOOK_TIMEOUT","ShutdownRegistryTag","Service","runHook","report","hook","run","pipe","timeout","catchCause","cause","_tag","name","phase","pretty","makeShutdownRegistry","void","hooks","terminating","registry","register","sync","push","drain","gen","length","forEach","filter","discard","begin"],"mappings":"AAAA,SAASA,KAAK,EAAEC,OAAO,EAAEC,MAAM,QAAQ,SAAS;AAEhD;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,kBAAkB;IAAC;IAAe;CAAQ,CAAU;AAmBjE;;;;;;;;CAQC,GACD,MAAMC,eAAe;AA6DrB,OAAO,MAAMC,4BAA4BJ,QAAQK,OAAO,GAGpD;AAAwB;AAE5B;;;;;;;CAOC,GACD,MAAMC,UACJ,CAACC,SACD,CAACC,OACCA,KAAKC,GAAG,CAACC,IAAI,CACXT,OAAOU,OAAO,CAACR,eACfF,OAAOW,UAAU,CAACC,CAAAA,QAChBN,OAAO;gBACLO,MAAM;gBACNN,MAAMA,KAAKO,IAAI;gBACfC,OAAOR,KAAKQ,KAAK;gBACjBH,OAAOd,MAAMkB,MAAM,CAACJ;YACtB;AAIR;;;;;;;;;CASC,GACD,OAAO,MAAMK,uBAAuB,CAClCX,SAA8B,IAAMN,OAAOkB,IAAI;IAK/C,MAAMC,QAAwB,EAAE;IAChC,IAAIC,cAAc;IAElB,MAAMC,WAA6B;QACjCC,UAAUf,CAAAA,OACRP,OAAOuB,IAAI,CAAC;gBACVJ,MAAMK,IAAI,CAACjB;YACb;QAEFa,aAAapB,OAAOuB,IAAI,CAAC,IAAMH;QAE/BK,OAAOzB,OAAO0B,GAAG,CAAC;YAChBN,cAAc;YACd,OAAOd,OAAO;gBAAEO,MAAM;gBAAYM,OAAOA,MAAMQ,MAAM;YAAC;YACtD,KAAK,MAAMZ,SAASd,gBAAiB;gBACnC,sEAAsE;gBACtE,uEAAuE;gBACvE,iCAAiC;gBACjC,OAAOD,OAAO4B,OAAO,CACnBT,MAAMU,MAAM,CAACtB,CAAAA,OAAQA,KAAKQ,KAAK,KAAKA,QACpCV,QAAQC,SACR;oBAAEwB,SAAS;gBAAK;YAEpB;YACA,OAAOxB,OAAO;gBAAEO,MAAM;YAAU;QAClC;IACF;IAEA,OAAO;QACLQ;QACAU,OAAO;YACLX,cAAc;QAChB;IACF;AACF,EAAE"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Invalid, StoreFailure } from '@entifix/core';
|
|
2
2
|
import { Context, type Effect } from 'effect';
|
|
3
3
|
/**
|
|
4
4
|
* Resolves the storage handle for one organization.
|
|
@@ -24,7 +24,7 @@ import { Context, type Effect } from 'effect';
|
|
|
24
24
|
* a call-site change.
|
|
25
25
|
*/
|
|
26
26
|
export interface TenantDatabaseResolver<THandle> {
|
|
27
|
-
readonly forOrganization: (organizationId: string) => Effect.Effect<THandle,
|
|
27
|
+
readonly forOrganization: (organizationId: string) => Effect.Effect<THandle, StoreFailure | Invalid>;
|
|
28
28
|
}
|
|
29
29
|
declare const TenantDatabaseResolverTag_base: Context.ServiceClass<TenantDatabaseResolverTag, "TenantDatabaseResolverTag", TenantDatabaseResolver<unknown>>;
|
|
30
30
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant-database-resolver.d.ts","sourceRoot":"","sources":["../../src/tenancy/tenant-database-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"tenant-database-resolver.d.ts","sourceRoot":"","sources":["../../src/tenancy/tenant-database-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,WAAW,sBAAsB,CAAC,OAAO;IAC7C,QAAQ,CAAC,eAAe,EAAE,CACxB,cAAc,EAAE,MAAM,KACnB,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,CAAC;CACrD;;AAED;;;;GAIG;AACH,qBAAa,yBAA0B,SAAQ,8BAGf;CAAG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tenancy/tenant-database-resolver.ts"],"sourcesContent":["import type {
|
|
1
|
+
{"version":3,"sources":["../../src/tenancy/tenant-database-resolver.ts"],"sourcesContent":["import type { Invalid, StoreFailure } from '@entifix/core';\nimport { Context, type Effect } from 'effect';\n\n/**\n * Resolves the storage handle for one organization.\n *\n * The seam the whole tenancy model turns on. Business entities carry no\n * `organizationId` and no tenant filter, so isolation is *which handle a request\n * resolves to* — which means no query can leak by omission, because there is no\n * column to forget.\n *\n * Two rules the implementations must keep:\n *\n * - **Resolve per request, never per `Layer`.** The connection pool is acquired\n * once at boot (`MongoDatabaseLayer`'s `Layer.effect`); a handle is not a\n * connection. Building a `Layer` per request would rebuild the pool per\n * request.\n * - **Derive the storage name from the organization id**, never from a mutable\n * attribute such as a slug, so renaming an organization can never strand its\n * data.\n *\n * `THandle` is the datastore's own handle type (`Db` for Mongo). Keeping it\n * generic is what lets a Postgres adapter — where the same idea is a schema on\n * a shared pool rather than a separate database — satisfy the same port without\n * a call-site change.\n */\nexport interface TenantDatabaseResolver<THandle> {\n readonly forOrganization: (\n organizationId: string,\n ) => Effect.Effect<THandle, StoreFailure | Invalid>;\n}\n\n/**\n * DI tag for the resolver. Declared with an `unknown` handle because the tag is\n * datastore-agnostic; a service narrows it at the composition root, where it\n * also knows which driver it provided.\n */\nexport class TenantDatabaseResolverTag extends Context.Service<\n TenantDatabaseResolverTag,\n TenantDatabaseResolver<unknown>\n>()('TenantDatabaseResolverTag') {}\n"],"names":["Context","TenantDatabaseResolverTag","Service"],"mappings":"AACA,SAASA,OAAO,QAAqB,SAAS;AA+B9C;;;;CAIC,GACD,OAAO,MAAMC,kCAAkCD,QAAQE,OAAO,GAG1D;AAA8B"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { StoreFailure } from '@entifix/core';
|
|
2
2
|
import { Context, type Effect } from 'effect';
|
|
3
3
|
/** One organization's storage, as a {@link TenantDirectory} lists it. */
|
|
4
4
|
export interface TenantEntry<THandle> {
|
|
@@ -18,7 +18,7 @@ export interface TenantEntry<THandle> {
|
|
|
18
18
|
* storage it does not own.
|
|
19
19
|
*/
|
|
20
20
|
export interface TenantDirectory<THandle> {
|
|
21
|
-
readonly list: () => Effect.Effect<readonly TenantEntry<THandle>[],
|
|
21
|
+
readonly list: () => Effect.Effect<readonly TenantEntry<THandle>[], StoreFailure>;
|
|
22
22
|
}
|
|
23
23
|
declare const TenantDirectoryTag_base: Context.ServiceClass<TenantDirectoryTag, "TenantDirectoryTag", TenantDirectory<unknown>>;
|
|
24
24
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant-directory.d.ts","sourceRoot":"","sources":["../../src/tenancy/tenant-directory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"tenant-directory.d.ts","sourceRoot":"","sources":["../../src/tenancy/tenant-directory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C,yEAAyE;AACzE,MAAM,WAAW,WAAW,CAAC,OAAO;IAClC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,eAAe,CAAC,OAAO;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,MAAM,CAAC,MAAM,CAChC,SAAS,WAAW,CAAC,OAAO,CAAC,EAAE,EAC/B,YAAY,CACb,CAAC;CACH;;AAED;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,uBAGf;CAAG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tenancy/tenant-directory.ts"],"sourcesContent":["import type {
|
|
1
|
+
{"version":3,"sources":["../../src/tenancy/tenant-directory.ts"],"sourcesContent":["import type { StoreFailure } from '@entifix/core';\nimport { Context, type Effect } from 'effect';\n\n/** One organization's storage, as a {@link TenantDirectory} lists it. */\nexport interface TenantEntry<THandle> {\n readonly organizationId: string;\n readonly handle: THandle;\n}\n\n/**\n * Every organization's storage for one per-organization store.\n *\n * The other half of {@link TenantDatabaseResolver}: that one answers a\n * request about **one** organization; this one is for the background work\n * that has to visit **all** of them — an outbox relay, an expiry sweep, a\n * rebuild walk. A request handler never needs it, and should not have it.\n *\n * Enumerating is only legitimate for the slice that writes the store being\n * enumerated. A process listing another slice's tenant databases is reading\n * storage it does not own.\n */\nexport interface TenantDirectory<THandle> {\n readonly list: () => Effect.Effect<\n readonly TenantEntry<THandle>[],\n StoreFailure\n >;\n}\n\n/**\n * DI tag for the directory, with an `unknown` handle for the reason\n * {@link TenantDatabaseResolverTag} gives: the composition root narrows it.\n */\nexport class TenantDirectoryTag extends Context.Service<\n TenantDirectoryTag,\n TenantDirectory<unknown>\n>()('TenantDirectoryTag') {}\n"],"names":["Context","TenantDirectoryTag","Service"],"mappings":"AACA,SAASA,OAAO,QAAqB,SAAS;AA2B9C;;;CAGC,GACD,OAAO,MAAMC,2BAA2BD,QAAQE,OAAO,GAGnD;AAAuB"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Unavailable } from '@entifix/core';
|
|
2
|
+
declare const InvalidToken_base: import("effect/Schema").Class<InvalidToken, import("effect/Schema").TaggedStruct<"InvalidToken", Record<never, never>>, import("effect/Cause").YieldableError & import("@entifix/core").FailureFacts<"invalidToken", "unauthenticated">>;
|
|
3
|
+
/**
|
|
4
|
+
* A token whose signature, algorithm, issuer, audience or shape does not
|
|
5
|
+
* verify. The caller has to sign in again; retrying sends the same token.
|
|
6
|
+
*/
|
|
7
|
+
export declare class InvalidToken extends InvalidToken_base {
|
|
8
|
+
}
|
|
9
|
+
declare const ExpiredToken_base: import("effect/Schema").Class<ExpiredToken, import("effect/Schema").TaggedStruct<"ExpiredToken", Record<never, never>>, import("effect/Cause").YieldableError & import("@entifix/core").FailureFacts<"expiredToken", "unauthenticated">>;
|
|
10
|
+
/** A token that verified once and has since expired. */
|
|
11
|
+
export declare class ExpiredToken extends ExpiredToken_base {
|
|
12
|
+
}
|
|
13
|
+
/** Why a token did not verify, as a host can act on it. */
|
|
14
|
+
export type TokenFailure = InvalidToken | ExpiredToken | Unavailable;
|
|
15
|
+
/**
|
|
16
|
+
* A verification error from `jose`, read by its `code` and never by class:
|
|
17
|
+
* expiry is its own answer, a key set that could not be fetched is the
|
|
18
|
+
* provider being unreachable, and everything else is a token that does not
|
|
19
|
+
* verify. `ERR_JWKS_NO_MATCHING_KEY` counts as unreachable because a key
|
|
20
|
+
* rotation the cache has not caught up with clears on the next fetch.
|
|
21
|
+
*/
|
|
22
|
+
export declare const tokenFailureFrom: (error: unknown, dependency?: string) => TokenFailure;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=token-failures.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-failures.d.ts","sourceRoot":"","sources":["../../src/tokens/token-failures.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,WAAW,EAAE,MAAM,eAAe,CAAC;;AAErD;;;GAGG;AACH,qBAAa,YAAa,SAAQ,iBAGjC;CAAG;;AAEJ,wDAAwD;AACxD,qBAAa,YAAa,SAAQ,iBAGjC;CAAG;AAEJ,2DAA2D;AAC3D,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,YAAY,GAAG,WAAW,CAAC;AAOrE;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAC3B,OAAO,OAAO,EACd,mBAAmB,KAClB,YAYF,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Failure, Unavailable } from '@entifix/core';
|
|
2
|
+
/**
|
|
3
|
+
* A token whose signature, algorithm, issuer, audience or shape does not
|
|
4
|
+
* verify. The caller has to sign in again; retrying sends the same token.
|
|
5
|
+
*/ export class InvalidToken extends Failure.unauthenticated()('InvalidToken', 'invalidToken') {
|
|
6
|
+
}
|
|
7
|
+
/** A token that verified once and has since expired. */ export class ExpiredToken extends Failure.unauthenticated()('ExpiredToken', 'expiredToken') {
|
|
8
|
+
}
|
|
9
|
+
const JWKS_UNREACHABLE = new Set([
|
|
10
|
+
'ERR_JWKS_TIMEOUT',
|
|
11
|
+
'ERR_JWKS_NO_MATCHING_KEY'
|
|
12
|
+
]);
|
|
13
|
+
/**
|
|
14
|
+
* A verification error from `jose`, read by its `code` and never by class:
|
|
15
|
+
* expiry is its own answer, a key set that could not be fetched is the
|
|
16
|
+
* provider being unreachable, and everything else is a token that does not
|
|
17
|
+
* verify. `ERR_JWKS_NO_MATCHING_KEY` counts as unreachable because a key
|
|
18
|
+
* rotation the cache has not caught up with clears on the next fetch.
|
|
19
|
+
*/ export const tokenFailureFrom = (error, dependency = 'jwks')=>{
|
|
20
|
+
const shape = typeof error === 'object' && error !== null ? error : {};
|
|
21
|
+
if (shape.code === 'ERR_JWT_EXPIRED') return new ExpiredToken();
|
|
22
|
+
if (typeof shape.code === 'string' && JWKS_UNREACHABLE.has(shape.code) || shape.name === 'TypeError') return new Unavailable({
|
|
23
|
+
dependency
|
|
24
|
+
});
|
|
25
|
+
return new InvalidToken();
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=token-failures.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/tokens/token-failures.ts"],"sourcesContent":["import { Failure, Unavailable } from '@entifix/core';\n\n/**\n * A token whose signature, algorithm, issuer, audience or shape does not\n * verify. The caller has to sign in again; retrying sends the same token.\n */\nexport class InvalidToken extends Failure.unauthenticated<InvalidToken>()(\n 'InvalidToken',\n 'invalidToken',\n) {}\n\n/** A token that verified once and has since expired. */\nexport class ExpiredToken extends Failure.unauthenticated<ExpiredToken>()(\n 'ExpiredToken',\n 'expiredToken',\n) {}\n\n/** Why a token did not verify, as a host can act on it. */\nexport type TokenFailure = InvalidToken | ExpiredToken | Unavailable;\n\nconst JWKS_UNREACHABLE = new Set([\n 'ERR_JWKS_TIMEOUT',\n 'ERR_JWKS_NO_MATCHING_KEY',\n]);\n\n/**\n * A verification error from `jose`, read by its `code` and never by class:\n * expiry is its own answer, a key set that could not be fetched is the\n * provider being unreachable, and everything else is a token that does not\n * verify. `ERR_JWKS_NO_MATCHING_KEY` counts as unreachable because a key\n * rotation the cache has not caught up with clears on the next fetch.\n */\nexport const tokenFailureFrom = (\n error: unknown,\n dependency = 'jwks',\n): TokenFailure => {\n const shape =\n typeof error === 'object' && error !== null\n ? (error as { readonly code?: unknown; readonly name?: unknown })\n : {};\n if (shape.code === 'ERR_JWT_EXPIRED') return new ExpiredToken();\n if (\n (typeof shape.code === 'string' && JWKS_UNREACHABLE.has(shape.code)) ||\n shape.name === 'TypeError'\n )\n return new Unavailable({ dependency });\n return new InvalidToken();\n};\n"],"names":["Failure","Unavailable","InvalidToken","unauthenticated","ExpiredToken","JWKS_UNREACHABLE","Set","tokenFailureFrom","error","dependency","shape","code","has","name"],"mappings":"AAAA,SAASA,OAAO,EAAEC,WAAW,QAAQ,gBAAgB;AAErD;;;CAGC,GACD,OAAO,MAAMC,qBAAqBF,QAAQG,eAAe,GACvD,gBACA;AACC;AAEH,sDAAsD,GACtD,OAAO,MAAMC,qBAAqBJ,QAAQG,eAAe,GACvD,gBACA;AACC;AAKH,MAAME,mBAAmB,IAAIC,IAAI;IAC/B;IACA;CACD;AAED;;;;;;CAMC,GACD,OAAO,MAAMC,mBAAmB,CAC9BC,OACAC,aAAa,MAAM;IAEnB,MAAMC,QACJ,OAAOF,UAAU,YAAYA,UAAU,OAClCA,QACD,CAAC;IACP,IAAIE,MAAMC,IAAI,KAAK,mBAAmB,OAAO,IAAIP;IACjD,IACE,AAAC,OAAOM,MAAMC,IAAI,KAAK,YAAYN,iBAAiBO,GAAG,CAACF,MAAMC,IAAI,KAClED,MAAMG,IAAI,KAAK,aAEf,OAAO,IAAIZ,YAAY;QAAEQ;IAAW;IACtC,OAAO,IAAIP;AACb,EAAE"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { EntityId, Internal } from '@entifix/core';
|
|
2
2
|
import { Context } from 'effect';
|
|
3
3
|
import { Effect } from 'effect/Effect';
|
|
4
|
+
import type { TokenFailure } from './token-failures.js';
|
|
4
5
|
/**
|
|
5
6
|
* The claims a stateless access token carries between the perimeter that mints
|
|
6
7
|
* it and the services that verify it. Kept small on purpose: everything here is
|
|
@@ -89,9 +90,9 @@ export interface TokenClaims {
|
|
|
89
90
|
*/
|
|
90
91
|
export interface TokenService {
|
|
91
92
|
/** Sign the claims into a compact token that expires after `ttlSeconds`. */
|
|
92
|
-
sign(claims: TokenClaims, ttlSeconds: number): Effect<string,
|
|
93
|
+
sign(claims: TokenClaims, ttlSeconds: number): Effect<string, Internal>;
|
|
93
94
|
/** Verify signature + expiry and return the claims, or fail. */
|
|
94
|
-
verify(token: string): Effect<TokenClaims,
|
|
95
|
+
verify(token: string): Effect<TokenClaims, TokenFailure>;
|
|
95
96
|
}
|
|
96
97
|
declare const TokenServiceTag_base: Context.ServiceClass<TokenServiceTag, "TokenServiceTag", TokenService>;
|
|
97
98
|
/** DI tag the composition root binds to a concrete {@link TokenService}. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-service.d.ts","sourceRoot":"","sources":["../../src/tokens/token-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"token-service.d.ts","sourceRoot":"","sources":["../../src/tokens/token-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,gEAAgE;IAChE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,wEAAwE;IACxE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,gEAAgE;IAChE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACxE,gEAAgE;IAChE,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;CAC1D;;AAED,4EAA4E;AAC5E,qBAAa,eAAgB,SAAQ,oBAGf;CAAG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tokens/token-service.ts"],"sourcesContent":["import type {
|
|
1
|
+
{"version":3,"sources":["../../src/tokens/token-service.ts"],"sourcesContent":["import type { EntityId, Internal } from '@entifix/core';\nimport { Context } from 'effect';\nimport { Effect } from 'effect/Effect';\n\nimport type { TokenFailure } from './token-failures.js';\n\n/**\n * The claims a stateless access token carries between the perimeter that mints\n * it and the services that verify it. Kept small on purpose: everything here is\n * copied into every request and can only refresh when a new token is minted, so\n * only authorization-critical, stable subject data belongs in it. Richer or\n * volatile state stays in the session record and is read on demand.\n */\nexport interface TokenClaims {\n /** Canonical user id — the value services key their data against. */\n readonly userId: EntityId;\n /** Upstream identity subject that authenticated the session. */\n readonly subject: string;\n /** The session id the token was minted from; the revocation linkage. */\n readonly sessionId: string;\n readonly roles: readonly string[];\n /**\n * The organization this session is acting for, and the value every\n * tenant-plane storage handle is resolved from.\n *\n * A property of the **session**, not of the user: a party may hold several\n * memberships, and switching re-mints the token. Absent for a session with no\n * organization — a buyer, or an operator, who holds no tenant scope at all.\n *\n * Note this is a *routing* key, not an authorization input. Grants still come\n * from `roles` resolved at the consumer.\n */\n readonly activeOrganizationId?: string;\n /**\n * Which population the session belongs to — a buyer, a vendor's staff, or\n * platform staff. Typed as a plain string here because the closed set lives\n * in the business layer (`PartyRoles` in `business-ts-party-management`),\n * which this layer may not import; consumers narrow it, exactly as they\n * already do for `roles`.\n *\n * It exists because the alternative was inferring it from an *absent*\n * `activeOrganizationId`, which cannot tell a buyer from an operator — two\n * populations whose reach differs enormously. A capability that sensitive\n * must not be signalled by a missing field.\n *\n * Like `activeOrganizationId` this is routing/context, not a grant: what a\n * principal may do still comes from `roles` resolved at the consumer.\n */\n readonly partyRole?: string;\n /**\n * The party behind the account — the `Individual` id that domains recording a\n * party key against, and the value a `ProductOrder.buyerId` holds.\n *\n * It is carried because the hop from an account to a party exists in exactly\n * one store, the one that mints this token. A service scoping a read to the\n * caller's own records needs the party and cannot derive it from\n * {@link TokenClaims.userId} without reading another slice's collections.\n *\n * Absent for an account with no party record. A consumer must read the\n * absence as *no party* — scoping to nothing — never as *any party*.\n *\n * Like `activeOrganizationId` and `partyRole` this is context, not a grant:\n * it says whose records these are, never what may be done with them.\n */\n readonly partyId?: string;\n /**\n * The business domains the acting organization is provisioned for — the\n * second assignment ceiling beside the role, carried so a server-rendered menu can\n * shape itself without a lookup on every render.\n *\n * Absent for a session with no organization, and that is **not** the same as\n * an empty list: an operator holds no tenant scope, so the ceiling does not\n * apply at all, while a member of an organization provisioned for nothing is\n * entitled to nothing. Consumers therefore decide from\n * {@link TokenClaims.activeOrganizationId} whether to consult this at all.\n *\n * Plain strings for the same reason `roles` is: the domain names live in the\n * business layer, which this one may not import.\n *\n * Like `activeOrganizationId` and `partyRole` this is context, not a grant.\n */\n readonly entitlements?: readonly string[];\n /** Room for a few extra stable claims without a type change. */\n readonly [key: string]: unknown;\n}\n\n/**\n * Signs and verifies short-lived access tokens. The perimeter provides it to\n * mint a token from a resolved session; every downstream service provides it to\n * verify one statelessly (no store round trip on the hot path). Vendor-neutral:\n * a `jose`/HS256 adapter backs it now, an RS256/JWKS one can later without\n * touching a consumer.\n */\nexport interface TokenService {\n /** Sign the claims into a compact token that expires after `ttlSeconds`. */\n sign(claims: TokenClaims, ttlSeconds: number): Effect<string, Internal>;\n /** Verify signature + expiry and return the claims, or fail. */\n verify(token: string): Effect<TokenClaims, TokenFailure>;\n}\n\n/** DI tag the composition root binds to a concrete {@link TokenService}. */\nexport class TokenServiceTag extends Context.Service<\n TokenServiceTag,\n TokenService\n>()('TokenServiceTag') {}\n"],"names":["Context","TokenServiceTag","Service"],"mappings":"AACA,SAASA,OAAO,QAAQ,SAAS;AAmGjC,0EAA0E,GAC1E,OAAO,MAAMC,wBAAwBD,QAAQE,OAAO,GAGhD;AAAoB"}
|
|
@@ -9,5 +9,5 @@ import { EntityIdTag } from '../../repository/entity-mixins.repository.js';
|
|
|
9
9
|
* the only shape a caller ever has at the point of deletion (a table row, a
|
|
10
10
|
* route param), so that is what this exposes.
|
|
11
11
|
*/
|
|
12
|
-
export declare function deleteUCFactory<TEntity extends Entity>(): Effect.Effect<void, import("@entifix/core").
|
|
12
|
+
export declare function deleteUCFactory<TEntity extends Entity>(): Effect.Effect<void, import("@entifix/core").TransitionalFailure, EntityRepositoryTag | EntityIdTag>;
|
|
13
13
|
//# sourceMappingURL=delete.uc.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delete.uc.d.ts","sourceRoot":"","sources":["../../../src/use-case/delete/delete.uc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,8CAA8C,CAAC;AAE3E;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,SAAS,MAAM,
|
|
1
|
+
{"version":3,"file":"delete.uc.d.ts","sourceRoot":"","sources":["../../../src/use-case/delete/delete.uc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,8CAA8C,CAAC;AAE3E;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,SAAS,MAAM,yGAOrD"}
|
|
@@ -8,5 +8,5 @@ import { EntityFacetCounterTag } from '../../repository/entity-facet-counter.js'
|
|
|
8
8
|
* entity's type, and the cast a load request needs to cross one is exactly
|
|
9
9
|
* what this use case has no reason to inherit.
|
|
10
10
|
*/
|
|
11
|
-
export declare const facetCountsUCFactory: <TEntity extends Entity>(queries: readonly EntityFacetQuery<TEntity>[]) => Effect.Effect<Readonly<Record<string, readonly import("@entifix/core").EntityFacetValue[]>>, import("@entifix/core").
|
|
11
|
+
export declare const facetCountsUCFactory: <TEntity extends Entity>(queries: readonly EntityFacetQuery<TEntity>[]) => Effect.Effect<Readonly<Record<string, readonly import("@entifix/core").EntityFacetValue[]>>, import("@entifix/core").TransitionalFailure, EntityFacetCounterTag>;
|
|
12
12
|
//# sourceMappingURL=facet-counts.uc.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"facet-counts.uc.d.ts","sourceRoot":"","sources":["../../../src/use-case/facets/facet-counts.uc.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AAEjF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,OAAO,SAAS,MAAM,EACzD,SAAS,SAAS,gBAAgB,CAAC,OAAO,CAAC,EAAE,
|
|
1
|
+
{"version":3,"file":"facet-counts.uc.d.ts","sourceRoot":"","sources":["../../../src/use-case/facets/facet-counts.uc.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AAEjF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,OAAO,SAAS,MAAM,EACzD,SAAS,SAAS,gBAAgB,CAAC,OAAO,CAAC,EAAE,qKAK3C,CAAC"}
|
|
@@ -8,5 +8,5 @@ import { EntityIdTag } from '../../repository/entity-mixins.repository.js';
|
|
|
8
8
|
* The factory's type parameter is what makes the result usable without a cast
|
|
9
9
|
* at the call site — {@link EntityIdTag} carries no entity type of its own.
|
|
10
10
|
*/
|
|
11
|
-
export declare function getUCFactory<TEntity extends Entity>(): Effect.Effect<TEntity, import("@entifix/core").
|
|
11
|
+
export declare function getUCFactory<TEntity extends Entity>(): Effect.Effect<TEntity, import("@entifix/core").TransitionalFailure, EntityRepositoryTag | EntityIdTag>;
|
|
12
12
|
//# sourceMappingURL=get.uc.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get.uc.d.ts","sourceRoot":"","sources":["../../../src/use-case/get/get.uc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,8CAA8C,CAAC;AAE3E;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,SAAS,MAAM,
|
|
1
|
+
{"version":3,"file":"get.uc.d.ts","sourceRoot":"","sources":["../../../src/use-case/get/get.uc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,8CAA8C,CAAC;AAE3E;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,SAAS,MAAM,4GASlD"}
|
|
@@ -15,5 +15,5 @@ export declare const LOAD_ALL_PAGE_SIZE = 100;
|
|
|
15
15
|
* as large as can be counted works against an in-memory source, but a real
|
|
16
16
|
* adapter would hand that size to its database.
|
|
17
17
|
*/
|
|
18
|
-
export declare function loadAllUCFactory<TEntity extends Entity>(pageSize?: number): Effect.Effect<TEntity[], import("@entifix/core").
|
|
18
|
+
export declare function loadAllUCFactory<TEntity extends Entity>(pageSize?: number): Effect.Effect<TEntity[], import("@entifix/core").TransitionalFailure, EntityRepositoryTag | EntityLoadRequestTag>;
|
|
19
19
|
//# sourceMappingURL=load-all.uc.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-all.uc.d.ts","sourceRoot":"","sources":["../../../src/use-case/load/load-all.uc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,8CAA8C,CAAC;AAEpF,4EAA4E;AAC5E,MAAM,MAAM,cAAc,CAAC,OAAO,SAAS,MAAM,IAAI,IAAI,CACvD,iBAAiB,CAAC,OAAO,CAAC,EAC1B,MAAM,GAAG,UAAU,CACpB,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,SAAS,MAAM,EACrD,QAAQ,GAAE,MAA2B,
|
|
1
|
+
{"version":3,"file":"load-all.uc.d.ts","sourceRoot":"","sources":["../../../src/use-case/load/load-all.uc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,8CAA8C,CAAC;AAEpF,4EAA4E;AAC5E,MAAM,MAAM,cAAc,CAAC,OAAO,SAAS,MAAM,IAAI,IAAI,CACvD,iBAAiB,CAAC,OAAO,CAAC,EAC1B,MAAM,GAAG,UAAU,CACpB,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,SAAS,MAAM,EACrD,QAAQ,GAAE,MAA2B,qHAuBtC"}
|
|
@@ -2,5 +2,5 @@ import { Entity } from '@entifix/core';
|
|
|
2
2
|
import { Effect } from 'effect';
|
|
3
3
|
import { EntityRepositoryTag } from '../../repository/entity.repository.js';
|
|
4
4
|
import { EntityLoadRequestTag } from '../../repository/entity-mixins.repository.js';
|
|
5
|
-
export declare function loadUCFactory<TEntity extends Entity>(): Effect.Effect<import("@entifix/core").EntityPage<TEntity>, import("@entifix/core").
|
|
5
|
+
export declare function loadUCFactory<TEntity extends Entity>(): Effect.Effect<import("@entifix/core").EntityPage<TEntity>, import("@entifix/core").TransitionalFailure, EntityRepositoryTag | EntityLoadRequestTag>;
|
|
6
6
|
//# sourceMappingURL=load.uc.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load.uc.d.ts","sourceRoot":"","sources":["../../../src/use-case/load/load.uc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAqB,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,8CAA8C,CAAC;AAEpF,wBAAgB,aAAa,CAAC,OAAO,SAAS,MAAM,
|
|
1
|
+
{"version":3,"file":"load.uc.d.ts","sourceRoot":"","sources":["../../../src/use-case/load/load.uc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAqB,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,8CAA8C,CAAC;AAEpF,wBAAgB,aAAa,CAAC,OAAO,SAAS,MAAM,yJAenD"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Entity, type TransitionalFailure } from '@entifix/core';
|
|
2
2
|
import { Effect } from 'effect';
|
|
3
3
|
import { EntityLinkResolverTag } from '../../repository/entity-link-resolver.repository.js';
|
|
4
4
|
/** The members of an entity that link to other records. */
|
|
@@ -16,5 +16,5 @@ export type LinkMember<TEntity> = {
|
|
|
16
16
|
* each through `targetOf`/`targetsOf`, with no `id → record` map built by
|
|
17
17
|
* hand. Every link resolves concurrently; one that fails fails the whole.
|
|
18
18
|
*/
|
|
19
|
-
export declare function resolveLinksUCFactory<TEntity extends Entity>(records: readonly TEntity[], members: readonly LinkMember<TEntity>[]): Effect.Effect<TEntity[],
|
|
19
|
+
export declare function resolveLinksUCFactory<TEntity extends Entity>(records: readonly TEntity[], members: readonly LinkMember<TEntity>[]): Effect.Effect<TEntity[], TransitionalFailure, EntityLinkResolverTag>;
|
|
20
20
|
//# sourceMappingURL=resolve-links.uc.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-links.uc.d.ts","sourceRoot":"","sources":["../../../src/use-case/load/resolve-links.uc.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,
|
|
1
|
+
{"version":3,"file":"resolve-links.uc.d.ts","sourceRoot":"","sources":["../../../src/use-case/load/resolve-links.uc.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,MAAM,EAEX,KAAK,mBAAmB,EACzB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,qBAAqB,EAAE,MAAM,qDAAqD,CAAC;AAE5F,2DAA2D;AAC3D,MAAM,MAAM,UAAU,CAAC,OAAO,IAAI;KAC/B,IAAI,IAAI,MAAM,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS;QACtD,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;QACpC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;KAC5B,GACG,IAAI,GACJ,KAAK;CACV,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC;AAQ1B;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,SAAS,MAAM,EAC1D,OAAO,EAAE,SAAS,OAAO,EAAE,EAC3B,OAAO,EAAE,SAAS,UAAU,CAAC,OAAO,CAAC,EAAE,wEAexC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/use-case/load/resolve-links.uc.ts"],"sourcesContent":["import {\n type
|
|
1
|
+
{"version":3,"sources":["../../../src/use-case/load/resolve-links.uc.ts"],"sourcesContent":["import {\n type Entity,\n type EntityLinkResolver,\n type TransitionalFailure,\n} from '@entifix/core';\nimport { Effect } from 'effect';\n\nimport { EntityLinkResolverTag } from '../../repository/entity-link-resolver.repository.js';\n\n/** The members of an entity that link to other records. */\nexport type LinkMember<TEntity> = {\n [TKey in keyof TEntity & string]: TEntity[TKey] extends {\n readonly entityConstructor: unknown;\n readonly isLoaded: boolean;\n }\n ? TKey\n : never;\n}[keyof TEntity & string];\n\ntype ResolvableLink = {\n resolve(\n resolver: EntityLinkResolver,\n ): Effect.Effect<unknown, TransitionalFailure>;\n};\n\n/**\n * Resolves `members` of every record, in place, through the\n * `EntityLinkResolverTag` in context, and hands the records back.\n *\n * A record read with its linked records (a post with its tags) then reads\n * each through `targetOf`/`targetsOf`, with no `id → record` map built by\n * hand. Every link resolves concurrently; one that fails fails the whole.\n */\nexport function resolveLinksUCFactory<TEntity extends Entity>(\n records: readonly TEntity[],\n members: readonly LinkMember<TEntity>[],\n) {\n return Effect.gen(function* () {\n const resolver = yield* EntityLinkResolverTag;\n yield* Effect.forEach(\n records.flatMap(record =>\n members.map(member =>\n (record[member] as ResolvableLink).resolve(resolver),\n ),\n ),\n resolved => resolved,\n { concurrency: 'unbounded', discard: true },\n );\n return [...records];\n });\n}\n"],"names":["Effect","EntityLinkResolverTag","resolveLinksUCFactory","records","members","gen","resolver","forEach","flatMap","record","map","member","resolve","resolved","concurrency","discard"],"mappings":"AAKA,SAASA,MAAM,QAAQ,SAAS;AAEhC,SAASC,qBAAqB,QAAQ,sDAAsD;AAkB5F;;;;;;;CAOC,GACD,OAAO,SAASC,sBACdC,OAA2B,EAC3BC,OAAuC;IAEvC,OAAOJ,OAAOK,GAAG,CAAC;QAChB,MAAMC,WAAW,OAAOL;QACxB,OAAOD,OAAOO,OAAO,CACnBJ,QAAQK,OAAO,CAACC,CAAAA,SACdL,QAAQM,GAAG,CAACC,CAAAA,SACV,AAACF,MAAM,CAACE,OAAO,CAAoBC,OAAO,CAACN,aAG/CO,CAAAA,WAAYA,UACZ;YAAEC,aAAa;YAAaC,SAAS;QAAK;QAE5C,OAAO;eAAIZ;SAAQ;IACrB;AACF"}
|
|
@@ -12,5 +12,5 @@ import { EntityTag } from '../../repository/entity-mixins.repository.js';
|
|
|
12
12
|
* taking them as arguments, so the same use-case saves over REST on the web and
|
|
13
13
|
* over Mongo on a backend.
|
|
14
14
|
*/
|
|
15
|
-
export declare function saveUCFactory<TEntity extends Entity>(): Effect.Effect<TEntity, [Effect.Effect<import("../../index.js").EntityRepository, never, EntityRepositoryTag> | Effect.Effect<Entity, never, EntityTag> | Effect.Effect<TEntity, import("@entifix/core").
|
|
15
|
+
export declare function saveUCFactory<TEntity extends Entity>(): Effect.Effect<TEntity, [Effect.Effect<import("../../index.js").EntityRepository, never, EntityRepositoryTag> | Effect.Effect<Entity, never, EntityTag> | Effect.Effect<TEntity, import("@entifix/core").TransitionalFailure, never>] extends [Effect.Effect<infer _A, infer E, infer _R>] ? E : never, [Effect.Effect<import("../../index.js").EntityRepository, never, EntityRepositoryTag> | Effect.Effect<Entity, never, EntityTag> | Effect.Effect<TEntity, import("@entifix/core").TransitionalFailure, never>] extends [Effect.Effect<infer _A_1, infer _E, infer R>] ? R : never>;
|
|
16
16
|
//# sourceMappingURL=save.uc.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"save.uc.d.ts","sourceRoot":"","sources":["../../../src/use-case/save/save.uc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAC5E,OAAO,EAAE,SAAS,EAAE,MAAM,8CAA8C,CAAC;AAEzE;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,OAAO,SAAS,MAAM,
|
|
1
|
+
{"version":3,"file":"save.uc.d.ts","sourceRoot":"","sources":["../../../src/use-case/save/save.uc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAC5E,OAAO,EAAE,SAAS,EAAE,MAAM,8CAA8C,CAAC;AAEzE;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,OAAO,SAAS,MAAM,8jBAWnD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entifix/business",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "The ports every adapter implements — repository, session, token, health — and the generic get, load, save and delete use-cases.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -51,12 +51,12 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@swc/helpers": "0.5.23",
|
|
54
|
-
"@entifix/core": "0.
|
|
54
|
+
"@entifix/core": "0.10.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"effect": "^4.0.
|
|
57
|
+
"effect": "^4.0.6"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"effect": "4.0.
|
|
60
|
+
"effect": "4.0.6"
|
|
61
61
|
}
|
|
62
62
|
}
|