@entifix/core 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/envelope/make-envelope.d.ts +7 -1
- package/dist/envelope/make-envelope.d.ts.map +1 -1
- package/dist/envelope/make-envelope.js +12 -0
- package/dist/envelope/make-envelope.js.map +1 -1
- package/dist/envelope/read-envelope.d.ts +7 -0
- package/dist/envelope/read-envelope.d.ts.map +1 -1
- package/dist/envelope/read-envelope.js +20 -0
- package/dist/envelope/read-envelope.js.map +1 -1
- package/dist/envelope/types.d.ts +3 -1
- package/dist/envelope/types.d.ts.map +1 -1
- package/dist/envelope/types.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/query/fold.d.ts +40 -0
- package/dist/query/fold.d.ts.map +1 -0
- package/dist/query/fold.js +49 -0
- package/dist/query/fold.js.map +1 -0
- package/dist/query/in-memory-query.d.ts +12 -1
- package/dist/query/in-memory-query.d.ts.map +1 -1
- package/dist/query/in-memory-query.js +33 -3
- package/dist/query/in-memory-query.js.map +1 -1
- package/dist/rsql/facet-query-url.d.ts +25 -0
- package/dist/rsql/facet-query-url.d.ts.map +1 -0
- package/dist/rsql/facet-query-url.js +63 -0
- package/dist/rsql/facet-query-url.js.map +1 -0
- package/dist/rsql/index.d.ts +1 -0
- package/dist/rsql/index.d.ts.map +1 -1
- package/dist/rsql/index.js +1 -0
- package/dist/rsql/index.js.map +1 -1
- package/dist/types/EntityFacets.d.ts +30 -0
- package/dist/types/EntityFacets.d.ts.map +1 -0
- package/dist/types/EntityFacets.js +10 -0
- package/dist/types/EntityFacets.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,9 +12,9 @@ pnpm add @entifix/core effect
|
|
|
12
12
|
|
|
13
13
|
## Entry points
|
|
14
14
|
|
|
15
|
-
| Import | What it holds
|
|
16
|
-
| --------------- |
|
|
17
|
-
| `@entifix/core` | Decorators (`entity`, `accessor`, `method`, `useCase`), metadata readers, serialization, envelopes, the RSQL parser and serializer, locales, wizard state, selection |
|
|
15
|
+
| Import | What it holds |
|
|
16
|
+
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
17
|
+
| `@entifix/core` | Decorators (`entity`, `accessor`, `method`, `useCase`), metadata readers, serialization, envelopes, the RSQL parser and serializer and the facet-count query protocol, the in-memory query (`like` ignores accents and case, by the rule in `foldText`) and `countFacets`, locales, wizard state, selection |
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { EntityMetadataDocument } from '../entity-definition/metadata/index.js';
|
|
2
2
|
import type { Entity, EntityConstructor } from '../types/Entity.js';
|
|
3
|
+
import type { EntityFacetCounts } from '../types/EntityFacets.js';
|
|
3
4
|
import type { EntityPage } from '../types/EntityPage.js';
|
|
4
|
-
import type { DomainEvent, EntifixEnvelope, EntifixEnvelopeLink, EntifixEnvelopeType, EntityCollectionEnvelope, EntityEnvelope, EntityMetadataEnvelope, EntityPageEnvelope, EventEnvelope } from './types.js';
|
|
5
|
+
import type { DomainEvent, EntifixEnvelope, EntifixEnvelopeLink, EntifixEnvelopeType, EntityCollectionEnvelope, EntityEnvelope, EntityFacetsEnvelope, EntityMetadataEnvelope, EntityPageEnvelope, EventEnvelope } from './types.js';
|
|
5
6
|
/**
|
|
6
7
|
* The transport-free envelope builder for payloads that are not serialized
|
|
7
8
|
* entities — `command`/`transactionRecord` messages carry their own `data`
|
|
@@ -28,6 +29,11 @@ export declare function makeEntityPageEnvelope<TEntity extends Entity>(entityCon
|
|
|
28
29
|
* makes the document per entity rather than per service.
|
|
29
30
|
*/
|
|
30
31
|
export declare function makeEntityMetadataEnvelope<TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, document: EntityMetadataDocument, links?: EntifixEnvelopeLink[]): EntityMetadataEnvelope;
|
|
32
|
+
/**
|
|
33
|
+
* The response to `GET /api/<entity>/$facets`: the counts as they are, since a
|
|
34
|
+
* count is a plain shape of values and numbers with nothing to serialize.
|
|
35
|
+
*/
|
|
36
|
+
export declare function makeEntityFacetsEnvelope<TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, counts: EntityFacetCounts): EntityFacetsEnvelope;
|
|
31
37
|
/**
|
|
32
38
|
* Frames a domain event for the bus.
|
|
33
39
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"make-envelope.d.ts","sourceRoot":"","sources":["../../src/envelope/make-envelope.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAKrF,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,wBAAwB,EACxB,cAAc,EACd,sBAAsB,EACtB,kBAAkB,EAClB,aAAa,EACd,MAAM,YAAY,CAAC;AAEpB;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAChC,IAAI,EAAE,mBAAmB,EACzB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,KAAK,EACX,KAAK,CAAC,EAAE,mBAAmB,EAAE,GAC5B,eAAe,CAAC,KAAK,CAAC,CASxB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,SAAS,MAAM,EACvD,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAC5C,MAAM,CAGR;AAED,wBAAgB,kBAAkB,CAAC,OAAO,SAAS,MAAM,EACvD,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,QAAQ,EAAE,OAAO,EACjB,KAAK,CAAC,EAAE,mBAAmB,EAAE,GAC5B,cAAc,CAShB;AAED,wBAAgB,4BAA4B,CAAC,OAAO,SAAS,MAAM,EACjE,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,SAAS,EAAE,SAAS,OAAO,EAAE,EAC7B,KAAK,CAAC,EAAE,mBAAmB,EAAE,GAC5B,wBAAwB,CAS1B;AAED,wBAAgB,sBAAsB,CAAC,OAAO,SAAS,MAAM,EAC3D,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,EACzB,KAAK,CAAC,EAAE,mBAAmB,EAAE,GAC5B,kBAAkB,CAAC,OAAO,CAAC,CAa7B;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,SAAS,MAAM,EAC/D,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,QAAQ,EAAE,sBAAsB,EAChC,KAAK,CAAC,EAAE,mBAAmB,EAAE,GAC5B,sBAAsB,CASxB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EACrC,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC,GACxB,aAAa,CAAC,KAAK,CAAC,CAMtB"}
|
|
1
|
+
{"version":3,"file":"make-envelope.d.ts","sourceRoot":"","sources":["../../src/envelope/make-envelope.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAKrF,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,mBAAmB,EACnB,mBAAmB,EACnB,wBAAwB,EACxB,cAAc,EACd,oBAAoB,EACpB,sBAAsB,EACtB,kBAAkB,EAClB,aAAa,EACd,MAAM,YAAY,CAAC;AAEpB;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAChC,IAAI,EAAE,mBAAmB,EACzB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,KAAK,EACX,KAAK,CAAC,EAAE,mBAAmB,EAAE,GAC5B,eAAe,CAAC,KAAK,CAAC,CASxB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,SAAS,MAAM,EACvD,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAC5C,MAAM,CAGR;AAED,wBAAgB,kBAAkB,CAAC,OAAO,SAAS,MAAM,EACvD,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,QAAQ,EAAE,OAAO,EACjB,KAAK,CAAC,EAAE,mBAAmB,EAAE,GAC5B,cAAc,CAShB;AAED,wBAAgB,4BAA4B,CAAC,OAAO,SAAS,MAAM,EACjE,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,SAAS,EAAE,SAAS,OAAO,EAAE,EAC7B,KAAK,CAAC,EAAE,mBAAmB,EAAE,GAC5B,wBAAwB,CAS1B;AAED,wBAAgB,sBAAsB,CAAC,OAAO,SAAS,MAAM,EAC3D,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,EACzB,KAAK,CAAC,EAAE,mBAAmB,EAAE,GAC5B,kBAAkB,CAAC,OAAO,CAAC,CAa7B;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,SAAS,MAAM,EAC/D,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,QAAQ,EAAE,sBAAsB,EAChC,KAAK,CAAC,EAAE,mBAAmB,EAAE,GAC5B,sBAAsB,CASxB;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,SAAS,MAAM,EAC7D,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,MAAM,EAAE,iBAAiB,GACxB,oBAAoB,CAQtB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EACrC,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC,GACxB,aAAa,CAAC,KAAK,CAAC,CAMtB"}
|
|
@@ -83,6 +83,18 @@ export function makeEntityPageEnvelope(entityConstructor, page, links) {
|
|
|
83
83
|
data: document
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* The response to `GET /api/<entity>/$facets`: the counts as they are, since a
|
|
88
|
+
* count is a plain shape of values and numbers with nothing to serialize.
|
|
89
|
+
*/ export function makeEntityFacetsEnvelope(entityConstructor, counts) {
|
|
90
|
+
return {
|
|
91
|
+
meta: {
|
|
92
|
+
type: 'entityFacets',
|
|
93
|
+
entity: envelopeEntityName(entityConstructor)
|
|
94
|
+
},
|
|
95
|
+
data: counts
|
|
96
|
+
};
|
|
97
|
+
}
|
|
86
98
|
/**
|
|
87
99
|
* Frames a domain event for the bus.
|
|
88
100
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/envelope/make-envelope.ts"],"sourcesContent":["import { extractMetaEntity } from '../entity-definition/helpers/index.js';\nimport type { EntityMetadataDocument } from '../entity-definition/metadata/index.js';\nimport {\n serializeEntity,\n serializeEntityCollection,\n} from '../entity-definition/serializer/index.js';\nimport type { Entity, EntityConstructor } from '../types/Entity.js';\nimport type { EntityPage } from '../types/EntityPage.js';\nimport type {\n DomainEvent,\n EntifixEnvelope,\n EntifixEnvelopeLink,\n EntifixEnvelopeType,\n EntityCollectionEnvelope,\n EntityEnvelope,\n EntityMetadataEnvelope,\n EntityPageEnvelope,\n EventEnvelope,\n} from './types.js';\n\n/**\n * The transport-free envelope builder for payloads that are not serialized\n * entities — `command`/`transactionRecord` messages carry their own `data`\n * shapes (defined by the transactions layer), so they cannot go through the\n * entity builders. `entity` is still the routing label (`meta.entity`): the\n * target entity's `key` for a command, or the entity/transaction subject for an\n * event.\n */\nexport function makeEnvelope<TData>(\n type: EntifixEnvelopeType,\n entity: string,\n data: TData,\n links?: EntifixEnvelopeLink[],\n): EntifixEnvelope<TData> {\n return {\n meta: {\n type,\n entity,\n ...(links ? { links } : {}),\n },\n data,\n };\n}\n\n/**\n * The entity name carried in `meta.entity` — the same `key ?? name` resolution\n * every adapter uses for collections and endpoints.\n */\nexport function envelopeEntityName<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n): string {\n const metaEntity = extractMetaEntity(entityConstructor);\n return metaEntity.key ?? metaEntity.name;\n}\n\nexport function makeEntityEnvelope<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n instance: TEntity,\n links?: EntifixEnvelopeLink[],\n): EntityEnvelope {\n return {\n meta: {\n type: 'entity',\n entity: envelopeEntityName(entityConstructor),\n ...(links ? { links } : {}),\n },\n data: serializeEntity(entityConstructor, instance),\n };\n}\n\nexport function makeEntityCollectionEnvelope<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n instances: readonly TEntity[],\n links?: EntifixEnvelopeLink[],\n): EntityCollectionEnvelope {\n return {\n meta: {\n type: 'entityCollection',\n entity: envelopeEntityName(entityConstructor),\n ...(links ? { links } : {}),\n },\n data: serializeEntityCollection(entityConstructor, instances),\n };\n}\n\nexport function makeEntityPageEnvelope<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n page: EntityPage<TEntity>,\n links?: EntifixEnvelopeLink[],\n): EntityPageEnvelope<TEntity> {\n return {\n meta: {\n type: 'entityPage',\n entity: envelopeEntityName(entityConstructor),\n ...(links ? { links } : {}),\n },\n data: {\n items: serializeEntityCollection(entityConstructor, page.items),\n total: page.total,\n request: page.request,\n },\n };\n}\n\n/**\n * The response to `GET /api/<entity>/$metadata`.\n *\n * Unlike its neighbours this serializes nothing: the document is already a plain\n * shape of catalog keys and verbs, and it describes the class rather than any\n * instance of it. `meta.entity` is still the target entity's key, which is what\n * makes the document per entity rather than per service.\n */\nexport function makeEntityMetadataEnvelope<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n document: EntityMetadataDocument,\n links?: EntifixEnvelopeLink[],\n): EntityMetadataEnvelope {\n return {\n meta: {\n type: 'entityMetadata',\n entity: envelopeEntityName(entityConstructor),\n ...(links ? { links } : {}),\n },\n data: document,\n };\n}\n\n/**\n * Frames a domain event for the bus.\n *\n * The split is mechanical and worth stating: everything about the *message*\n * (name, id, source, time, correlation) goes to `meta.event`; the payload — what\n * actually happened — is `data`. A consumer therefore never has to reach into\n * metadata to read its own domain object, and never has to reach into the\n * payload to find the routing key.\n *\n * `meta.entity` is deliberately left unset. A bus message says what it *is* in\n * `meta.event.name`; only the HTTP arm needs an entity label, and inventing one\n * here would put `settlement` on a message about a settlement *run*.\n */\nexport function makeEventEnvelope<TData>(\n event: DomainEvent<TData>,\n): EventEnvelope<TData> {\n const { data, ...eventMeta } = event;\n return {\n meta: { type: 'event', event: eventMeta },\n data,\n };\n}\n"],"names":["extractMetaEntity","serializeEntity","serializeEntityCollection","makeEnvelope","type","entity","data","links","meta","envelopeEntityName","entityConstructor","metaEntity","key","name","makeEntityEnvelope","instance","makeEntityCollectionEnvelope","instances","makeEntityPageEnvelope","page","items","total","request","makeEntityMetadataEnvelope","document","makeEventEnvelope","event","eventMeta"],"mappings":";;AAAA,SAASA,iBAAiB,QAAQ,wCAAwC;AAE1E,SACEC,eAAe,EACfC,yBAAyB,QACpB,2CAA2C;
|
|
1
|
+
{"version":3,"sources":["../../src/envelope/make-envelope.ts"],"sourcesContent":["import { extractMetaEntity } from '../entity-definition/helpers/index.js';\nimport type { EntityMetadataDocument } from '../entity-definition/metadata/index.js';\nimport {\n serializeEntity,\n serializeEntityCollection,\n} from '../entity-definition/serializer/index.js';\nimport type { Entity, EntityConstructor } from '../types/Entity.js';\nimport type { EntityFacetCounts } from '../types/EntityFacets.js';\nimport type { EntityPage } from '../types/EntityPage.js';\nimport type {\n DomainEvent,\n EntifixEnvelope,\n EntifixEnvelopeLink,\n EntifixEnvelopeType,\n EntityCollectionEnvelope,\n EntityEnvelope,\n EntityFacetsEnvelope,\n EntityMetadataEnvelope,\n EntityPageEnvelope,\n EventEnvelope,\n} from './types.js';\n\n/**\n * The transport-free envelope builder for payloads that are not serialized\n * entities — `command`/`transactionRecord` messages carry their own `data`\n * shapes (defined by the transactions layer), so they cannot go through the\n * entity builders. `entity` is still the routing label (`meta.entity`): the\n * target entity's `key` for a command, or the entity/transaction subject for an\n * event.\n */\nexport function makeEnvelope<TData>(\n type: EntifixEnvelopeType,\n entity: string,\n data: TData,\n links?: EntifixEnvelopeLink[],\n): EntifixEnvelope<TData> {\n return {\n meta: {\n type,\n entity,\n ...(links ? { links } : {}),\n },\n data,\n };\n}\n\n/**\n * The entity name carried in `meta.entity` — the same `key ?? name` resolution\n * every adapter uses for collections and endpoints.\n */\nexport function envelopeEntityName<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n): string {\n const metaEntity = extractMetaEntity(entityConstructor);\n return metaEntity.key ?? metaEntity.name;\n}\n\nexport function makeEntityEnvelope<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n instance: TEntity,\n links?: EntifixEnvelopeLink[],\n): EntityEnvelope {\n return {\n meta: {\n type: 'entity',\n entity: envelopeEntityName(entityConstructor),\n ...(links ? { links } : {}),\n },\n data: serializeEntity(entityConstructor, instance),\n };\n}\n\nexport function makeEntityCollectionEnvelope<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n instances: readonly TEntity[],\n links?: EntifixEnvelopeLink[],\n): EntityCollectionEnvelope {\n return {\n meta: {\n type: 'entityCollection',\n entity: envelopeEntityName(entityConstructor),\n ...(links ? { links } : {}),\n },\n data: serializeEntityCollection(entityConstructor, instances),\n };\n}\n\nexport function makeEntityPageEnvelope<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n page: EntityPage<TEntity>,\n links?: EntifixEnvelopeLink[],\n): EntityPageEnvelope<TEntity> {\n return {\n meta: {\n type: 'entityPage',\n entity: envelopeEntityName(entityConstructor),\n ...(links ? { links } : {}),\n },\n data: {\n items: serializeEntityCollection(entityConstructor, page.items),\n total: page.total,\n request: page.request,\n },\n };\n}\n\n/**\n * The response to `GET /api/<entity>/$metadata`.\n *\n * Unlike its neighbours this serializes nothing: the document is already a plain\n * shape of catalog keys and verbs, and it describes the class rather than any\n * instance of it. `meta.entity` is still the target entity's key, which is what\n * makes the document per entity rather than per service.\n */\nexport function makeEntityMetadataEnvelope<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n document: EntityMetadataDocument,\n links?: EntifixEnvelopeLink[],\n): EntityMetadataEnvelope {\n return {\n meta: {\n type: 'entityMetadata',\n entity: envelopeEntityName(entityConstructor),\n ...(links ? { links } : {}),\n },\n data: document,\n };\n}\n\n/**\n * The response to `GET /api/<entity>/$facets`: the counts as they are, since a\n * count is a plain shape of values and numbers with nothing to serialize.\n */\nexport function makeEntityFacetsEnvelope<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n counts: EntityFacetCounts,\n): EntityFacetsEnvelope {\n return {\n meta: {\n type: 'entityFacets',\n entity: envelopeEntityName(entityConstructor),\n },\n data: counts,\n };\n}\n\n/**\n * Frames a domain event for the bus.\n *\n * The split is mechanical and worth stating: everything about the *message*\n * (name, id, source, time, correlation) goes to `meta.event`; the payload — what\n * actually happened — is `data`. A consumer therefore never has to reach into\n * metadata to read its own domain object, and never has to reach into the\n * payload to find the routing key.\n *\n * `meta.entity` is deliberately left unset. A bus message says what it *is* in\n * `meta.event.name`; only the HTTP arm needs an entity label, and inventing one\n * here would put `settlement` on a message about a settlement *run*.\n */\nexport function makeEventEnvelope<TData>(\n event: DomainEvent<TData>,\n): EventEnvelope<TData> {\n const { data, ...eventMeta } = event;\n return {\n meta: { type: 'event', event: eventMeta },\n data,\n };\n}\n"],"names":["extractMetaEntity","serializeEntity","serializeEntityCollection","makeEnvelope","type","entity","data","links","meta","envelopeEntityName","entityConstructor","metaEntity","key","name","makeEntityEnvelope","instance","makeEntityCollectionEnvelope","instances","makeEntityPageEnvelope","page","items","total","request","makeEntityMetadataEnvelope","document","makeEntityFacetsEnvelope","counts","makeEventEnvelope","event","eventMeta"],"mappings":";;AAAA,SAASA,iBAAiB,QAAQ,wCAAwC;AAE1E,SACEC,eAAe,EACfC,yBAAyB,QACpB,2CAA2C;AAiBlD;;;;;;;CAOC,GACD,OAAO,SAASC,aACdC,IAAyB,EACzBC,MAAc,EACdC,IAAW,EACXC,KAA6B;IAE7B,OAAO;QACLC,MAAM;YACJJ;YACAC;WACIE,QAAQ;YAAEA;QAAM,IAAI,CAAC;QAE3BD;IACF;AACF;AAEA;;;CAGC,GACD,OAAO,SAASG,mBACdC,iBAA6C;QAGtCC;IADP,MAAMA,aAAaX,kBAAkBU;IACrC,QAAOC,kBAAAA,WAAWC,GAAG,YAAdD,kBAAkBA,WAAWE,IAAI;AAC1C;AAEA,OAAO,SAASC,mBACdJ,iBAA6C,EAC7CK,QAAiB,EACjBR,KAA6B;IAE7B,OAAO;QACLC,MAAM;YACJJ,MAAM;YACNC,QAAQI,mBAAmBC;WACvBH,QAAQ;YAAEA;QAAM,IAAI,CAAC;QAE3BD,MAAML,gBAAgBS,mBAAmBK;IAC3C;AACF;AAEA,OAAO,SAASC,6BACdN,iBAA6C,EAC7CO,SAA6B,EAC7BV,KAA6B;IAE7B,OAAO;QACLC,MAAM;YACJJ,MAAM;YACNC,QAAQI,mBAAmBC;WACvBH,QAAQ;YAAEA;QAAM,IAAI,CAAC;QAE3BD,MAAMJ,0BAA0BQ,mBAAmBO;IACrD;AACF;AAEA,OAAO,SAASC,uBACdR,iBAA6C,EAC7CS,IAAyB,EACzBZ,KAA6B;IAE7B,OAAO;QACLC,MAAM;YACJJ,MAAM;YACNC,QAAQI,mBAAmBC;WACvBH,QAAQ;YAAEA;QAAM,IAAI,CAAC;QAE3BD,MAAM;YACJc,OAAOlB,0BAA0BQ,mBAAmBS,KAAKC,KAAK;YAC9DC,OAAOF,KAAKE,KAAK;YACjBC,SAASH,KAAKG,OAAO;QACvB;IACF;AACF;AAEA;;;;;;;CAOC,GACD,OAAO,SAASC,2BACdb,iBAA6C,EAC7Cc,QAAgC,EAChCjB,KAA6B;IAE7B,OAAO;QACLC,MAAM;YACJJ,MAAM;YACNC,QAAQI,mBAAmBC;WACvBH,QAAQ;YAAEA;QAAM,IAAI,CAAC;QAE3BD,MAAMkB;IACR;AACF;AAEA;;;CAGC,GACD,OAAO,SAASC,yBACdf,iBAA6C,EAC7CgB,MAAyB;IAEzB,OAAO;QACLlB,MAAM;YACJJ,MAAM;YACNC,QAAQI,mBAAmBC;QAC7B;QACAJ,MAAMoB;IACR;AACF;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,kBACdC,KAAyB;IAEzB,MAAM,EAAEtB,IAAI,EAAgB,GAAGsB,OAAdC,6CAAcD;;;IAC/B,OAAO;QACLpB,MAAM;YAAEJ,MAAM;YAASwB,OAAOC;QAAU;QACxCvB;IACF;AACF"}
|
|
@@ -2,6 +2,7 @@ import { Effect } from 'effect';
|
|
|
2
2
|
import { EntifixBuildError } from '../base-entities/entifix-error/index.js';
|
|
3
3
|
import type { EntityMetadataDocument } from '../entity-definition/metadata/index.js';
|
|
4
4
|
import type { Entity, EntityConstructor } from '../types/Entity.js';
|
|
5
|
+
import type { EntityFacetValue } from '../types/EntityFacets.js';
|
|
5
6
|
import type { DomainEvent, EntifixEnvelope, EntifixEnvelopeType } from './types.js';
|
|
6
7
|
/**
|
|
7
8
|
* Structural check only — `meta.type` is validated separately so a mismatch
|
|
@@ -30,6 +31,12 @@ export declare const readEntityPageEnvelope: <TEntity extends Entity>(entityCons
|
|
|
30
31
|
* the way a half-populated entity does.
|
|
31
32
|
*/
|
|
32
33
|
export declare const readEntityMetadataEnvelope: <TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, body: unknown) => Effect.Effect<EntityMetadataDocument, EntifixBuildError, never>;
|
|
34
|
+
/**
|
|
35
|
+
* Reads a `$facets` response back into its counts, refusing a body whose lists
|
|
36
|
+
* are not `{ value, count }` rows — a panel that rendered a malformed count
|
|
37
|
+
* would show a number nobody computed.
|
|
38
|
+
*/
|
|
39
|
+
export declare const readEntityFacetsEnvelope: <TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, body: unknown) => Effect.Effect<Readonly<Record<string, readonly EntityFacetValue[]>>, EntifixBuildError, never>;
|
|
33
40
|
/**
|
|
34
41
|
* Reads a bus message back into a {@link DomainEvent}.
|
|
35
42
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read-envelope.d.ts","sourceRoot":"","sources":["../../src/envelope/read-envelope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,iBAAiB,EAAE,MAAM,yCAAyC,CAAC;AAC5E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAKrF,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"read-envelope.d.ts","sourceRoot":"","sources":["../../src/envelope/read-envelope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,iBAAiB,EAAE,MAAM,yCAAyC,CAAC;AAC5E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAKrF,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,KAAK,EAEV,gBAAgB,EACjB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,mBAAmB,EAGpB,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,eAAe,CAQxE;AAmCD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAChC,IAAI,EAAE,OAAO,EACb,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,SAAY,GAChB,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,iBAAiB,CAAC,CAoB1D;AAED,eAAO,MAAM,kBAAkB,GAAI,OAAO,SAAS,MAAM,EACvD,mBAAmB,iBAAiB,CAAC,OAAO,CAAC,EAC7C,MAAM,OAAO,qDAwBX,CAAC;AAEL,eAAO,MAAM,4BAA4B,GAAI,OAAO,SAAS,MAAM,EACjE,mBAAmB,iBAAiB,CAAC,OAAO,CAAC,EAC7C,MAAM,OAAO,uDAaX,CAAC;AAEL,eAAO,MAAM,sBAAsB,GAAI,OAAO,SAAS,MAAM,EAC3D,mBAAmB,iBAAiB,CAAC,OAAO,CAAC,EAC7C,MAAM,OAAO;WAYO,OAAO,EAAE;;;4BAI3B,CAAC;AAEL;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,GAAI,OAAO,SAAS,MAAM,EAC/D,mBAAmB,iBAAiB,CAAC,OAAO,CAAC,EAC7C,MAAM,OAAO,oEAwBX,CAAC;AAUL;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GAAI,OAAO,SAAS,MAAM,EAC7D,mBAAmB,iBAAiB,CAAC,OAAO,CAAC,EAC7C,MAAM,OAAO,mGA2BX,CAAC;AAKL;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,GAAG,OAAO,EAC/C,IAAI,EAAE,OAAO,GACZ,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,iBAAiB,CAAC,CAoBtD"}
|
|
@@ -99,6 +99,26 @@ export const readEntityPageEnvelope = (entityConstructor, body)=>Effect.gen(func
|
|
|
99
99
|
}
|
|
100
100
|
return document;
|
|
101
101
|
});
|
|
102
|
+
const isFacetValueRow = (row)=>row != null && typeof row === 'object' && [
|
|
103
|
+
'string',
|
|
104
|
+
'number',
|
|
105
|
+
'boolean'
|
|
106
|
+
].includes(typeof row.value) && typeof row.count === 'number';
|
|
107
|
+
/**
|
|
108
|
+
* Reads a `$facets` response back into its counts, refusing a body whose lists
|
|
109
|
+
* are not `{ value, count }` rows — a panel that rendered a malformed count
|
|
110
|
+
* would show a number nobody computed.
|
|
111
|
+
*/ export const readEntityFacetsEnvelope = (entityConstructor, body)=>Effect.gen(function*() {
|
|
112
|
+
const envelope = yield* assertEnvelope(entityConstructor, body, 'entityFacets');
|
|
113
|
+
const counts = envelope.data;
|
|
114
|
+
const wellFormed = counts != null && typeof counts === 'object' && Object.values(counts).every((rows)=>Array.isArray(rows) && rows.every(isFacetValueRow));
|
|
115
|
+
if (!wellFormed) {
|
|
116
|
+
return yield* Effect.fail(new EntifixBuildError(`EntifixEnvelope for "${envelopeEntityName(entityConstructor)}" carried no facet counts`, undefined, {
|
|
117
|
+
entity: envelopeEntityName(entityConstructor)
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
return counts;
|
|
121
|
+
});
|
|
102
122
|
/** Every member `meta.event` must carry for a message to be routable at all. */ const REQUIRED_EVENT_META = [
|
|
103
123
|
'name',
|
|
104
124
|
'id',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/envelope/read-envelope.ts"],"sourcesContent":["import { Effect } from 'effect';\n\nimport { EntifixBuildError } from '../base-entities/entifix-error/index.js';\nimport type { EntityMetadataDocument } from '../entity-definition/metadata/index.js';\nimport {\n deserializeEntityCollection,\n deserializeSingleEntity,\n} from '../entity-definition/serializer/index.js';\nimport type { Entity, EntityConstructor } from '../types/Entity.js';\nimport type { EntityPage } from '../types/EntityPage.js';\nimport { envelopeEntityName } from './make-envelope.js';\nimport type {\n DomainEvent,\n EntifixEnvelope,\n EntifixEnvelopeType,\n EntifixEventMeta,\n SerializedEntityPage,\n} from './types.js';\n\n/**\n * Structural check only — `meta.type` is validated separately so a mismatch\n * reports the expected/actual types rather than a generic \"not an envelope\".\n */\nexport function isEntifixEnvelope(body: unknown): body is EntifixEnvelope {\n if (body == null || typeof body !== 'object') {\n return false;\n }\n const meta = (body as EntifixEnvelope).meta;\n return (\n meta != null && typeof meta === 'object' && typeof meta.type === 'string'\n );\n}\n\n/**\n * Narrows an arbitrary body to an envelope of the expected `meta.type`, failing\n * with an {@link EntifixBuildError} otherwise. A wrong shape must never\n * silently deserialize into a half-populated entity.\n */\nfunction assertEnvelope<TEntity extends Entity, TData>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n expected: EntifixEnvelopeType,\n): Effect.Effect<EntifixEnvelope<TData>, EntifixBuildError> {\n const entity = envelopeEntityName(entityConstructor);\n\n if (!isEntifixEnvelope(body)) {\n return Effect.fail(\n new EntifixBuildError(\n `Expected an EntifixEnvelope for \"${entity}\" but the payload carried no meta.type`,\n undefined,\n { entity, expected, body },\n ),\n );\n }\n if (body.meta.type !== expected) {\n return Effect.fail(\n new EntifixBuildError(\n `Expected an EntifixEnvelope of type \"${expected}\" for \"${entity}\" but got \"${body.meta.type}\"`,\n undefined,\n { entity, expected, actual: body.meta.type },\n ),\n );\n }\n return Effect.succeed(body as EntifixEnvelope<TData>);\n}\n\n/**\n * Narrows an arbitrary body to an envelope of the expected `meta.type` without\n * requiring an entity constructor — for `command`/`transactionRecord` messages\n * whose `data` is not a serialized entity. `label` only sharpens error text.\n */\nexport function readEnvelope<TData>(\n body: unknown,\n expected: EntifixEnvelopeType,\n label = 'message',\n): Effect.Effect<EntifixEnvelope<TData>, EntifixBuildError> {\n if (!isEntifixEnvelope(body)) {\n return Effect.fail(\n new EntifixBuildError(\n `Expected an EntifixEnvelope for \"${label}\" but the payload carried no meta.type`,\n undefined,\n { label, expected, body },\n ),\n );\n }\n if (body.meta.type !== expected) {\n return Effect.fail(\n new EntifixBuildError(\n `Expected an EntifixEnvelope of type \"${expected}\" for \"${label}\" but got \"${body.meta.type}\"`,\n undefined,\n { label, expected, actual: body.meta.type },\n ),\n );\n }\n return Effect.succeed(body as EntifixEnvelope<TData>);\n}\n\nexport const readEntityEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, unknown>(\n entityConstructor,\n body,\n 'entity',\n );\n const entity = yield* deserializeSingleEntity(\n entityConstructor,\n envelope.data,\n );\n if (entity === undefined) {\n return yield* Effect.fail(\n new EntifixBuildError(\n `EntifixEnvelope for \"${envelopeEntityName(\n entityConstructor,\n )}\" carried no data`,\n undefined,\n { entity: envelopeEntityName(entityConstructor) },\n ),\n );\n }\n return entity;\n });\n\nexport const readEntityCollectionEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, unknown>(\n entityConstructor,\n body,\n 'entityCollection',\n );\n const items = yield* deserializeEntityCollection(\n entityConstructor,\n envelope.data,\n );\n return items as TEntity[];\n });\n\nexport const readEntityPageEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<\n TEntity,\n SerializedEntityPage<TEntity>\n >(entityConstructor, body, 'entityPage');\n const items = yield* deserializeEntityCollection(\n entityConstructor,\n envelope.data?.items,\n );\n return {\n items: items as TEntity[],\n total: envelope.data?.total ?? 0,\n request: envelope.data?.request ?? {},\n } satisfies EntityPage<TEntity>;\n });\n\n/**\n * Reads a `$metadata` response back into its document.\n *\n * No deserialization step, and no defaulting of the members either: an envelope\n * whose `data` is missing a list would leave a form guessing whether \"no\n * actions\" meant \"denied\" or \"malformed\", so a broken payload fails loudly here\n * the way a half-populated entity does.\n */\nexport const readEntityMetadataEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, EntityMetadataDocument>(\n entityConstructor,\n body,\n 'entityMetadata',\n );\n const document = envelope.data;\n if (\n !Array.isArray(document?.actions) ||\n !Array.isArray(document?.useCases)\n ) {\n return yield* Effect.fail(\n new EntifixBuildError(\n `EntifixEnvelope for \"${envelopeEntityName(\n entityConstructor,\n )}\" carried no metadata document`,\n undefined,\n { entity: envelopeEntityName(entityConstructor) },\n ),\n );\n }\n return document;\n });\n\n/** Every member `meta.event` must carry for a message to be routable at all. */\nconst REQUIRED_EVENT_META = ['name', 'id', 'source', 'at'] as const;\n\n/**\n * Reads a bus message back into a {@link DomainEvent}.\n *\n * The metadata is validated rather than defaulted, and that is the point: a\n * message missing `id` cannot be deduplicated and a message missing `name`\n * cannot be routed, so both are failures a consumer must see rather than\n * absences it silently works around. It is the same call\n * {@link readEntityMetadataEnvelope} makes — a broken payload fails loudly\n * instead of arriving half-populated.\n *\n * Note this does not validate `data`. The transport has no idea what a\n * `catalog.published` payload should look like, and inventing an opinion here\n * is how the bus would start knowing about domains.\n */\nexport function readEventEnvelope<TData = unknown>(\n body: unknown,\n): Effect.Effect<DomainEvent<TData>, EntifixBuildError> {\n return Effect.gen(function* () {\n const envelope = yield* readEnvelope<TData>(body, 'event', 'event');\n const event = envelope.meta.event;\n const missing = REQUIRED_EVENT_META.filter(\n member => typeof event?.[member] !== 'string' || event[member] === '',\n );\n if (missing.length > 0) {\n return yield* Effect.fail(\n new EntifixBuildError(\n `event envelope carried incomplete meta.event (missing ${missing.join(\n ', ',\n )})`,\n undefined,\n { missing, body },\n ),\n );\n }\n return { ...(event as EntifixEventMeta), data: envelope.data };\n });\n}\n"],"names":["Effect","EntifixBuildError","deserializeEntityCollection","deserializeSingleEntity","envelopeEntityName","isEntifixEnvelope","body","meta","type","assertEnvelope","entityConstructor","expected","entity","fail","undefined","actual","succeed","readEnvelope","label","readEntityEnvelope","gen","envelope","data","readEntityCollectionEnvelope","items","readEntityPageEnvelope","total","request","readEntityMetadataEnvelope","document","Array","isArray","actions","useCases","REQUIRED_EVENT_META","readEventEnvelope","event","missing","filter","member","length","join"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/envelope/read-envelope.ts"],"sourcesContent":["import { Effect } from 'effect';\n\nimport { EntifixBuildError } from '../base-entities/entifix-error/index.js';\nimport type { EntityMetadataDocument } from '../entity-definition/metadata/index.js';\nimport {\n deserializeEntityCollection,\n deserializeSingleEntity,\n} from '../entity-definition/serializer/index.js';\nimport type { Entity, EntityConstructor } from '../types/Entity.js';\nimport type {\n EntityFacetCounts,\n EntityFacetValue,\n} from '../types/EntityFacets.js';\nimport type { EntityPage } from '../types/EntityPage.js';\nimport { envelopeEntityName } from './make-envelope.js';\nimport type {\n DomainEvent,\n EntifixEnvelope,\n EntifixEnvelopeType,\n EntifixEventMeta,\n SerializedEntityPage,\n} from './types.js';\n\n/**\n * Structural check only — `meta.type` is validated separately so a mismatch\n * reports the expected/actual types rather than a generic \"not an envelope\".\n */\nexport function isEntifixEnvelope(body: unknown): body is EntifixEnvelope {\n if (body == null || typeof body !== 'object') {\n return false;\n }\n const meta = (body as EntifixEnvelope).meta;\n return (\n meta != null && typeof meta === 'object' && typeof meta.type === 'string'\n );\n}\n\n/**\n * Narrows an arbitrary body to an envelope of the expected `meta.type`, failing\n * with an {@link EntifixBuildError} otherwise. A wrong shape must never\n * silently deserialize into a half-populated entity.\n */\nfunction assertEnvelope<TEntity extends Entity, TData>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n expected: EntifixEnvelopeType,\n): Effect.Effect<EntifixEnvelope<TData>, EntifixBuildError> {\n const entity = envelopeEntityName(entityConstructor);\n\n if (!isEntifixEnvelope(body)) {\n return Effect.fail(\n new EntifixBuildError(\n `Expected an EntifixEnvelope for \"${entity}\" but the payload carried no meta.type`,\n undefined,\n { entity, expected, body },\n ),\n );\n }\n if (body.meta.type !== expected) {\n return Effect.fail(\n new EntifixBuildError(\n `Expected an EntifixEnvelope of type \"${expected}\" for \"${entity}\" but got \"${body.meta.type}\"`,\n undefined,\n { entity, expected, actual: body.meta.type },\n ),\n );\n }\n return Effect.succeed(body as EntifixEnvelope<TData>);\n}\n\n/**\n * Narrows an arbitrary body to an envelope of the expected `meta.type` without\n * requiring an entity constructor — for `command`/`transactionRecord` messages\n * whose `data` is not a serialized entity. `label` only sharpens error text.\n */\nexport function readEnvelope<TData>(\n body: unknown,\n expected: EntifixEnvelopeType,\n label = 'message',\n): Effect.Effect<EntifixEnvelope<TData>, EntifixBuildError> {\n if (!isEntifixEnvelope(body)) {\n return Effect.fail(\n new EntifixBuildError(\n `Expected an EntifixEnvelope for \"${label}\" but the payload carried no meta.type`,\n undefined,\n { label, expected, body },\n ),\n );\n }\n if (body.meta.type !== expected) {\n return Effect.fail(\n new EntifixBuildError(\n `Expected an EntifixEnvelope of type \"${expected}\" for \"${label}\" but got \"${body.meta.type}\"`,\n undefined,\n { label, expected, actual: body.meta.type },\n ),\n );\n }\n return Effect.succeed(body as EntifixEnvelope<TData>);\n}\n\nexport const readEntityEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, unknown>(\n entityConstructor,\n body,\n 'entity',\n );\n const entity = yield* deserializeSingleEntity(\n entityConstructor,\n envelope.data,\n );\n if (entity === undefined) {\n return yield* Effect.fail(\n new EntifixBuildError(\n `EntifixEnvelope for \"${envelopeEntityName(\n entityConstructor,\n )}\" carried no data`,\n undefined,\n { entity: envelopeEntityName(entityConstructor) },\n ),\n );\n }\n return entity;\n });\n\nexport const readEntityCollectionEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, unknown>(\n entityConstructor,\n body,\n 'entityCollection',\n );\n const items = yield* deserializeEntityCollection(\n entityConstructor,\n envelope.data,\n );\n return items as TEntity[];\n });\n\nexport const readEntityPageEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<\n TEntity,\n SerializedEntityPage<TEntity>\n >(entityConstructor, body, 'entityPage');\n const items = yield* deserializeEntityCollection(\n entityConstructor,\n envelope.data?.items,\n );\n return {\n items: items as TEntity[],\n total: envelope.data?.total ?? 0,\n request: envelope.data?.request ?? {},\n } satisfies EntityPage<TEntity>;\n });\n\n/**\n * Reads a `$metadata` response back into its document.\n *\n * No deserialization step, and no defaulting of the members either: an envelope\n * whose `data` is missing a list would leave a form guessing whether \"no\n * actions\" meant \"denied\" or \"malformed\", so a broken payload fails loudly here\n * the way a half-populated entity does.\n */\nexport const readEntityMetadataEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, EntityMetadataDocument>(\n entityConstructor,\n body,\n 'entityMetadata',\n );\n const document = envelope.data;\n if (\n !Array.isArray(document?.actions) ||\n !Array.isArray(document?.useCases)\n ) {\n return yield* Effect.fail(\n new EntifixBuildError(\n `EntifixEnvelope for \"${envelopeEntityName(\n entityConstructor,\n )}\" carried no metadata document`,\n undefined,\n { entity: envelopeEntityName(entityConstructor) },\n ),\n );\n }\n return document;\n });\n\nconst isFacetValueRow = (row: unknown): row is EntityFacetValue =>\n row != null &&\n typeof row === 'object' &&\n ['string', 'number', 'boolean'].includes(\n typeof (row as EntityFacetValue).value,\n ) &&\n typeof (row as EntityFacetValue).count === 'number';\n\n/**\n * Reads a `$facets` response back into its counts, refusing a body whose lists\n * are not `{ value, count }` rows — a panel that rendered a malformed count\n * would show a number nobody computed.\n */\nexport const readEntityFacetsEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, EntityFacetCounts>(\n entityConstructor,\n body,\n 'entityFacets',\n );\n const counts = envelope.data;\n const wellFormed =\n counts != null &&\n typeof counts === 'object' &&\n Object.values(counts).every(\n rows => Array.isArray(rows) && rows.every(isFacetValueRow),\n );\n if (!wellFormed) {\n return yield* Effect.fail(\n new EntifixBuildError(\n `EntifixEnvelope for \"${envelopeEntityName(\n entityConstructor,\n )}\" carried no facet counts`,\n undefined,\n { entity: envelopeEntityName(entityConstructor) },\n ),\n );\n }\n return counts;\n });\n\n/** Every member `meta.event` must carry for a message to be routable at all. */\nconst REQUIRED_EVENT_META = ['name', 'id', 'source', 'at'] as const;\n\n/**\n * Reads a bus message back into a {@link DomainEvent}.\n *\n * The metadata is validated rather than defaulted, and that is the point: a\n * message missing `id` cannot be deduplicated and a message missing `name`\n * cannot be routed, so both are failures a consumer must see rather than\n * absences it silently works around. It is the same call\n * {@link readEntityMetadataEnvelope} makes — a broken payload fails loudly\n * instead of arriving half-populated.\n *\n * Note this does not validate `data`. The transport has no idea what a\n * `catalog.published` payload should look like, and inventing an opinion here\n * is how the bus would start knowing about domains.\n */\nexport function readEventEnvelope<TData = unknown>(\n body: unknown,\n): Effect.Effect<DomainEvent<TData>, EntifixBuildError> {\n return Effect.gen(function* () {\n const envelope = yield* readEnvelope<TData>(body, 'event', 'event');\n const event = envelope.meta.event;\n const missing = REQUIRED_EVENT_META.filter(\n member => typeof event?.[member] !== 'string' || event[member] === '',\n );\n if (missing.length > 0) {\n return yield* Effect.fail(\n new EntifixBuildError(\n `event envelope carried incomplete meta.event (missing ${missing.join(\n ', ',\n )})`,\n undefined,\n { missing, body },\n ),\n );\n }\n return { ...(event as EntifixEventMeta), data: envelope.data };\n });\n}\n"],"names":["Effect","EntifixBuildError","deserializeEntityCollection","deserializeSingleEntity","envelopeEntityName","isEntifixEnvelope","body","meta","type","assertEnvelope","entityConstructor","expected","entity","fail","undefined","actual","succeed","readEnvelope","label","readEntityEnvelope","gen","envelope","data","readEntityCollectionEnvelope","items","readEntityPageEnvelope","total","request","readEntityMetadataEnvelope","document","Array","isArray","actions","useCases","isFacetValueRow","row","includes","value","count","readEntityFacetsEnvelope","counts","wellFormed","Object","values","every","rows","REQUIRED_EVENT_META","readEventEnvelope","event","missing","filter","member","length","join"],"mappings":"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"}
|
package/dist/envelope/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EntityMetadataDocument } from '../entity-definition/metadata/index.js';
|
|
2
2
|
import type { SerializedEntity } from '../entity-definition/serializer/index.js';
|
|
3
3
|
import type { Entity, EntityId } from '../types/Entity.js';
|
|
4
|
+
import type { EntityFacetCounts } from '../types/EntityFacets.js';
|
|
4
5
|
import type { EntityLoadRequest } from '../types/EntityLoadRequest.js';
|
|
5
6
|
/**
|
|
6
7
|
* Discriminates what {@link EntifixEnvelope.data} carries.
|
|
@@ -35,7 +36,7 @@ import type { EntityLoadRequest } from '../types/EntityLoadRequest.js';
|
|
|
35
36
|
* both the service that computes it and the controls that render it are already
|
|
36
37
|
* below the layer that would otherwise host it.
|
|
37
38
|
*/
|
|
38
|
-
export type EntifixEnvelopeType = 'entity' | 'entityCollection' | 'entityPage' | 'command' | 'event' | 'transactionAccepted' | 'transactionRecord' | 'sagaResult' | 'sagaInstance' | 'entityMetadata';
|
|
39
|
+
export type EntifixEnvelopeType = 'entity' | 'entityCollection' | 'entityPage' | 'command' | 'event' | 'transactionAccepted' | 'transactionRecord' | 'sagaResult' | 'sagaInstance' | 'entityMetadata' | 'entityFacets';
|
|
39
40
|
export type EntifixEnvelopeMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
40
41
|
/**
|
|
41
42
|
* A HATEOAS affordance: where the peer can go next from this message. Optional —
|
|
@@ -159,6 +160,7 @@ export type EntityEnvelope = EntifixEnvelope<SerializedEntity>;
|
|
|
159
160
|
export type EntityCollectionEnvelope = EntifixEnvelope<SerializedEntity[]>;
|
|
160
161
|
export type EntityPageEnvelope<TEntity extends Entity = Entity> = EntifixEnvelope<SerializedEntityPage<TEntity>>;
|
|
161
162
|
export type EntityMetadataEnvelope = EntifixEnvelope<EntityMetadataDocument>;
|
|
163
|
+
export type EntityFacetsEnvelope = EntifixEnvelope<EntityFacetCounts>;
|
|
162
164
|
/**
|
|
163
165
|
* A bus message on the wire. `meta.event` is always populated, and
|
|
164
166
|
* `readEventEnvelope` is what proves it before a consumer sees the payload.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/envelope/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AACjF,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,MAAM,mBAAmB,GAC3B,QAAQ,GACR,kBAAkB,GAClB,YAAY,GACZ,SAAS,GACT,OAAO,GACP,qBAAqB,GACrB,mBAAmB,GACnB,YAAY,GACZ,cAAc,GACd,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/envelope/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/envelope/types.ts"],"sourcesContent":["import type { EntityMetadataDocument } from '../entity-definition/metadata/index.js';\nimport type { SerializedEntity } from '../entity-definition/serializer/index.js';\nimport type { Entity, EntityId } from '../types/Entity.js';\nimport type { EntityLoadRequest } from '../types/EntityLoadRequest.js';\n\n/**\n * Discriminates what {@link EntifixEnvelope.data} carries.\n *\n * `command` extends the contract for the transactions layer: a write is issued\n * as a `command`, whose `data` shape lives in `@entifix/transactions` —\n * core only owns the discriminant so every artifact agrees on it.\n *\n * `event` is what actually rides the bus, and it is deliberately not named\n * after one publisher's flow: once a message can be `catalog.published` as\n * easily as `transaction.completed`, `transactionEvent` would be the wrong\n * name for it.\n *\n * ⚠️ **The four transaction-shaped discriminants below are four because they\n * carry four shapes.** `transactionEvent` used to mean all of them, and a\n * discriminant that means several things asserts nothing when it is read —\n * which is what `readTransactionEventEnvelope` was doing on the `202`: checking\n * a name that the record route and the event type both also answered to.\n *\n * - `transactionAccepted` — the `202` accept body, `{ transactionId, state }`.\n * Not a record: it has no `createdAt` and names no entity id yet.\n * - `transactionRecord` — the fold a client polls, a `TransactionRecord`.\n * - `sagaResult` — how a settled multi-step walk ended: which steps ran and\n * what each returned.\n * - `sagaInstance` — the durable instance itself, which is what a resumable\n * coordinator reads and what `GET /api/saga/:id` serves. It differs from\n * `sagaResult` in carrying the inputs and the resume count, because those are\n * what an operator needs to answer *why is this one still here*.\n *\n * `entityMetadata` is the same extension made once more, for the action model:\n * its `data` is an {@link EntityMetadataDocument}, which core does own because\n * both the service that computes it and the controls that render it are already\n * below the layer that would otherwise host it.\n */\nexport type EntifixEnvelopeType =\n | 'entity'\n | 'entityCollection'\n | 'entityPage'\n | 'command'\n | 'event'\n | 'transactionAccepted'\n | 'transactionRecord'\n | 'sagaResult'\n | 'sagaInstance'\n | 'entityMetadata';\n\nexport type EntifixEnvelopeMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';\n\n/**\n * A HATEOAS affordance: where the peer can go next from this message. Optional —\n * the envelope stands on its own without links, and only the artifact that owns\n * a route surface (today the REST service) is able to fill them in.\n */\nexport interface EntifixEnvelopeLink {\n rel: string;\n href: string;\n method?: EntifixEnvelopeMethod;\n}\n\n/**\n * The facts a message carries about *itself*, as opposed to about what\n * happened. Present only when the envelope is a bus message.\n *\n * The split rule: `meta` describes the message, `data` describes the\n * occurrence. So `correlationId` belongs here and an outcome's `code` does not.\n *\n * **`source` is never a consumer branch.** It is there for routing,\n * observability and audit. A handler that behaves differently depending on who\n * published re-couples the two services the bus decoupled — which is why .NET's\n * canonical `(sender, eventArgs)` types its sender as bare `object`, making the\n * dependency awkward on purpose. TypeScript cannot reproduce that friction, so\n * here it is a rule rather than a type.\n */\nexport interface EntifixEventMeta {\n /**\n * What happened, in the register's vocabulary: `catalog.published`,\n * `transaction.completed`. Declared in `tools/slices/*.slice.ts` as\n * `publishedEvents`/`subscriptions`, and used verbatim as the AMQP routing\n * key — which is what makes a subscriber's declared interest and its actual\n * queue binding the same string.\n */\n name: string;\n /**\n * Unique per message, and **the** deduplication key. Delivery is\n * at-least-once, so a consumer that must not fold twice keys on this.\n *\n * Not `correlationId`: one flow emits several messages, so correlating and\n * deduplicating are different questions. Keying dedup on the correlation id\n * makes a transaction's `completed` look like a duplicate of its `accepted`.\n */\n id: string;\n /** The emitting **slice** — the unit that owns a store — e.g. `catalog`. */\n source: string;\n /** ISO-8601 emission time. */\n at: string;\n /** Ties every message of one flow together — a transaction id, a saga id. */\n correlationId?: string;\n}\n\nexport interface EntifixEnvelopeMeta {\n type: EntifixEnvelopeType;\n /**\n * The target entity's `key` (falling back to its class name).\n *\n * Optional, because a bus message need not be about an entity at all —\n * `settlement.run.completed` is about a run. What a message *is* lives in\n * {@link EntifixEventMeta.name}; this stays the entity label the HTTP arm\n * routes on.\n */\n entity?: string;\n links?: EntifixEnvelopeLink[];\n /** Present only on a bus message. */\n event?: EntifixEventMeta;\n}\n\n/**\n * One message on the bus: its own metadata, plus the payload describing what\n * happened.\n *\n * Generic in the payload so a publisher keeps its own shape —\n * `DomainEvent<TransactionEvent>` today, `DomainEvent<PublishedOffering>` when\n * a projection's event lands — without the transport learning either.\n */\nexport interface DomainEvent<TData = unknown> extends EntifixEventMeta {\n data: TData;\n}\n\n/**\n * A change to an entity that happened on the server, delivered out-of-band —\n * the `data` of a bus/stream `event` envelope rather than a framing of its own\n *. `entity` is the `key ?? name` an adapter routes on, the same\n * string `entityQueryScope` uses, so a subscriber can invalidate exactly the\n * affected query keys.\n *\n * It lives in core rather than beside the React `ReactiveChannel` that consumes\n * it because both ends need it: the service maps a `TransactionEvent` onto it,\n * and the React packages sit above every tier that does the mapping.\n *\n * Deliberately **no** `transactionId`, timestamp or sequence: all three already\n * exist one level up as `meta.event.correlationId`, `.at` and `.id`, and\n * duplicating them onto the payload is how two sources of the same fact drift.\n */\nexport interface EntityChangeEvent {\n entity: string;\n change: 'created' | 'updated' | 'deleted';\n id: EntityId;\n}\n\n/**\n * The standard message exchanged between entifix artifacts.\n *\n * Every request and response body is an envelope: `meta` describes the payload,\n * `data` is the payload. It is deliberately transport-free — it lives in `core`\n * and knows nothing about HTTP — so the same contract can carry entities over\n * REST today and amqp or websockets later.\n */\nexport interface EntifixEnvelope<TData = unknown> {\n meta: EntifixEnvelopeMeta;\n data: TData;\n}\n\n/**\n * `data` shape when `meta.type` is `entityPage`. Generic in the entity so the\n * echoed `request` keeps its `keyof TEntity` filter/sort types — `EntityLoadRequest`\n * is invariant in its entity, so a non-generic `EntityLoadRequest<Entity>` here\n * would not accept a caller's `EntityLoadRequest<Product>`.\n */\nexport interface SerializedEntityPage<TEntity extends Entity = Entity> {\n items: SerializedEntity[];\n total: number;\n request?: EntityLoadRequest<TEntity>;\n}\n\nexport type EntityEnvelope = EntifixEnvelope<SerializedEntity>;\nexport type EntityCollectionEnvelope = EntifixEnvelope<SerializedEntity[]>;\nexport type EntityPageEnvelope<TEntity extends Entity = Entity> =\n EntifixEnvelope<SerializedEntityPage<TEntity>>;\nexport type EntityMetadataEnvelope = EntifixEnvelope<EntityMetadataDocument>;\n\n/**\n * A bus message on the wire. `meta.event` is always populated, and\n * `readEventEnvelope` is what proves it before a consumer sees the payload.\n */\nexport type EventEnvelope<TData = unknown> = EntifixEnvelope<TData> & {\n meta: EntifixEnvelopeMeta & { event: EntifixEventMeta };\n};\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/envelope/types.ts"],"sourcesContent":["import type { EntityMetadataDocument } from '../entity-definition/metadata/index.js';\nimport type { SerializedEntity } from '../entity-definition/serializer/index.js';\nimport type { Entity, EntityId } from '../types/Entity.js';\nimport type { EntityFacetCounts } from '../types/EntityFacets.js';\nimport type { EntityLoadRequest } from '../types/EntityLoadRequest.js';\n\n/**\n * Discriminates what {@link EntifixEnvelope.data} carries.\n *\n * `command` extends the contract for the transactions layer: a write is issued\n * as a `command`, whose `data` shape lives in `@entifix/transactions` —\n * core only owns the discriminant so every artifact agrees on it.\n *\n * `event` is what actually rides the bus, and it is deliberately not named\n * after one publisher's flow: once a message can be `catalog.published` as\n * easily as `transaction.completed`, `transactionEvent` would be the wrong\n * name for it.\n *\n * ⚠️ **The four transaction-shaped discriminants below are four because they\n * carry four shapes.** `transactionEvent` used to mean all of them, and a\n * discriminant that means several things asserts nothing when it is read —\n * which is what `readTransactionEventEnvelope` was doing on the `202`: checking\n * a name that the record route and the event type both also answered to.\n *\n * - `transactionAccepted` — the `202` accept body, `{ transactionId, state }`.\n * Not a record: it has no `createdAt` and names no entity id yet.\n * - `transactionRecord` — the fold a client polls, a `TransactionRecord`.\n * - `sagaResult` — how a settled multi-step walk ended: which steps ran and\n * what each returned.\n * - `sagaInstance` — the durable instance itself, which is what a resumable\n * coordinator reads and what `GET /api/saga/:id` serves. It differs from\n * `sagaResult` in carrying the inputs and the resume count, because those are\n * what an operator needs to answer *why is this one still here*.\n *\n * `entityMetadata` is the same extension made once more, for the action model:\n * its `data` is an {@link EntityMetadataDocument}, which core does own because\n * both the service that computes it and the controls that render it are already\n * below the layer that would otherwise host it.\n */\nexport type EntifixEnvelopeType =\n | 'entity'\n | 'entityCollection'\n | 'entityPage'\n | 'command'\n | 'event'\n | 'transactionAccepted'\n | 'transactionRecord'\n | 'sagaResult'\n | 'sagaInstance'\n | 'entityMetadata'\n | 'entityFacets';\n\nexport type EntifixEnvelopeMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';\n\n/**\n * A HATEOAS affordance: where the peer can go next from this message. Optional —\n * the envelope stands on its own without links, and only the artifact that owns\n * a route surface (today the REST service) is able to fill them in.\n */\nexport interface EntifixEnvelopeLink {\n rel: string;\n href: string;\n method?: EntifixEnvelopeMethod;\n}\n\n/**\n * The facts a message carries about *itself*, as opposed to about what\n * happened. Present only when the envelope is a bus message.\n *\n * The split rule: `meta` describes the message, `data` describes the\n * occurrence. So `correlationId` belongs here and an outcome's `code` does not.\n *\n * **`source` is never a consumer branch.** It is there for routing,\n * observability and audit. A handler that behaves differently depending on who\n * published re-couples the two services the bus decoupled — which is why .NET's\n * canonical `(sender, eventArgs)` types its sender as bare `object`, making the\n * dependency awkward on purpose. TypeScript cannot reproduce that friction, so\n * here it is a rule rather than a type.\n */\nexport interface EntifixEventMeta {\n /**\n * What happened, in the register's vocabulary: `catalog.published`,\n * `transaction.completed`. Declared in `tools/slices/*.slice.ts` as\n * `publishedEvents`/`subscriptions`, and used verbatim as the AMQP routing\n * key — which is what makes a subscriber's declared interest and its actual\n * queue binding the same string.\n */\n name: string;\n /**\n * Unique per message, and **the** deduplication key. Delivery is\n * at-least-once, so a consumer that must not fold twice keys on this.\n *\n * Not `correlationId`: one flow emits several messages, so correlating and\n * deduplicating are different questions. Keying dedup on the correlation id\n * makes a transaction's `completed` look like a duplicate of its `accepted`.\n */\n id: string;\n /** The emitting **slice** — the unit that owns a store — e.g. `catalog`. */\n source: string;\n /** ISO-8601 emission time. */\n at: string;\n /** Ties every message of one flow together — a transaction id, a saga id. */\n correlationId?: string;\n}\n\nexport interface EntifixEnvelopeMeta {\n type: EntifixEnvelopeType;\n /**\n * The target entity's `key` (falling back to its class name).\n *\n * Optional, because a bus message need not be about an entity at all —\n * `settlement.run.completed` is about a run. What a message *is* lives in\n * {@link EntifixEventMeta.name}; this stays the entity label the HTTP arm\n * routes on.\n */\n entity?: string;\n links?: EntifixEnvelopeLink[];\n /** Present only on a bus message. */\n event?: EntifixEventMeta;\n}\n\n/**\n * One message on the bus: its own metadata, plus the payload describing what\n * happened.\n *\n * Generic in the payload so a publisher keeps its own shape —\n * `DomainEvent<TransactionEvent>` today, `DomainEvent<PublishedOffering>` when\n * a projection's event lands — without the transport learning either.\n */\nexport interface DomainEvent<TData = unknown> extends EntifixEventMeta {\n data: TData;\n}\n\n/**\n * A change to an entity that happened on the server, delivered out-of-band —\n * the `data` of a bus/stream `event` envelope rather than a framing of its own\n *. `entity` is the `key ?? name` an adapter routes on, the same\n * string `entityQueryScope` uses, so a subscriber can invalidate exactly the\n * affected query keys.\n *\n * It lives in core rather than beside the React `ReactiveChannel` that consumes\n * it because both ends need it: the service maps a `TransactionEvent` onto it,\n * and the React packages sit above every tier that does the mapping.\n *\n * Deliberately **no** `transactionId`, timestamp or sequence: all three already\n * exist one level up as `meta.event.correlationId`, `.at` and `.id`, and\n * duplicating them onto the payload is how two sources of the same fact drift.\n */\nexport interface EntityChangeEvent {\n entity: string;\n change: 'created' | 'updated' | 'deleted';\n id: EntityId;\n}\n\n/**\n * The standard message exchanged between entifix artifacts.\n *\n * Every request and response body is an envelope: `meta` describes the payload,\n * `data` is the payload. It is deliberately transport-free — it lives in `core`\n * and knows nothing about HTTP — so the same contract can carry entities over\n * REST today and amqp or websockets later.\n */\nexport interface EntifixEnvelope<TData = unknown> {\n meta: EntifixEnvelopeMeta;\n data: TData;\n}\n\n/**\n * `data` shape when `meta.type` is `entityPage`. Generic in the entity so the\n * echoed `request` keeps its `keyof TEntity` filter/sort types — `EntityLoadRequest`\n * is invariant in its entity, so a non-generic `EntityLoadRequest<Entity>` here\n * would not accept a caller's `EntityLoadRequest<Product>`.\n */\nexport interface SerializedEntityPage<TEntity extends Entity = Entity> {\n items: SerializedEntity[];\n total: number;\n request?: EntityLoadRequest<TEntity>;\n}\n\nexport type EntityEnvelope = EntifixEnvelope<SerializedEntity>;\nexport type EntityCollectionEnvelope = EntifixEnvelope<SerializedEntity[]>;\nexport type EntityPageEnvelope<TEntity extends Entity = Entity> =\n EntifixEnvelope<SerializedEntityPage<TEntity>>;\nexport type EntityMetadataEnvelope = EntifixEnvelope<EntityMetadataDocument>;\nexport type EntityFacetsEnvelope = EntifixEnvelope<EntityFacetCounts>;\n\n/**\n * A bus message on the wire. `meta.event` is always populated, and\n * `readEventEnvelope` is what proves it before a consumer sees the payload.\n */\nexport type EventEnvelope<TData = unknown> = EntifixEnvelope<TData> & {\n meta: EntifixEnvelopeMeta & { event: EntifixEventMeta };\n};\n"],"names":[],"mappings":"AA0LA;;;CAGC,GACD,WAEE"}
|
package/dist/index.d.ts
CHANGED
|
@@ -28,12 +28,14 @@ export * from './i18n/format.js';
|
|
|
28
28
|
export * from './i18n/locales.js';
|
|
29
29
|
export * from './i18n/localized-text.js';
|
|
30
30
|
export * from './i18n/negotiate.js';
|
|
31
|
+
export * from './query/fold.js';
|
|
31
32
|
export * from './query/in-memory-query.js';
|
|
32
33
|
export * from './rsql/index.js';
|
|
33
34
|
export * from './types/BulkOutcome.js';
|
|
34
35
|
export * from './types/Entity.js';
|
|
35
36
|
export * from './types/EntityAction.js';
|
|
36
37
|
export * from './types/EntityDraft.js';
|
|
38
|
+
export * from './types/EntityFacets.js';
|
|
37
39
|
export * from './types/EntityFiltering.js';
|
|
38
40
|
export * from './types/EntityLoadRequest.js';
|
|
39
41
|
export * from './types/EntityPage.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,+CAA+C,CAAC;AAC9D,cAAc,wCAAwC,CAAC;AACvD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,kDAAkD,CAAC;AACjE,cAAc,gDAAgD,CAAC;AAC/D,cAAc,gDAAgD,CAAC;AAC/D,cAAc,kDAAkD,CAAC;AACjE,cAAc,uCAAuC,CAAC;AACtD,cAAc,mCAAmC,CAAC;AAClD,cAAc,sCAAsC,CAAC;AACrD,cAAc,uDAAuD,CAAC;AACtE,cAAc,2DAA2D,CAAC;AAC1E,cAAc,gDAAgD,CAAC;AAC/D,cAAc,uDAAuD,CAAC;AACtE,cAAc,iDAAiD,CAAC;AAChE,cAAc,0DAA0D,CAAC;AACzE,cAAc,wDAAwD,CAAC;AACvE,cAAc,wDAAwD,CAAC;AACvE,cAAc,0DAA0D,CAAC;AACzE,cAAc,uCAAuC,CAAC;AACtD,cAAc,0CAA0C,CAAC;AACzD,cAAc,yCAAyC,CAAC;AACxD,cAAc,uCAAuC,CAAC;AACtD,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iBAAiB,CAAC;AAChC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,0BAA0B,CAAC;AACzC,cAAc,iBAAiB,CAAC;AAChC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,+CAA+C,CAAC;AAC9D,cAAc,wCAAwC,CAAC;AACvD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,kDAAkD,CAAC;AACjE,cAAc,gDAAgD,CAAC;AAC/D,cAAc,gDAAgD,CAAC;AAC/D,cAAc,kDAAkD,CAAC;AACjE,cAAc,uCAAuC,CAAC;AACtD,cAAc,mCAAmC,CAAC;AAClD,cAAc,sCAAsC,CAAC;AACrD,cAAc,uDAAuD,CAAC;AACtE,cAAc,2DAA2D,CAAC;AAC1E,cAAc,gDAAgD,CAAC;AAC/D,cAAc,uDAAuD,CAAC;AACtE,cAAc,iDAAiD,CAAC;AAChE,cAAc,0DAA0D,CAAC;AACzE,cAAc,wDAAwD,CAAC;AACvE,cAAc,wDAAwD,CAAC;AACvE,cAAc,0DAA0D,CAAC;AACzE,cAAc,uCAAuC,CAAC;AACtD,cAAc,0CAA0C,CAAC;AACzD,cAAc,yCAAyC,CAAC;AACxD,cAAc,uCAAuC,CAAC;AACtD,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iBAAiB,CAAC;AAChC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,0BAA0B,CAAC;AACzC,cAAc,iBAAiB,CAAC;AAChC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -28,12 +28,14 @@ export * from './i18n/format.js';
|
|
|
28
28
|
export * from './i18n/locales.js';
|
|
29
29
|
export * from './i18n/localized-text.js';
|
|
30
30
|
export * from './i18n/negotiate.js';
|
|
31
|
+
export * from './query/fold.js';
|
|
31
32
|
export * from './query/in-memory-query.js';
|
|
32
33
|
export * from './rsql/index.js';
|
|
33
34
|
export * from './types/BulkOutcome.js';
|
|
34
35
|
export * from './types/Entity.js';
|
|
35
36
|
export * from './types/EntityAction.js';
|
|
36
37
|
export * from './types/EntityDraft.js';
|
|
38
|
+
export * from './types/EntityFacets.js';
|
|
37
39
|
export * from './types/EntityFiltering.js';
|
|
38
40
|
export * from './types/EntityLoadRequest.js';
|
|
39
41
|
export * from './types/EntityPage.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './base-entities/configuration-client/index.js';\nexport * from './base-entities/entifix-error/index.js';\nexport * from './command-palette/index.js';\nexport * from './entity-definition/decorators/accessor/index.js';\nexport * from './entity-definition/decorators/entity/index.js';\nexport * from './entity-definition/decorators/method/index.js';\nexport * from './entity-definition/decorators/use-case/index.js';\nexport * from './entity-definition/describe/index.js';\nexport * from './entity-definition/field-path.js';\nexport * from './entity-definition/helpers/index.js';\nexport * from './entity-definition/links/apply-entity-links/index.js';\nexport * from './entity-definition/links/entity-collection-link/index.js';\nexport * from './entity-definition/links/entity-link/index.js';\nexport * from './entity-definition/links/entity-link-source/index.js';\nexport * from './entity-definition/links/link-targets/index.js';\nexport * from './entity-definition/meta-entities/meta-accessor/index.js';\nexport * from './entity-definition/meta-entities/meta-entity/index.js';\nexport * from './entity-definition/meta-entities/meta-method/index.js';\nexport * from './entity-definition/meta-entities/meta-use-case/index.js';\nexport * from './entity-definition/metadata/index.js';\nexport * from './entity-definition/reconstruct/index.js';\nexport * from './entity-definition/serializer/index.js';\nexport * from './entity-definition/validate/index.js';\nexport * from './envelope/index.js';\nexport * from './hierarchy/index.js';\nexport * from './http/session-cookies.js';\nexport * from './i18n/format.js';\nexport * from './i18n/locales.js';\nexport * from './i18n/localized-text.js';\nexport * from './i18n/negotiate.js';\nexport * from './query/in-memory-query.js';\nexport * from './rsql/index.js';\nexport * from './types/BulkOutcome.js';\nexport * from './types/Entity.js';\nexport * from './types/EntityAction.js';\nexport * from './types/EntityDraft.js';\nexport * from './types/EntityFiltering.js';\nexport * from './types/EntityLoadRequest.js';\nexport * from './types/EntityPage.js';\nexport * from './types/EntityRowDraft.js';\nexport * from './types/EntitySelection.js';\nexport * from './types/EntitySorting.js';\nexport * from './types/Json.js';\nexport * from './types/StandardSchema.js';\nexport * from './types/UseCase.js';\nexport * from './wizard/index.js';\n"],"names":[],"mappings":"AAAA,cAAc,gDAAgD;AAC9D,cAAc,yCAAyC;AACvD,cAAc,6BAA6B;AAC3C,cAAc,mDAAmD;AACjE,cAAc,iDAAiD;AAC/D,cAAc,iDAAiD;AAC/D,cAAc,mDAAmD;AACjE,cAAc,wCAAwC;AACtD,cAAc,oCAAoC;AAClD,cAAc,uCAAuC;AACrD,cAAc,wDAAwD;AACtE,cAAc,4DAA4D;AAC1E,cAAc,iDAAiD;AAC/D,cAAc,wDAAwD;AACtE,cAAc,kDAAkD;AAChE,cAAc,2DAA2D;AACzE,cAAc,yDAAyD;AACvE,cAAc,yDAAyD;AACvE,cAAc,2DAA2D;AACzE,cAAc,wCAAwC;AACtD,cAAc,2CAA2C;AACzD,cAAc,0CAA0C;AACxD,cAAc,wCAAwC;AACtD,cAAc,sBAAsB;AACpC,cAAc,uBAAuB;AACrC,cAAc,4BAA4B;AAC1C,cAAc,mBAAmB;AACjC,cAAc,oBAAoB;AAClC,cAAc,2BAA2B;AACzC,cAAc,sBAAsB;AACpC,cAAc,6BAA6B;AAC3C,cAAc,kBAAkB;AAChC,cAAc,yBAAyB;AACvC,cAAc,oBAAoB;AAClC,cAAc,0BAA0B;AACxC,cAAc,yBAAyB;AACvC,cAAc,6BAA6B;AAC3C,cAAc,+BAA+B;AAC7C,cAAc,wBAAwB;AACtC,cAAc,4BAA4B;AAC1C,cAAc,6BAA6B;AAC3C,cAAc,2BAA2B;AACzC,cAAc,kBAAkB;AAChC,cAAc,4BAA4B;AAC1C,cAAc,qBAAqB;AACnC,cAAc,oBAAoB"}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './base-entities/configuration-client/index.js';\nexport * from './base-entities/entifix-error/index.js';\nexport * from './command-palette/index.js';\nexport * from './entity-definition/decorators/accessor/index.js';\nexport * from './entity-definition/decorators/entity/index.js';\nexport * from './entity-definition/decorators/method/index.js';\nexport * from './entity-definition/decorators/use-case/index.js';\nexport * from './entity-definition/describe/index.js';\nexport * from './entity-definition/field-path.js';\nexport * from './entity-definition/helpers/index.js';\nexport * from './entity-definition/links/apply-entity-links/index.js';\nexport * from './entity-definition/links/entity-collection-link/index.js';\nexport * from './entity-definition/links/entity-link/index.js';\nexport * from './entity-definition/links/entity-link-source/index.js';\nexport * from './entity-definition/links/link-targets/index.js';\nexport * from './entity-definition/meta-entities/meta-accessor/index.js';\nexport * from './entity-definition/meta-entities/meta-entity/index.js';\nexport * from './entity-definition/meta-entities/meta-method/index.js';\nexport * from './entity-definition/meta-entities/meta-use-case/index.js';\nexport * from './entity-definition/metadata/index.js';\nexport * from './entity-definition/reconstruct/index.js';\nexport * from './entity-definition/serializer/index.js';\nexport * from './entity-definition/validate/index.js';\nexport * from './envelope/index.js';\nexport * from './hierarchy/index.js';\nexport * from './http/session-cookies.js';\nexport * from './i18n/format.js';\nexport * from './i18n/locales.js';\nexport * from './i18n/localized-text.js';\nexport * from './i18n/negotiate.js';\nexport * from './query/fold.js';\nexport * from './query/in-memory-query.js';\nexport * from './rsql/index.js';\nexport * from './types/BulkOutcome.js';\nexport * from './types/Entity.js';\nexport * from './types/EntityAction.js';\nexport * from './types/EntityDraft.js';\nexport * from './types/EntityFacets.js';\nexport * from './types/EntityFiltering.js';\nexport * from './types/EntityLoadRequest.js';\nexport * from './types/EntityPage.js';\nexport * from './types/EntityRowDraft.js';\nexport * from './types/EntitySelection.js';\nexport * from './types/EntitySorting.js';\nexport * from './types/Json.js';\nexport * from './types/StandardSchema.js';\nexport * from './types/UseCase.js';\nexport * from './wizard/index.js';\n"],"names":[],"mappings":"AAAA,cAAc,gDAAgD;AAC9D,cAAc,yCAAyC;AACvD,cAAc,6BAA6B;AAC3C,cAAc,mDAAmD;AACjE,cAAc,iDAAiD;AAC/D,cAAc,iDAAiD;AAC/D,cAAc,mDAAmD;AACjE,cAAc,wCAAwC;AACtD,cAAc,oCAAoC;AAClD,cAAc,uCAAuC;AACrD,cAAc,wDAAwD;AACtE,cAAc,4DAA4D;AAC1E,cAAc,iDAAiD;AAC/D,cAAc,wDAAwD;AACtE,cAAc,kDAAkD;AAChE,cAAc,2DAA2D;AACzE,cAAc,yDAAyD;AACvE,cAAc,yDAAyD;AACvE,cAAc,2DAA2D;AACzE,cAAc,wCAAwC;AACtD,cAAc,2CAA2C;AACzD,cAAc,0CAA0C;AACxD,cAAc,wCAAwC;AACtD,cAAc,sBAAsB;AACpC,cAAc,uBAAuB;AACrC,cAAc,4BAA4B;AAC1C,cAAc,mBAAmB;AACjC,cAAc,oBAAoB;AAClC,cAAc,2BAA2B;AACzC,cAAc,sBAAsB;AACpC,cAAc,kBAAkB;AAChC,cAAc,6BAA6B;AAC3C,cAAc,kBAAkB;AAChC,cAAc,yBAAyB;AACvC,cAAc,oBAAoB;AAClC,cAAc,0BAA0B;AACxC,cAAc,yBAAyB;AACvC,cAAc,0BAA0B;AACxC,cAAc,6BAA6B;AAC3C,cAAc,+BAA+B;AAC7C,cAAc,wBAAwB;AACtC,cAAc,4BAA4B;AAC1C,cAAc,6BAA6B;AAC3C,cAAc,2BAA2B;AACzC,cAAc,kBAAkB;AAChC,cAAc,4BAA4B;AAC1C,cAAc,qBAAqB;AACnC,cAAc,oBAAoB"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `like` compares: text with its accents and its case taken off.
|
|
3
|
+
*
|
|
4
|
+
* One rule, stated once, because three engines answer `like` and each needs it
|
|
5
|
+
* in a different form — the in-memory matcher folds both sides, the Mongo
|
|
6
|
+
* adapter cannot fold a stored value and so widens the query instead, and the
|
|
7
|
+
* SQL adapter folds the column with `translate`. Every form is derived from
|
|
8
|
+
* {@link foldText} here, so none of them can drift into a different rule.
|
|
9
|
+
*
|
|
10
|
+
* The rule is Unicode's: decompose (NFD), drop the combining marks, lowercase.
|
|
11
|
+
* `é` is `e`, `ü` is `u`, and `ñ` is `n` — a buyer on a keyboard without the
|
|
12
|
+
* letter types `nino` for `niño`, and finding nothing is the worse answer. A
|
|
13
|
+
* letter that does not decompose (`ß`, `ø`, `æ`, `ł`) is left as it is, by
|
|
14
|
+
* every engine alike.
|
|
15
|
+
*/
|
|
16
|
+
/** `value` with its accents and case removed. */
|
|
17
|
+
export declare const foldText: (value: string) => string;
|
|
18
|
+
/**
|
|
19
|
+
* The precomposed letters each base letter stands for, both cases and the base
|
|
20
|
+
* itself included: `e` → `eEéÉèÈêÊëË…`.
|
|
21
|
+
*
|
|
22
|
+
* Built from Latin-1 Supplement and Latin Extended-A by {@link foldText}
|
|
23
|
+
* itself, rather than typed out, so a letter folds in the table exactly when
|
|
24
|
+
* it folds in the rule. Both cases are listed explicitly so a pattern built
|
|
25
|
+
* from it never depends on how a regex engine case-folds outside ASCII.
|
|
26
|
+
*/
|
|
27
|
+
export declare const FOLD_TABLE: Readonly<Record<string, string>>;
|
|
28
|
+
/** Escapes a string so it is matched literally inside a regular expression. */
|
|
29
|
+
export declare const escapeRegex: (value: string) => string;
|
|
30
|
+
/**
|
|
31
|
+
* A regular expression source that matches `value` as a substring the way
|
|
32
|
+
* `like` does, for an engine that cannot fold the text it searches: the value
|
|
33
|
+
* is folded, and each letter that has accented forms becomes a class of all of
|
|
34
|
+
* them, so `cafe` matches `Café` and `CAFÉ`. Everything else is escaped, so a
|
|
35
|
+
* `.` or a `(` in a search is matched literally.
|
|
36
|
+
*/
|
|
37
|
+
export declare const foldedPattern: (value: string) => string;
|
|
38
|
+
/** Whether `actual` contains `value`, both folded. */
|
|
39
|
+
export declare const containsFolded: (actual: string, value: string) => boolean;
|
|
40
|
+
//# sourceMappingURL=fold.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fold.d.ts","sourceRoot":"","sources":["../../src/query/fold.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,iDAAiD;AACjD,eAAO,MAAM,QAAQ,GAAI,OAAO,MAAM,KAAG,MACoB,CAAC;AAE9D;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUpD,CAAC;AAEL,+EAA+E;AAC/E,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,KAAG,MACE,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,MAMjC,CAAC;AAEd,sDAAsD;AACtD,eAAO,MAAM,cAAc,GAAI,QAAQ,MAAM,EAAE,OAAO,MAAM,KAAG,OACnB,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `like` compares: text with its accents and its case taken off.
|
|
3
|
+
*
|
|
4
|
+
* One rule, stated once, because three engines answer `like` and each needs it
|
|
5
|
+
* in a different form — the in-memory matcher folds both sides, the Mongo
|
|
6
|
+
* adapter cannot fold a stored value and so widens the query instead, and the
|
|
7
|
+
* SQL adapter folds the column with `translate`. Every form is derived from
|
|
8
|
+
* {@link foldText} here, so none of them can drift into a different rule.
|
|
9
|
+
*
|
|
10
|
+
* The rule is Unicode's: decompose (NFD), drop the combining marks, lowercase.
|
|
11
|
+
* `é` is `e`, `ü` is `u`, and `ñ` is `n` — a buyer on a keyboard without the
|
|
12
|
+
* letter types `nino` for `niño`, and finding nothing is the worse answer. A
|
|
13
|
+
* letter that does not decompose (`ß`, `ø`, `æ`, `ł`) is left as it is, by
|
|
14
|
+
* every engine alike.
|
|
15
|
+
*/ /** `value` with its accents and case removed. */ export const foldText = (value)=>value.normalize('NFD').replace(RegExp("[\\u0300-\\u036F\\u0483-\\u0489\\u0591-\\u05BD\\u05BF\\u05C1-\\u05C2\\u05C4-\\u05C5\\u05C7\\u0610-\\u061A\\u064B-\\u065F\\u0670\\u06D6-\\u06DC\\u06DF-\\u06E4\\u06E7-\\u06E8\\u06EA-\\u06ED\\u0711\\u0730-\\u074A\\u07A6-\\u07B0\\u07EB-\\u07F3\\u07FD\\u0816-\\u0819\\u081B-\\u0823\\u0825-\\u0827\\u0829-\\u082D\\u0859-\\u085B\\u0897-\\u089F\\u08CA-\\u08E1\\u08E3-\\u0903\\u093A-\\u093C\\u093E-\\u094F\\u0951-\\u0957\\u0962-\\u0963\\u0981-\\u0983\\u09BC\\u09BE-\\u09C4\\u09C7-\\u09C8\\u09CB-\\u09CD\\u09D7\\u09E2-\\u09E3\\u09FE\\u0A01-\\u0A03\\u0A3C\\u0A3E-\\u0A42\\u0A47-\\u0A48\\u0A4B-\\u0A4D\\u0A51\\u0A70-\\u0A71\\u0A75\\u0A81-\\u0A83\\u0ABC\\u0ABE-\\u0AC5\\u0AC7-\\u0AC9\\u0ACB-\\u0ACD\\u0AE2-\\u0AE3\\u0AFA-\\u0AFF\\u0B01-\\u0B03\\u0B3C\\u0B3E-\\u0B44\\u0B47-\\u0B48\\u0B4B-\\u0B4D\\u0B55-\\u0B57\\u0B62-\\u0B63\\u0B82\\u0BBE-\\u0BC2\\u0BC6-\\u0BC8\\u0BCA-\\u0BCD\\u0BD7\\u0C00-\\u0C04\\u0C3C\\u0C3E-\\u0C44\\u0C46-\\u0C48\\u0C4A-\\u0C4D\\u0C55-\\u0C56\\u0C62-\\u0C63\\u0C81-\\u0C83\\u0CBC\\u0CBE-\\u0CC4\\u0CC6-\\u0CC8\\u0CCA-\\u0CCD\\u0CD5-\\u0CD6\\u0CE2-\\u0CE3\\u0CF3\\u0D00-\\u0D03\\u0D3B-\\u0D3C\\u0D3E-\\u0D44\\u0D46-\\u0D48\\u0D4A-\\u0D4D\\u0D57\\u0D62-\\u0D63\\u0D81-\\u0D83\\u0DCA\\u0DCF-\\u0DD4\\u0DD6\\u0DD8-\\u0DDF\\u0DF2-\\u0DF3\\u0E31\\u0E34-\\u0E3A\\u0E47-\\u0E4E\\u0EB1\\u0EB4-\\u0EBC\\u0EC8-\\u0ECE\\u0F18-\\u0F19\\u0F35\\u0F37\\u0F39\\u0F3E-\\u0F3F\\u0F71-\\u0F84\\u0F86-\\u0F87\\u0F8D-\\u0F97\\u0F99-\\u0FBC\\u0FC6\\u102B-\\u103E\\u1056-\\u1059\\u105E-\\u1060\\u1062-\\u1064\\u1067-\\u106D\\u1071-\\u1074\\u1082-\\u108D\\u108F\\u109A-\\u109D\\u135D-\\u135F\\u1712-\\u1715\\u1732-\\u1734\\u1752-\\u1753\\u1772-\\u1773\\u17B4-\\u17D3\\u17DD\\u180B-\\u180D\\u180F\\u1885-\\u1886\\u18A9\\u1920-\\u192B\\u1930-\\u193B\\u1A17-\\u1A1B\\u1A55-\\u1A5E\\u1A60-\\u1A7C\\u1A7F\\u1AB0-\\u1ADD\\u1AE0-\\u1AEB\\u1B00-\\u1B04\\u1B34-\\u1B44\\u1B6B-\\u1B73\\u1B80-\\u1B82\\u1BA1-\\u1BAD\\u1BE6-\\u1BF3\\u1C24-\\u1C37\\u1CD0-\\u1CD2\\u1CD4-\\u1CE8\\u1CED\\u1CF4\\u1CF7-\\u1CF9\\u1DC0-\\u1DFF\\u20D0-\\u20F0\\u2CEF-\\u2CF1\\u2D7F\\u2DE0-\\u2DFF\\u302A-\\u302F\\u3099-\\u309A\\uA66F-\\uA672\\uA674-\\uA67D\\uA69E-\\uA69F\\uA6F0-\\uA6F1\\uA802\\uA806\\uA80B\\uA823-\\uA827\\uA82C\\uA880-\\uA881\\uA8B4-\\uA8C5\\uA8E0-\\uA8F1\\uA8FF\\uA926-\\uA92D\\uA947-\\uA953\\uA980-\\uA983\\uA9B3-\\uA9C0\\uA9E5\\uAA29-\\uAA36\\uAA43\\uAA4C-\\uAA4D\\uAA7B-\\uAA7D\\uAAB0\\uAAB2-\\uAAB4\\uAAB7-\\uAAB8\\uAABE-\\uAABF\\uAAC1\\uAAEB-\\uAAEF\\uAAF5-\\uAAF6\\uABE3-\\uABEA\\uABEC-\\uABED\\uFB1E\\uFE00-\\uFE0F\\uFE20-\\uFE2F\\u{101FD}\\u{102E0}\\u{10376}-\\u{1037A}\\u{10A01}-\\u{10A03}\\u{10A05}-\\u{10A06}\\u{10A0C}-\\u{10A0F}\\u{10A38}-\\u{10A3A}\\u{10A3F}\\u{10AE5}-\\u{10AE6}\\u{10D24}-\\u{10D27}\\u{10D69}-\\u{10D6D}\\u{10EAB}-\\u{10EAC}\\u{10EFA}-\\u{10EFF}\\u{10F46}-\\u{10F50}\\u{10F82}-\\u{10F85}\\u{11000}-\\u{11002}\\u{11038}-\\u{11046}\\u{11070}\\u{11073}-\\u{11074}\\u{1107F}-\\u{11082}\\u{110B0}-\\u{110BA}\\u{110C2}\\u{11100}-\\u{11102}\\u{11127}-\\u{11134}\\u{11145}-\\u{11146}\\u{11173}\\u{11180}-\\u{11182}\\u{111B3}-\\u{111C0}\\u{111C9}-\\u{111CC}\\u{111CE}-\\u{111CF}\\u{1122C}-\\u{11237}\\u{1123E}\\u{11241}\\u{112DF}-\\u{112EA}\\u{11300}-\\u{11303}\\u{1133B}-\\u{1133C}\\u{1133E}-\\u{11344}\\u{11347}-\\u{11348}\\u{1134B}-\\u{1134D}\\u{11357}\\u{11362}-\\u{11363}\\u{11366}-\\u{1136C}\\u{11370}-\\u{11374}\\u{113B8}-\\u{113C0}\\u{113C2}\\u{113C5}\\u{113C7}-\\u{113CA}\\u{113CC}-\\u{113D0}\\u{113D2}\\u{113E1}-\\u{113E2}\\u{11435}-\\u{11446}\\u{1145E}\\u{114B0}-\\u{114C3}\\u{115AF}-\\u{115B5}\\u{115B8}-\\u{115C0}\\u{115DC}-\\u{115DD}\\u{11630}-\\u{11640}\\u{116AB}-\\u{116B7}\\u{1171D}-\\u{1172B}\\u{1182C}-\\u{1183A}\\u{11930}-\\u{11935}\\u{11937}-\\u{11938}\\u{1193B}-\\u{1193E}\\u{11940}\\u{11942}-\\u{11943}\\u{119D1}-\\u{119D7}\\u{119DA}-\\u{119E0}\\u{119E4}\\u{11A01}-\\u{11A0A}\\u{11A33}-\\u{11A39}\\u{11A3B}-\\u{11A3E}\\u{11A47}\\u{11A51}-\\u{11A5B}\\u{11A8A}-\\u{11A99}\\u{11B60}-\\u{11B67}\\u{11C2F}-\\u{11C36}\\u{11C38}-\\u{11C3F}\\u{11C92}-\\u{11CA7}\\u{11CA9}-\\u{11CB6}\\u{11D31}-\\u{11D36}\\u{11D3A}\\u{11D3C}-\\u{11D3D}\\u{11D3F}-\\u{11D45}\\u{11D47}\\u{11D8A}-\\u{11D8E}\\u{11D90}-\\u{11D91}\\u{11D93}-\\u{11D97}\\u{11EF3}-\\u{11EF6}\\u{11F00}-\\u{11F01}\\u{11F03}\\u{11F34}-\\u{11F3A}\\u{11F3E}-\\u{11F42}\\u{11F5A}\\u{13440}\\u{13447}-\\u{13455}\\u{1611E}-\\u{1612F}\\u{16AF0}-\\u{16AF4}\\u{16B30}-\\u{16B36}\\u{16F4F}\\u{16F51}-\\u{16F87}\\u{16F8F}-\\u{16F92}\\u{16FE4}\\u{16FF0}-\\u{16FF1}\\u{1BC9D}-\\u{1BC9E}\\u{1CF00}-\\u{1CF2D}\\u{1CF30}-\\u{1CF46}\\u{1D165}-\\u{1D169}\\u{1D16D}-\\u{1D172}\\u{1D17B}-\\u{1D182}\\u{1D185}-\\u{1D18B}\\u{1D1AA}-\\u{1D1AD}\\u{1D242}-\\u{1D244}\\u{1DA00}-\\u{1DA36}\\u{1DA3B}-\\u{1DA6C}\\u{1DA75}\\u{1DA84}\\u{1DA9B}-\\u{1DA9F}\\u{1DAA1}-\\u{1DAAF}\\u{1E000}-\\u{1E006}\\u{1E008}-\\u{1E018}\\u{1E01B}-\\u{1E021}\\u{1E023}-\\u{1E024}\\u{1E026}-\\u{1E02A}\\u{1E08F}\\u{1E130}-\\u{1E136}\\u{1E2AE}\\u{1E2EC}-\\u{1E2EF}\\u{1E4EC}-\\u{1E4EF}\\u{1E5EE}-\\u{1E5EF}\\u{1E6E3}\\u{1E6E6}\\u{1E6EE}-\\u{1E6EF}\\u{1E6F5}\\u{1E8D0}-\\u{1E8D6}\\u{1E944}-\\u{1E94A}\\u{E0100}-\\u{E01EF}]", "gu"), '').toLowerCase();
|
|
16
|
+
/**
|
|
17
|
+
* The precomposed letters each base letter stands for, both cases and the base
|
|
18
|
+
* itself included: `e` → `eEéÉèÈêÊëË…`.
|
|
19
|
+
*
|
|
20
|
+
* Built from Latin-1 Supplement and Latin Extended-A by {@link foldText}
|
|
21
|
+
* itself, rather than typed out, so a letter folds in the table exactly when
|
|
22
|
+
* it folds in the rule. Both cases are listed explicitly so a pattern built
|
|
23
|
+
* from it never depends on how a regex engine case-folds outside ASCII.
|
|
24
|
+
*/ export const FOLD_TABLE = (()=>{
|
|
25
|
+
const table = {};
|
|
26
|
+
for(let code = 0xc0; code <= 0x17f; code += 1){
|
|
27
|
+
var _table, _base, _;
|
|
28
|
+
const letter = String.fromCharCode(code);
|
|
29
|
+
const base = foldText(letter);
|
|
30
|
+
if (base === letter || !/^[a-z]$/.test(base)) continue;
|
|
31
|
+
(_ = (_table = table)[_base = base]) != null ? _ : _table[_base] = `${base}${base.toUpperCase()}`;
|
|
32
|
+
table[base] += letter;
|
|
33
|
+
}
|
|
34
|
+
return table;
|
|
35
|
+
})();
|
|
36
|
+
/** Escapes a string so it is matched literally inside a regular expression. */ export const escapeRegex = (value)=>value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
37
|
+
/**
|
|
38
|
+
* A regular expression source that matches `value` as a substring the way
|
|
39
|
+
* `like` does, for an engine that cannot fold the text it searches: the value
|
|
40
|
+
* is folded, and each letter that has accented forms becomes a class of all of
|
|
41
|
+
* them, so `cafe` matches `Café` and `CAFÉ`. Everything else is escaped, so a
|
|
42
|
+
* `.` or a `(` in a search is matched literally.
|
|
43
|
+
*/ export const foldedPattern = (value)=>Array.from(foldText(value)).map((char)=>{
|
|
44
|
+
const forms = FOLD_TABLE[char];
|
|
45
|
+
return forms === undefined ? escapeRegex(char) : `[${forms}]`;
|
|
46
|
+
}).join('');
|
|
47
|
+
/** Whether `actual` contains `value`, both folded. */ export const containsFolded = (actual, value)=>foldText(actual).includes(foldText(value));
|
|
48
|
+
|
|
49
|
+
//# sourceMappingURL=fold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/query/fold.ts"],"sourcesContent":["/**\n * What `like` compares: text with its accents and its case taken off.\n *\n * One rule, stated once, because three engines answer `like` and each needs it\n * in a different form — the in-memory matcher folds both sides, the Mongo\n * adapter cannot fold a stored value and so widens the query instead, and the\n * SQL adapter folds the column with `translate`. Every form is derived from\n * {@link foldText} here, so none of them can drift into a different rule.\n *\n * The rule is Unicode's: decompose (NFD), drop the combining marks, lowercase.\n * `é` is `e`, `ü` is `u`, and `ñ` is `n` — a buyer on a keyboard without the\n * letter types `nino` for `niño`, and finding nothing is the worse answer. A\n * letter that does not decompose (`ß`, `ø`, `æ`, `ł`) is left as it is, by\n * every engine alike.\n */\n\n/** `value` with its accents and case removed. */\nexport const foldText = (value: string): string =>\n value.normalize('NFD').replace(/\\p{M}/gu, '').toLowerCase();\n\n/**\n * The precomposed letters each base letter stands for, both cases and the base\n * itself included: `e` → `eEéÉèÈêÊëË…`.\n *\n * Built from Latin-1 Supplement and Latin Extended-A by {@link foldText}\n * itself, rather than typed out, so a letter folds in the table exactly when\n * it folds in the rule. Both cases are listed explicitly so a pattern built\n * from it never depends on how a regex engine case-folds outside ASCII.\n */\nexport const FOLD_TABLE: Readonly<Record<string, string>> = (() => {\n const table: Record<string, string> = {};\n for (let code = 0xc0; code <= 0x17f; code += 1) {\n const letter = String.fromCharCode(code);\n const base = foldText(letter);\n if (base === letter || !/^[a-z]$/.test(base)) continue;\n table[base] ??= `${base}${base.toUpperCase()}`;\n table[base] += letter;\n }\n return table;\n})();\n\n/** Escapes a string so it is matched literally inside a regular expression. */\nexport const escapeRegex = (value: string): string =>\n value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n/**\n * A regular expression source that matches `value` as a substring the way\n * `like` does, for an engine that cannot fold the text it searches: the value\n * is folded, and each letter that has accented forms becomes a class of all of\n * them, so `cafe` matches `Café` and `CAFÉ`. Everything else is escaped, so a\n * `.` or a `(` in a search is matched literally.\n */\nexport const foldedPattern = (value: string): string =>\n Array.from(foldText(value))\n .map(char => {\n const forms = FOLD_TABLE[char];\n return forms === undefined ? escapeRegex(char) : `[${forms}]`;\n })\n .join('');\n\n/** Whether `actual` contains `value`, both folded. */\nexport const containsFolded = (actual: string, value: string): boolean =>\n foldText(actual).includes(foldText(value));\n"],"names":["foldText","value","normalize","replace","toLowerCase","FOLD_TABLE","table","code","base","letter","String","fromCharCode","test","toUpperCase","escapeRegex","foldedPattern","Array","from","map","char","forms","undefined","join","containsFolded","actual","includes"],"mappings":"AAAA;;;;;;;;;;;;;;CAcC,GAED,+CAA+C,GAC/C,OAAO,MAAMA,WAAW,CAACC,QACvBA,MAAMC,SAAS,CAAC,OAAOC,OAAO,CAAC,27JAAW,IAAIC,WAAW,GAAG;AAE9D;;;;;;;;CAQC,GACD,OAAO,MAAMC,aAA+C,AAAC,CAAA;IAC3D,MAAMC,QAAgC,CAAC;IACvC,IAAK,IAAIC,OAAO,MAAMA,QAAQ,OAAOA,QAAQ,EAAG;YAI9CD,QAAME,OAANF;QAHA,MAAMG,SAASC,OAAOC,YAAY,CAACJ;QACnC,MAAMC,OAAOR,SAASS;QACtB,IAAID,SAASC,UAAU,CAAC,UAAUG,IAAI,CAACJ,OAAO;SAC9CF,IAAAA,CAAAA,SAAAA,MAAK,CAACE,QAAAA,KAAK,YAAXF,IAAAA,MAAK,CAACE,MAAK,GAAK,GAAGA,OAAOA,KAAKK,WAAW,IAAI;QAC9CP,KAAK,CAACE,KAAK,IAAIC;IACjB;IACA,OAAOH;AACT,CAAA,IAAK;AAEL,6EAA6E,GAC7E,OAAO,MAAMQ,cAAc,CAACb,QAC1BA,MAAME,OAAO,CAAC,uBAAuB,QAAQ;AAE/C;;;;;;CAMC,GACD,OAAO,MAAMY,gBAAgB,CAACd,QAC5Be,MAAMC,IAAI,CAACjB,SAASC,QACjBiB,GAAG,CAACC,CAAAA;QACH,MAAMC,QAAQf,UAAU,CAACc,KAAK;QAC9B,OAAOC,UAAUC,YAAYP,YAAYK,QAAQ,CAAC,CAAC,EAAEC,MAAM,CAAC,CAAC;IAC/D,GACCE,IAAI,CAAC,IAAI;AAEd,oDAAoD,GACpD,OAAO,MAAMC,iBAAiB,CAACC,QAAgBvB,QAC7CD,SAASwB,QAAQC,QAAQ,CAACzB,SAASC,QAAQ"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Entity } from '../types/Entity.js';
|
|
2
|
+
import type { EntityFacetCounts, EntityFacetQuery } from '../types/EntityFacets.js';
|
|
2
3
|
import type { EntityFiltering } from '../types/EntityFiltering.js';
|
|
3
4
|
import type { EntitySorting } from '../types/EntitySorting.js';
|
|
4
5
|
/**
|
|
@@ -16,7 +17,8 @@ import type { EntitySorting } from '../types/EntitySorting.js';
|
|
|
16
17
|
* - `in` and `nin` against an array match when **any element** does, as `$in`;
|
|
17
18
|
* - a property may be a dotted path into a member holding an object, which is
|
|
18
19
|
* how a filter reaches one locale of a `localized` member (`name.en`);
|
|
19
|
-
* - `like` is a
|
|
20
|
+
* - `like` is a substring match that ignores accents and case — `cafe` finds
|
|
21
|
+
* `Café` — by the one rule in `fold.ts` every adapter derives its form from;
|
|
20
22
|
* - a missing value sorts below a present one, numbers and dates compare as
|
|
21
23
|
* themselves and anything else as text;
|
|
22
24
|
* - every top-level filtering entry is combined with `and`.
|
|
@@ -46,4 +48,13 @@ export declare function applyFiltering<TEntity extends Entity>(records: readonly
|
|
|
46
48
|
* accident. No key leaves the order the records were given in.
|
|
47
49
|
*/
|
|
48
50
|
export declare function applySorting<TEntity extends Entity>(records: readonly TEntity[], sorting: readonly EntitySorting<TEntity>[] | undefined): TEntity[];
|
|
51
|
+
/**
|
|
52
|
+
* How many records hold each value of each queried member, each member counted
|
|
53
|
+
* over the records **its own** filtering keeps — the in-memory form of a Mongo
|
|
54
|
+
* `$facet` of `$match`, `$unwind` and `$group`, with its rules:
|
|
55
|
+
* - an array member counts each element, as `$unwind` does;
|
|
56
|
+
* - a missing or `null` value, or an empty array, is not counted;
|
|
57
|
+
* - only a string, number or boolean is a value; anything else is skipped.
|
|
58
|
+
*/
|
|
59
|
+
export declare function countFacets<TEntity extends Entity>(records: readonly TEntity[], queries: readonly EntityFacetQuery<TEntity>[]): EntityFacetCounts;
|
|
49
60
|
//# sourceMappingURL=in-memory-query.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"in-memory-query.d.ts","sourceRoot":"","sources":["../../src/query/in-memory-query.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EAEV,eAAe,EAEhB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"in-memory-query.d.ts","sourceRoot":"","sources":["../../src/query/in-memory-query.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EACV,iBAAiB,EACjB,gBAAgB,EAEjB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAEV,eAAe,EAEhB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAG/D;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAInD;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CASjE;AAED,0EAA0E;AAC1E,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAYzE;AAsJD,wDAAwD;AACxD,wBAAgB,gBAAgB,CAAC,OAAO,SAAS,MAAM,EACrD,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,SAAS,eAAe,CAAC,OAAO,CAAC,EAAE,GAAG,SAAS,GACzD,OAAO,CAMT;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,OAAO,SAAS,MAAM,EACnD,OAAO,EAAE,SAAS,OAAO,EAAE,EAC3B,SAAS,EAAE,SAAS,eAAe,CAAC,OAAO,CAAC,EAAE,GAAG,SAAS,GACzD,OAAO,EAAE,CAEX;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,SAAS,MAAM,EACjD,OAAO,EAAE,SAAS,OAAO,EAAE,EAC3B,OAAO,EAAE,SAAS,aAAa,CAAC,OAAO,CAAC,EAAE,GAAG,SAAS,GACrD,OAAO,EAAE,CAqBX;AAWD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,MAAM,EAChD,OAAO,EAAE,SAAS,OAAO,EAAE,EAC3B,OAAO,EAAE,SAAS,gBAAgB,CAAC,OAAO,CAAC,EAAE,GAC5C,iBAAiB,CAgBnB"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EntifixBuildError } from '../base-entities/entifix-error/index.js';
|
|
2
2
|
import { EntityCollectionLink } from '../entity-definition/links/entity-collection-link/index.js';
|
|
3
3
|
import { EntityLink } from '../entity-definition/links/entity-link/index.js';
|
|
4
|
+
import { containsFolded } from './fold.js';
|
|
4
5
|
/**
|
|
5
6
|
* An {@link EntityLoadRequest}'s filtering and sorting, answered over records
|
|
6
7
|
* held in memory, with the semantics of the Mongo adapter.
|
|
@@ -16,7 +17,8 @@ import { EntityLink } from '../entity-definition/links/entity-link/index.js';
|
|
|
16
17
|
* - `in` and `nin` against an array match when **any element** does, as `$in`;
|
|
17
18
|
* - a property may be a dotted path into a member holding an object, which is
|
|
18
19
|
* how a filter reaches one locale of a `localized` member (`name.en`);
|
|
19
|
-
* - `like` is a
|
|
20
|
+
* - `like` is a substring match that ignores accents and case — `cafe` finds
|
|
21
|
+
* `Café` — by the one rule in `fold.ts` every adapter derives its form from;
|
|
20
22
|
* - a missing value sorts below a present one, numbers and dates compare as
|
|
21
23
|
* themselves and anything else as text;
|
|
22
24
|
* - every top-level filtering entry is combined with `and`.
|
|
@@ -56,8 +58,7 @@ import { EntityLink } from '../entity-definition/links/entity-link/index.js';
|
|
|
56
58
|
const matches = (element)=>values.some((value)=>isEqual(element, value));
|
|
57
59
|
return Array.isArray(actual) ? actual.some(matches) : matches(actual);
|
|
58
60
|
};
|
|
59
|
-
/**
|
|
60
|
-
const matchesLike = (actual, value)=>actual != null && new RegExp(escapeRegex(value), 'i').test(String(actual));
|
|
61
|
+
/** `like`: a substring match with accents and case folded off (`fold.ts`). */ const matchesLike = (actual, value)=>actual != null && containsFolded(String(actual), value);
|
|
61
62
|
const isFilterGroup = (node)=>!('property' in node) && (node.operator === 'and' || node.operator === 'or');
|
|
62
63
|
const isBetween = (actual, start, end)=>actual != null && compareStoredValues(actual, start) >= 0 && compareStoredValues(actual, end) <= 0;
|
|
63
64
|
/**
|
|
@@ -174,5 +175,34 @@ function matchesNode(record, node) {
|
|
|
174
175
|
return 0;
|
|
175
176
|
});
|
|
176
177
|
}
|
|
178
|
+
const isFacetValue = (value)=>typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
|
|
179
|
+
/** Count first, highest first; a tie by value, so the order is stable. */ const byCountThenValue = (left, right)=>right.count - left.count || compareStoredValues(left.value, right.value);
|
|
180
|
+
/**
|
|
181
|
+
* How many records hold each value of each queried member, each member counted
|
|
182
|
+
* over the records **its own** filtering keeps — the in-memory form of a Mongo
|
|
183
|
+
* `$facet` of `$match`, `$unwind` and `$group`, with its rules:
|
|
184
|
+
* - an array member counts each element, as `$unwind` does;
|
|
185
|
+
* - a missing or `null` value, or an empty array, is not counted;
|
|
186
|
+
* - only a string, number or boolean is a value; anything else is skipped.
|
|
187
|
+
*/ export function countFacets(records, queries) {
|
|
188
|
+
const counts = {};
|
|
189
|
+
for (const { member, filtering } of queries){
|
|
190
|
+
const tally = new Map();
|
|
191
|
+
for (const record of applyFiltering(records, filtering)){
|
|
192
|
+
const stored = valueAt(record, member);
|
|
193
|
+
for (const value of Array.isArray(stored) ? stored : [
|
|
194
|
+
stored
|
|
195
|
+
]){
|
|
196
|
+
var _tally_get;
|
|
197
|
+
if (isFacetValue(value)) tally.set(value, ((_tally_get = tally.get(value)) != null ? _tally_get : 0) + 1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
counts[member] = Array.from(tally, ([value, count])=>({
|
|
201
|
+
value,
|
|
202
|
+
count
|
|
203
|
+
})).sort(byCountThenValue);
|
|
204
|
+
}
|
|
205
|
+
return counts;
|
|
206
|
+
}
|
|
177
207
|
|
|
178
208
|
//# sourceMappingURL=in-memory-query.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/query/in-memory-query.ts"],"sourcesContent":["import { EntifixBuildError } from '../base-entities/entifix-error/index.js';\nimport { EntityCollectionLink } from '../entity-definition/links/entity-collection-link/index.js';\nimport { EntityLink } from '../entity-definition/links/entity-link/index.js';\nimport type { Entity } from '../types/Entity.js';\nimport type {\n EntityFilter,\n EntityFiltering,\n FilterGroup,\n} from '../types/EntityFiltering.js';\nimport type { EntitySorting } from '../types/EntitySorting.js';\n\n/**\n * An {@link EntityLoadRequest}'s filtering and sorting, answered over records\n * held in memory, with the semantics of the Mongo adapter.\n *\n * One implementation for everything that queries records it holds itself — the\n * in-memory test double, the static repository, a published view — so none of\n * them can drift into a more forgiving version of the others. They used to be\n * separate copies, and one of them compared an array member with `===` and so\n * never matched `in` (#34).\n *\n * The rules, each Mongo's:\n * - a link is its target's id, a collection link the list of their ids;\n * - `in` and `nin` against an array match when **any element** does, as `$in`;\n * - a property may be a dotted path into a member holding an object, which is\n * how a filter reaches one locale of a `localized` member (`name.en`);\n * - `like` is a case-insensitive substring match, as `$regex` with `i`;\n * - a missing value sorts below a present one, numbers and dates compare as\n * themselves and anything else as text;\n * - every top-level filtering entry is combined with `and`.\n */\n\n/**\n * A member as the Mongo adapter stores it, and so as its queries see it: a\n * link is its target's id, a collection link the list of their ids. Without\n * this no operator can compare a link, since `eq` against an id is never\n * `true` for an object.\n */\nexport function storedValue(value: unknown): unknown {\n if (value instanceof EntityLink) return value.id;\n if (value instanceof EntityCollectionLink) return value.ids;\n return value;\n}\n\n/**\n * The value a property names: a member, or a dotted path into one. A path\n * through something that is not an object ends in `undefined`, as a missing\n * field does in Mongo.\n */\nexport function valueAt(record: object, property: string): unknown {\n const [member, ...path] = property.split('.');\n return path.reduce<unknown>(\n (value, key) =>\n typeof value === 'object' && value !== null\n ? (value as Record<string, unknown>)[key]\n : undefined,\n storedValue((record as Record<string, unknown>)[member as string]),\n );\n}\n\n/** Ordering with Mongo's rules; `0` for two values it considers equal. */\nexport function compareStoredValues(left: unknown, right: unknown): number {\n if (left === right) return 0;\n // Mongo sorts missing values below present ones.\n if (left == null) return right == null ? 0 : -1;\n if (right == null) return 1;\n if (typeof left === 'number' && typeof right === 'number') {\n return left - right;\n }\n if (left instanceof Date && right instanceof Date) {\n return left.getTime() - right.getTime();\n }\n return String(left).localeCompare(String(right));\n}\n\n/** Equality by value, so two `Date`s naming one instant are equal. */\nconst isEqual = (actual: unknown, value: unknown): boolean =>\n actual instanceof Date && value instanceof Date\n ? actual.getTime() === value.getTime()\n : actual === value;\n\n/** `$in` semantics: a scalar matches a value, an array when any element does. */\nconst matchesAny = (actual: unknown, values: readonly unknown[]): boolean => {\n const matches = (element: unknown) =>\n values.some(value => isEqual(element, value));\n return Array.isArray(actual) ? actual.some(matches) : matches(actual);\n};\n\n/** Escapes a string so `like`/`nlike` match literally, as the Mongo adapter does. */\nconst escapeRegex = (value: string): string =>\n value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\nconst matchesLike = (actual: unknown, value: string): boolean =>\n actual != null && new RegExp(escapeRegex(value), 'i').test(String(actual));\n\nconst isFilterGroup = <TEntity extends Entity>(\n node: EntityFilter<TEntity> | FilterGroup<TEntity>,\n): node is FilterGroup<TEntity> =>\n !('property' in node) && (node.operator === 'and' || node.operator === 'or');\n\nconst isBetween = (actual: unknown, start: unknown, end: unknown) =>\n actual != null &&\n compareStoredValues(actual, start) >= 0 &&\n compareStoredValues(actual, end) <= 0;\n\n/**\n * The values a dotted path reaches **through an array** — every element's\n * member — or `undefined` when the path crosses no array, so the caller reads\n * the single value as before.\n */\nfunction valuesThroughArray(\n record: object,\n property: string,\n): unknown[] | undefined {\n const dot = property.indexOf('.');\n if (dot === -1) return undefined;\n const elements = storedValue(\n (record as Record<string, unknown>)[property.slice(0, dot)],\n );\n if (!Array.isArray(elements)) return undefined;\n const member = property.slice(dot + 1);\n return elements.map(element =>\n typeof element === 'object' && element !== null\n ? valueAt(element as object, member)\n : undefined,\n );\n}\n\n/**\n * Splits a filter into its positive test and whether the operator negates it,\n * so a path into an array can ask \"any element\" of the positive form and\n * negate the answer — never \"any element is not equal\", which almost always\n * holds.\n */\nfunction positiveTest<TEntity extends Entity>(\n filter: EntityFilter<TEntity>,\n): { readonly test: (actual: unknown) => boolean; readonly negated: boolean } {\n switch (filter.operator) {\n case 'eq':\n case 'ne':\n return {\n test: actual => isEqual(actual, filter.value),\n negated: filter.operator === 'ne',\n };\n case 'gt':\n return {\n test: actual =>\n actual != null && compareStoredValues(actual, filter.value) > 0,\n negated: false,\n };\n case 'gte':\n return {\n test: actual =>\n actual != null && compareStoredValues(actual, filter.value) >= 0,\n negated: false,\n };\n case 'lt':\n return {\n test: actual =>\n actual != null && compareStoredValues(actual, filter.value) < 0,\n negated: false,\n };\n case 'lte':\n return {\n test: actual =>\n actual != null && compareStoredValues(actual, filter.value) <= 0,\n negated: false,\n };\n case 'in':\n case 'nin':\n return {\n test: actual => matchesAny(actual, filter.values),\n negated: filter.operator === 'nin',\n };\n case 'between':\n case 'nbetween':\n return {\n test: actual => isBetween(actual, filter.start, filter.end),\n negated: filter.operator === 'nbetween',\n };\n case 'like':\n case 'nlike':\n return {\n test: actual => matchesLike(actual, filter.value),\n negated: filter.operator === 'nlike',\n };\n case 'isNull':\n case 'isNotNull':\n return {\n test: actual => actual != null,\n negated: filter.operator === 'isNull',\n };\n default: {\n // Exhaustiveness guard — a new operator must be handled explicitly.\n const never: never = filter;\n throw new EntifixBuildError(\n `Unknown filter operator \"${String((never as { operator: unknown }).operator)}\"`,\n );\n }\n }\n}\n\nfunction matchesFilter<TEntity extends Entity>(\n record: TEntity,\n filter: EntityFilter<TEntity>,\n): boolean {\n const { test, negated } = positiveTest(filter);\n const property = String(filter.property);\n const elements = valuesThroughArray(record, property);\n const holds =\n elements === undefined\n ? test(valueAt(record, property))\n : elements.some(test);\n return negated ? !holds : holds;\n}\n\nfunction matchesNode<TEntity extends Entity>(\n record: TEntity,\n node: EntityFilter<TEntity> | FilterGroup<TEntity>,\n): boolean {\n if (!isFilterGroup(node)) return matchesFilter(record, node);\n return node.operator === 'or'\n ? node.values.some(child => matchesNode(record, child))\n : node.values.every(child => matchesNode(record, child));\n}\n\n/** Whether one record matches a request's filtering. */\nexport function matchesFiltering<TEntity extends Entity>(\n record: TEntity,\n filtering: readonly EntityFiltering<TEntity>[] | undefined,\n): boolean {\n return (filtering ?? []).every(entry =>\n Array.isArray(entry)\n ? entry.every(child => matchesNode(record, child))\n : matchesNode(record, entry),\n );\n}\n\n/** The records a request's filtering keeps, in the order they were given. */\nexport function applyFiltering<TEntity extends Entity>(\n records: readonly TEntity[],\n filtering: readonly EntityFiltering<TEntity>[] | undefined,\n): TEntity[] {\n return records.filter(record => matchesFiltering(record, filtering));\n}\n\n/**\n * The records in the order a request asks for, as a new array. `EntitySorting`\n * is a record keyed by priority rather than a list, so two keys cannot tie by\n * accident. No key leaves the order the records were given in.\n */\nexport function applySorting<TEntity extends Entity>(\n records: readonly TEntity[],\n sorting: readonly EntitySorting<TEntity>[] | undefined,\n): TEntity[] {\n const keys = (sorting ?? []).flatMap(entry =>\n Object.keys(entry)\n .map(Number)\n .sort((left, right) => left - right)\n .map(priority => entry[priority])\n .filter(key => key != null),\n );\n if (keys.length === 0) return [...records];\n\n return [...records].sort((left, right) => {\n for (const key of keys) {\n const property = String(key.property);\n const compared = compareStoredValues(\n valueAt(left, property),\n valueAt(right, property),\n );\n if (compared !== 0) return key.type === 'desc' ? -compared : compared;\n }\n return 0;\n });\n}\n"],"names":["EntifixBuildError","EntityCollectionLink","EntityLink","storedValue","value","id","ids","valueAt","record","property","member","path","split","reduce","key","undefined","compareStoredValues","left","right","Date","getTime","String","localeCompare","isEqual","actual","matchesAny","values","matches","element","some","Array","isArray","escapeRegex","replace","matchesLike","RegExp","test","isFilterGroup","node","operator","isBetween","start","end","valuesThroughArray","dot","indexOf","elements","slice","map","positiveTest","filter","negated","never","matchesFilter","holds","matchesNode","child","every","matchesFiltering","filtering","entry","applyFiltering","records","applySorting","sorting","keys","flatMap","Object","Number","sort","priority","length","compared","type"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/query/in-memory-query.ts"],"sourcesContent":["import { EntifixBuildError } from '../base-entities/entifix-error/index.js';\nimport { EntityCollectionLink } from '../entity-definition/links/entity-collection-link/index.js';\nimport { EntityLink } from '../entity-definition/links/entity-link/index.js';\nimport type { Entity } from '../types/Entity.js';\nimport type {\n EntityFacetCounts,\n EntityFacetQuery,\n EntityFacetValue,\n} from '../types/EntityFacets.js';\nimport type {\n EntityFilter,\n EntityFiltering,\n FilterGroup,\n} from '../types/EntityFiltering.js';\nimport type { EntitySorting } from '../types/EntitySorting.js';\nimport { containsFolded } from './fold.js';\n\n/**\n * An {@link EntityLoadRequest}'s filtering and sorting, answered over records\n * held in memory, with the semantics of the Mongo adapter.\n *\n * One implementation for everything that queries records it holds itself — the\n * in-memory test double, the static repository, a published view — so none of\n * them can drift into a more forgiving version of the others. They used to be\n * separate copies, and one of them compared an array member with `===` and so\n * never matched `in` (#34).\n *\n * The rules, each Mongo's:\n * - a link is its target's id, a collection link the list of their ids;\n * - `in` and `nin` against an array match when **any element** does, as `$in`;\n * - a property may be a dotted path into a member holding an object, which is\n * how a filter reaches one locale of a `localized` member (`name.en`);\n * - `like` is a substring match that ignores accents and case — `cafe` finds\n * `Café` — by the one rule in `fold.ts` every adapter derives its form from;\n * - a missing value sorts below a present one, numbers and dates compare as\n * themselves and anything else as text;\n * - every top-level filtering entry is combined with `and`.\n */\n\n/**\n * A member as the Mongo adapter stores it, and so as its queries see it: a\n * link is its target's id, a collection link the list of their ids. Without\n * this no operator can compare a link, since `eq` against an id is never\n * `true` for an object.\n */\nexport function storedValue(value: unknown): unknown {\n if (value instanceof EntityLink) return value.id;\n if (value instanceof EntityCollectionLink) return value.ids;\n return value;\n}\n\n/**\n * The value a property names: a member, or a dotted path into one. A path\n * through something that is not an object ends in `undefined`, as a missing\n * field does in Mongo.\n */\nexport function valueAt(record: object, property: string): unknown {\n const [member, ...path] = property.split('.');\n return path.reduce<unknown>(\n (value, key) =>\n typeof value === 'object' && value !== null\n ? (value as Record<string, unknown>)[key]\n : undefined,\n storedValue((record as Record<string, unknown>)[member as string]),\n );\n}\n\n/** Ordering with Mongo's rules; `0` for two values it considers equal. */\nexport function compareStoredValues(left: unknown, right: unknown): number {\n if (left === right) return 0;\n // Mongo sorts missing values below present ones.\n if (left == null) return right == null ? 0 : -1;\n if (right == null) return 1;\n if (typeof left === 'number' && typeof right === 'number') {\n return left - right;\n }\n if (left instanceof Date && right instanceof Date) {\n return left.getTime() - right.getTime();\n }\n return String(left).localeCompare(String(right));\n}\n\n/** Equality by value, so two `Date`s naming one instant are equal. */\nconst isEqual = (actual: unknown, value: unknown): boolean =>\n actual instanceof Date && value instanceof Date\n ? actual.getTime() === value.getTime()\n : actual === value;\n\n/** `$in` semantics: a scalar matches a value, an array when any element does. */\nconst matchesAny = (actual: unknown, values: readonly unknown[]): boolean => {\n const matches = (element: unknown) =>\n values.some(value => isEqual(element, value));\n return Array.isArray(actual) ? actual.some(matches) : matches(actual);\n};\n\n/** `like`: a substring match with accents and case folded off (`fold.ts`). */\nconst matchesLike = (actual: unknown, value: string): boolean =>\n actual != null && containsFolded(String(actual), value);\n\nconst isFilterGroup = <TEntity extends Entity>(\n node: EntityFilter<TEntity> | FilterGroup<TEntity>,\n): node is FilterGroup<TEntity> =>\n !('property' in node) && (node.operator === 'and' || node.operator === 'or');\n\nconst isBetween = (actual: unknown, start: unknown, end: unknown) =>\n actual != null &&\n compareStoredValues(actual, start) >= 0 &&\n compareStoredValues(actual, end) <= 0;\n\n/**\n * The values a dotted path reaches **through an array** — every element's\n * member — or `undefined` when the path crosses no array, so the caller reads\n * the single value as before.\n */\nfunction valuesThroughArray(\n record: object,\n property: string,\n): unknown[] | undefined {\n const dot = property.indexOf('.');\n if (dot === -1) return undefined;\n const elements = storedValue(\n (record as Record<string, unknown>)[property.slice(0, dot)],\n );\n if (!Array.isArray(elements)) return undefined;\n const member = property.slice(dot + 1);\n return elements.map(element =>\n typeof element === 'object' && element !== null\n ? valueAt(element as object, member)\n : undefined,\n );\n}\n\n/**\n * Splits a filter into its positive test and whether the operator negates it,\n * so a path into an array can ask \"any element\" of the positive form and\n * negate the answer — never \"any element is not equal\", which almost always\n * holds.\n */\nfunction positiveTest<TEntity extends Entity>(\n filter: EntityFilter<TEntity>,\n): { readonly test: (actual: unknown) => boolean; readonly negated: boolean } {\n switch (filter.operator) {\n case 'eq':\n case 'ne':\n return {\n test: actual => isEqual(actual, filter.value),\n negated: filter.operator === 'ne',\n };\n case 'gt':\n return {\n test: actual =>\n actual != null && compareStoredValues(actual, filter.value) > 0,\n negated: false,\n };\n case 'gte':\n return {\n test: actual =>\n actual != null && compareStoredValues(actual, filter.value) >= 0,\n negated: false,\n };\n case 'lt':\n return {\n test: actual =>\n actual != null && compareStoredValues(actual, filter.value) < 0,\n negated: false,\n };\n case 'lte':\n return {\n test: actual =>\n actual != null && compareStoredValues(actual, filter.value) <= 0,\n negated: false,\n };\n case 'in':\n case 'nin':\n return {\n test: actual => matchesAny(actual, filter.values),\n negated: filter.operator === 'nin',\n };\n case 'between':\n case 'nbetween':\n return {\n test: actual => isBetween(actual, filter.start, filter.end),\n negated: filter.operator === 'nbetween',\n };\n case 'like':\n case 'nlike':\n return {\n test: actual => matchesLike(actual, filter.value),\n negated: filter.operator === 'nlike',\n };\n case 'isNull':\n case 'isNotNull':\n return {\n test: actual => actual != null,\n negated: filter.operator === 'isNull',\n };\n default: {\n // Exhaustiveness guard — a new operator must be handled explicitly.\n const never: never = filter;\n throw new EntifixBuildError(\n `Unknown filter operator \"${String((never as { operator: unknown }).operator)}\"`,\n );\n }\n }\n}\n\nfunction matchesFilter<TEntity extends Entity>(\n record: TEntity,\n filter: EntityFilter<TEntity>,\n): boolean {\n const { test, negated } = positiveTest(filter);\n const property = String(filter.property);\n const elements = valuesThroughArray(record, property);\n const holds =\n elements === undefined\n ? test(valueAt(record, property))\n : elements.some(test);\n return negated ? !holds : holds;\n}\n\nfunction matchesNode<TEntity extends Entity>(\n record: TEntity,\n node: EntityFilter<TEntity> | FilterGroup<TEntity>,\n): boolean {\n if (!isFilterGroup(node)) return matchesFilter(record, node);\n return node.operator === 'or'\n ? node.values.some(child => matchesNode(record, child))\n : node.values.every(child => matchesNode(record, child));\n}\n\n/** Whether one record matches a request's filtering. */\nexport function matchesFiltering<TEntity extends Entity>(\n record: TEntity,\n filtering: readonly EntityFiltering<TEntity>[] | undefined,\n): boolean {\n return (filtering ?? []).every(entry =>\n Array.isArray(entry)\n ? entry.every(child => matchesNode(record, child))\n : matchesNode(record, entry),\n );\n}\n\n/** The records a request's filtering keeps, in the order they were given. */\nexport function applyFiltering<TEntity extends Entity>(\n records: readonly TEntity[],\n filtering: readonly EntityFiltering<TEntity>[] | undefined,\n): TEntity[] {\n return records.filter(record => matchesFiltering(record, filtering));\n}\n\n/**\n * The records in the order a request asks for, as a new array. `EntitySorting`\n * is a record keyed by priority rather than a list, so two keys cannot tie by\n * accident. No key leaves the order the records were given in.\n */\nexport function applySorting<TEntity extends Entity>(\n records: readonly TEntity[],\n sorting: readonly EntitySorting<TEntity>[] | undefined,\n): TEntity[] {\n const keys = (sorting ?? []).flatMap(entry =>\n Object.keys(entry)\n .map(Number)\n .sort((left, right) => left - right)\n .map(priority => entry[priority])\n .filter(key => key != null),\n );\n if (keys.length === 0) return [...records];\n\n return [...records].sort((left, right) => {\n for (const key of keys) {\n const property = String(key.property);\n const compared = compareStoredValues(\n valueAt(left, property),\n valueAt(right, property),\n );\n if (compared !== 0) return key.type === 'desc' ? -compared : compared;\n }\n return 0;\n });\n}\n\nconst isFacetValue = (value: unknown): value is EntityFacetValue['value'] =>\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean';\n\n/** Count first, highest first; a tie by value, so the order is stable. */\nconst byCountThenValue = (left: EntityFacetValue, right: EntityFacetValue) =>\n right.count - left.count || compareStoredValues(left.value, right.value);\n\n/**\n * How many records hold each value of each queried member, each member counted\n * over the records **its own** filtering keeps — the in-memory form of a Mongo\n * `$facet` of `$match`, `$unwind` and `$group`, with its rules:\n * - an array member counts each element, as `$unwind` does;\n * - a missing or `null` value, or an empty array, is not counted;\n * - only a string, number or boolean is a value; anything else is skipped.\n */\nexport function countFacets<TEntity extends Entity>(\n records: readonly TEntity[],\n queries: readonly EntityFacetQuery<TEntity>[],\n): EntityFacetCounts {\n const counts: Record<string, readonly EntityFacetValue[]> = {};\n for (const { member, filtering } of queries) {\n const tally = new Map<EntityFacetValue['value'], number>();\n for (const record of applyFiltering(records, filtering)) {\n const stored = valueAt(record, member);\n for (const value of Array.isArray(stored) ? stored : [stored]) {\n if (isFacetValue(value)) tally.set(value, (tally.get(value) ?? 0) + 1);\n }\n }\n counts[member] = Array.from(tally, ([value, count]) => ({\n value,\n count,\n })).sort(byCountThenValue);\n }\n return counts;\n}\n"],"names":["EntifixBuildError","EntityCollectionLink","EntityLink","containsFolded","storedValue","value","id","ids","valueAt","record","property","member","path","split","reduce","key","undefined","compareStoredValues","left","right","Date","getTime","String","localeCompare","isEqual","actual","matchesAny","values","matches","element","some","Array","isArray","matchesLike","isFilterGroup","node","operator","isBetween","start","end","valuesThroughArray","dot","indexOf","elements","slice","map","positiveTest","filter","test","negated","never","matchesFilter","holds","matchesNode","child","every","matchesFiltering","filtering","entry","applyFiltering","records","applySorting","sorting","keys","flatMap","Object","Number","sort","priority","length","compared","type","isFacetValue","byCountThenValue","count","countFacets","queries","counts","tally","Map","stored","set","get","from"],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Entity, EntityConstructor } from '../types/Entity.js';
|
|
2
|
+
import type { EntityFacetQuery } from '../types/EntityFacets.js';
|
|
3
|
+
/** The parameter naming one member to count, repeated once per facet. */
|
|
4
|
+
export declare const FACET_PARAM = "facet";
|
|
5
|
+
/** The parameter holding one facet's own filtering: `rsql.<member>`. */
|
|
6
|
+
export declare const facetFilterParam: (member: string) => string;
|
|
7
|
+
/**
|
|
8
|
+
* Serializes facet queries into query parameters: each member as a `facet`,
|
|
9
|
+
* and each one's filtering as RSQL under `rsql.<member>` — omitted when it
|
|
10
|
+
* filters nothing, so an unfiltered count keeps a clean URL.
|
|
11
|
+
*/
|
|
12
|
+
export declare function serializeFacetQueryParams<TEntity extends Entity>(queries: readonly EntityFacetQuery<TEntity>[]): URLSearchParams;
|
|
13
|
+
/**
|
|
14
|
+
* Parses query parameters into facet queries, validated against the entity's
|
|
15
|
+
* own metadata — the server-side half of the facet protocol, and the same
|
|
16
|
+
* allowlist a load uses.
|
|
17
|
+
*
|
|
18
|
+
* A counted member must be `filterable`: a count is a question about the
|
|
19
|
+
* values a filter would choose between, and counting a member nobody may
|
|
20
|
+
* filter on would read out values the entity chose not to expose. A
|
|
21
|
+
* `localized` member is refused, since its value is a record of texts, not one
|
|
22
|
+
* to count. Every failure is an {@link EntifixBuildError}, so a `400`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseFacetQueryParams<TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, params: URLSearchParams): EntityFacetQuery<TEntity>[];
|
|
25
|
+
//# sourceMappingURL=facet-query-url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"facet-query-url.d.ts","sourceRoot":"","sources":["../../src/rsql/facet-query-url.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAUjE,yEAAyE;AACzE,eAAO,MAAM,WAAW,UAAU,CAAC;AAEnC,wEAAwE;AACxE,eAAO,MAAM,gBAAgB,GAAI,QAAQ,MAAM,KAAG,MACvB,CAAC;AAE5B;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,SAAS,MAAM,EAC9D,OAAO,EAAE,SAAS,gBAAgB,CAAC,OAAO,CAAC,EAAE,GAC5C,eAAe,CAQjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,SAAS,MAAM,EAC1D,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,MAAM,EAAE,eAAe,GACtB,gBAAgB,CAAC,OAAO,CAAC,EAAE,CA6C7B"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
import { EntifixBuildError } from '../base-entities/entifix-error/index.js';
|
|
3
|
+
import { allowedDescriptors, coerceFiltering, resolveAllowedProperty } from './coerce-rsql.js';
|
|
4
|
+
import { RSQL_PARAM } from './load-request-url.js';
|
|
5
|
+
import { parseRsql } from './parse-rsql.js';
|
|
6
|
+
import { serializeRsql } from './serialize-rsql.js';
|
|
7
|
+
/** The parameter naming one member to count, repeated once per facet. */ export const FACET_PARAM = 'facet';
|
|
8
|
+
/** The parameter holding one facet's own filtering: `rsql.<member>`. */ export const facetFilterParam = (member)=>`${RSQL_PARAM}.${member}`;
|
|
9
|
+
/**
|
|
10
|
+
* Serializes facet queries into query parameters: each member as a `facet`,
|
|
11
|
+
* and each one's filtering as RSQL under `rsql.<member>` — omitted when it
|
|
12
|
+
* filters nothing, so an unfiltered count keeps a clean URL.
|
|
13
|
+
*/ export function serializeFacetQueryParams(queries) {
|
|
14
|
+
const params = new URLSearchParams();
|
|
15
|
+
for (const { member, filtering } of queries){
|
|
16
|
+
params.append(FACET_PARAM, member);
|
|
17
|
+
const rsql = serializeRsql(filtering);
|
|
18
|
+
if (rsql !== '') params.set(facetFilterParam(member), rsql);
|
|
19
|
+
}
|
|
20
|
+
return params;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parses query parameters into facet queries, validated against the entity's
|
|
24
|
+
* own metadata — the server-side half of the facet protocol, and the same
|
|
25
|
+
* allowlist a load uses.
|
|
26
|
+
*
|
|
27
|
+
* A counted member must be `filterable`: a count is a question about the
|
|
28
|
+
* values a filter would choose between, and counting a member nobody may
|
|
29
|
+
* filter on would read out values the entity chose not to expose. A
|
|
30
|
+
* `localized` member is refused, since its value is a record of texts, not one
|
|
31
|
+
* to count. Every failure is an {@link EntifixBuildError}, so a `400`.
|
|
32
|
+
*/ export function parseFacetQueryParams(entityConstructor, params) {
|
|
33
|
+
const members = params.getAll(FACET_PARAM);
|
|
34
|
+
if (members.length === 0) {
|
|
35
|
+
throw new EntifixBuildError(`name at least one "${FACET_PARAM}"`);
|
|
36
|
+
}
|
|
37
|
+
if (new Set(members).size !== members.length) {
|
|
38
|
+
throw new EntifixBuildError(`a "${FACET_PARAM}" is named twice`, undefined, {
|
|
39
|
+
members
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const allowed = allowedDescriptors(entityConstructor, (descriptor)=>descriptor.filterable);
|
|
43
|
+
return members.map((member)=>{
|
|
44
|
+
const { key, locale } = resolveAllowedProperty(allowed, member, 'filterable');
|
|
45
|
+
if (locale !== undefined) {
|
|
46
|
+
// One locale of a localized member is prose, one value per record —
|
|
47
|
+
// there is nothing to choose between, so nothing to count.
|
|
48
|
+
throw new EntifixBuildError(`"${member}" cannot be counted`, undefined, {
|
|
49
|
+
member
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const expression = params.get(facetFilterParam(member));
|
|
53
|
+
return _extends({
|
|
54
|
+
member: key
|
|
55
|
+
}, (expression == null ? void 0 : expression.trim()) ? {
|
|
56
|
+
filtering: [
|
|
57
|
+
coerceFiltering(entityConstructor, parseRsql(expression))
|
|
58
|
+
]
|
|
59
|
+
} : {});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=facet-query-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/rsql/facet-query-url.ts"],"sourcesContent":["import { EntifixBuildError } from '../base-entities/entifix-error/index.js';\nimport type { Entity, EntityConstructor } from '../types/Entity.js';\nimport type { EntityFacetQuery } from '../types/EntityFacets.js';\nimport {\n allowedDescriptors,\n coerceFiltering,\n resolveAllowedProperty,\n} from './coerce-rsql.js';\nimport { RSQL_PARAM } from './load-request-url.js';\nimport { parseRsql } from './parse-rsql.js';\nimport { serializeRsql } from './serialize-rsql.js';\n\n/** The parameter naming one member to count, repeated once per facet. */\nexport const FACET_PARAM = 'facet';\n\n/** The parameter holding one facet's own filtering: `rsql.<member>`. */\nexport const facetFilterParam = (member: string): string =>\n `${RSQL_PARAM}.${member}`;\n\n/**\n * Serializes facet queries into query parameters: each member as a `facet`,\n * and each one's filtering as RSQL under `rsql.<member>` — omitted when it\n * filters nothing, so an unfiltered count keeps a clean URL.\n */\nexport function serializeFacetQueryParams<TEntity extends Entity>(\n queries: readonly EntityFacetQuery<TEntity>[],\n): URLSearchParams {\n const params = new URLSearchParams();\n for (const { member, filtering } of queries) {\n params.append(FACET_PARAM, member);\n const rsql = serializeRsql(filtering);\n if (rsql !== '') params.set(facetFilterParam(member), rsql);\n }\n return params;\n}\n\n/**\n * Parses query parameters into facet queries, validated against the entity's\n * own metadata — the server-side half of the facet protocol, and the same\n * allowlist a load uses.\n *\n * A counted member must be `filterable`: a count is a question about the\n * values a filter would choose between, and counting a member nobody may\n * filter on would read out values the entity chose not to expose. A\n * `localized` member is refused, since its value is a record of texts, not one\n * to count. Every failure is an {@link EntifixBuildError}, so a `400`.\n */\nexport function parseFacetQueryParams<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n params: URLSearchParams,\n): EntityFacetQuery<TEntity>[] {\n const members = params.getAll(FACET_PARAM);\n if (members.length === 0) {\n throw new EntifixBuildError(`name at least one \"${FACET_PARAM}\"`);\n }\n if (new Set(members).size !== members.length) {\n throw new EntifixBuildError(\n `a \"${FACET_PARAM}\" is named twice`,\n undefined,\n {\n members,\n },\n );\n }\n\n const allowed = allowedDescriptors(\n entityConstructor,\n descriptor => descriptor.filterable,\n );\n\n return members.map(member => {\n const { key, locale } = resolveAllowedProperty(\n allowed,\n member,\n 'filterable',\n );\n if (locale !== undefined) {\n // One locale of a localized member is prose, one value per record —\n // there is nothing to choose between, so nothing to count.\n throw new EntifixBuildError(`\"${member}\" cannot be counted`, undefined, {\n member,\n });\n }\n const expression = params.get(facetFilterParam(member));\n return {\n member: key as keyof TEntity & string,\n ...(expression?.trim()\n ? {\n filtering: [\n coerceFiltering(entityConstructor, parseRsql(expression)),\n ],\n }\n : {}),\n };\n });\n}\n"],"names":["EntifixBuildError","allowedDescriptors","coerceFiltering","resolveAllowedProperty","RSQL_PARAM","parseRsql","serializeRsql","FACET_PARAM","facetFilterParam","member","serializeFacetQueryParams","queries","params","URLSearchParams","filtering","append","rsql","set","parseFacetQueryParams","entityConstructor","members","getAll","length","Set","size","undefined","allowed","descriptor","filterable","map","key","locale","expression","get","trim"],"mappings":";AAAA,SAASA,iBAAiB,QAAQ,0CAA0C;AAG5E,SACEC,kBAAkB,EAClBC,eAAe,EACfC,sBAAsB,QACjB,mBAAmB;AAC1B,SAASC,UAAU,QAAQ,wBAAwB;AACnD,SAASC,SAAS,QAAQ,kBAAkB;AAC5C,SAASC,aAAa,QAAQ,sBAAsB;AAEpD,uEAAuE,GACvE,OAAO,MAAMC,cAAc,QAAQ;AAEnC,sEAAsE,GACtE,OAAO,MAAMC,mBAAmB,CAACC,SAC/B,GAAGL,WAAW,CAAC,EAAEK,QAAQ,CAAC;AAE5B;;;;CAIC,GACD,OAAO,SAASC,0BACdC,OAA6C;IAE7C,MAAMC,SAAS,IAAIC;IACnB,KAAK,MAAM,EAAEJ,MAAM,EAAEK,SAAS,EAAE,IAAIH,QAAS;QAC3CC,OAAOG,MAAM,CAACR,aAAaE;QAC3B,MAAMO,OAAOV,cAAcQ;QAC3B,IAAIE,SAAS,IAAIJ,OAAOK,GAAG,CAACT,iBAAiBC,SAASO;IACxD;IACA,OAAOJ;AACT;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASM,sBACdC,iBAA6C,EAC7CP,MAAuB;IAEvB,MAAMQ,UAAUR,OAAOS,MAAM,CAACd;IAC9B,IAAIa,QAAQE,MAAM,KAAK,GAAG;QACxB,MAAM,IAAItB,kBAAkB,CAAC,mBAAmB,EAAEO,YAAY,CAAC,CAAC;IAClE;IACA,IAAI,IAAIgB,IAAIH,SAASI,IAAI,KAAKJ,QAAQE,MAAM,EAAE;QAC5C,MAAM,IAAItB,kBACR,CAAC,GAAG,EAAEO,YAAY,gBAAgB,CAAC,EACnCkB,WACA;YACEL;QACF;IAEJ;IAEA,MAAMM,UAAUzB,mBACdkB,mBACAQ,CAAAA,aAAcA,WAAWC,UAAU;IAGrC,OAAOR,QAAQS,GAAG,CAACpB,CAAAA;QACjB,MAAM,EAAEqB,GAAG,EAAEC,MAAM,EAAE,GAAG5B,uBACtBuB,SACAjB,QACA;QAEF,IAAIsB,WAAWN,WAAW;YACxB,oEAAoE;YACpE,2DAA2D;YAC3D,MAAM,IAAIzB,kBAAkB,CAAC,CAAC,EAAES,OAAO,mBAAmB,CAAC,EAAEgB,WAAW;gBACtEhB;YACF;QACF;QACA,MAAMuB,aAAapB,OAAOqB,GAAG,CAACzB,iBAAiBC;QAC/C,OAAO;YACLA,QAAQqB;WACJE,CAAAA,8BAAAA,WAAYE,IAAI,MAChB;YACEpB,WAAW;gBACTZ,gBAAgBiB,mBAAmBd,UAAU2B;aAC9C;QACH,IACA,CAAC;IAET;AACF"}
|
package/dist/rsql/index.d.ts
CHANGED
package/dist/rsql/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rsql/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rsql/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC"}
|
package/dist/rsql/index.js
CHANGED
package/dist/rsql/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/rsql/index.ts"],"sourcesContent":["export * from './coerce-rsql.js';\nexport * from './load-request-url.js';\nexport * from './parse-rsql.js';\nexport * from './rsql-operators.js';\nexport * from './rsql-values.js';\nexport * from './serialize-rsql.js';\nexport * from './sort-param.js';\n"],"names":[],"mappings":"AAAA,cAAc,mBAAmB;AACjC,cAAc,wBAAwB;AACtC,cAAc,kBAAkB;AAChC,cAAc,sBAAsB;AACpC,cAAc,mBAAmB;AACjC,cAAc,sBAAsB;AACpC,cAAc,kBAAkB"}
|
|
1
|
+
{"version":3,"sources":["../../src/rsql/index.ts"],"sourcesContent":["export * from './coerce-rsql.js';\nexport * from './facet-query-url.js';\nexport * from './load-request-url.js';\nexport * from './parse-rsql.js';\nexport * from './rsql-operators.js';\nexport * from './rsql-values.js';\nexport * from './serialize-rsql.js';\nexport * from './sort-param.js';\n"],"names":[],"mappings":"AAAA,cAAc,mBAAmB;AACjC,cAAc,uBAAuB;AACrC,cAAc,wBAAwB;AACtC,cAAc,kBAAkB;AAChC,cAAc,sBAAsB;AACpC,cAAc,mBAAmB;AACjC,cAAc,sBAAsB;AACpC,cAAc,kBAAkB"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Entity } from './Entity.js';
|
|
2
|
+
import type { EntityFiltering } from './EntityFiltering.js';
|
|
3
|
+
/**
|
|
4
|
+
* One member to count the values of, under its own filtering.
|
|
5
|
+
*
|
|
6
|
+
* Each facet carries its own filtering rather than sharing one, because the
|
|
7
|
+
* counts a shop shows are usually **disjunctive**: a brand facet is counted
|
|
8
|
+
* under every filter except the brand choice itself, so choosing one brand
|
|
9
|
+
* does not drop every other brand to zero. Which filters a facet leaves out is
|
|
10
|
+
* the caller's to say — the counter counts what it is asked to.
|
|
11
|
+
*/
|
|
12
|
+
export interface EntityFacetQuery<TEntity extends Entity = Entity> {
|
|
13
|
+
readonly member: keyof TEntity & string;
|
|
14
|
+
readonly filtering?: EntityFiltering<TEntity>[];
|
|
15
|
+
}
|
|
16
|
+
/** One value a member holds, and how many records hold it. */
|
|
17
|
+
export interface EntityFacetValue {
|
|
18
|
+
readonly value: string | number | boolean;
|
|
19
|
+
readonly count: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The values each queried member holds, keyed by the member as it was asked
|
|
23
|
+
* for. Every queried member is present; one no record holds is an empty list.
|
|
24
|
+
* A value held by no record is absent rather than counted `0`, since the
|
|
25
|
+
* counter cannot know which values the caller offers.
|
|
26
|
+
*
|
|
27
|
+
* Ordered by count, highest first, then by value.
|
|
28
|
+
*/
|
|
29
|
+
export type EntityFacetCounts = Readonly<Record<string, readonly EntityFacetValue[]>>;
|
|
30
|
+
//# sourceMappingURL=EntityFacets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EntityFacets.d.ts","sourceRoot":"","sources":["../../src/types/EntityFacets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE5D;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM;IAC/D,QAAQ,CAAC,MAAM,EAAE,MAAM,OAAO,GAAG,MAAM,CAAC;IACxC,QAAQ,CAAC,SAAS,CAAC,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC;CACjD;AAED,8DAA8D;AAC9D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CACtC,MAAM,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC,CAC5C,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The values each queried member holds, keyed by the member as it was asked
|
|
3
|
+
* for. Every queried member is present; one no record holds is an empty list.
|
|
4
|
+
* A value held by no record is absent rather than counted `0`, since the
|
|
5
|
+
* counter cannot know which values the caller offers.
|
|
6
|
+
*
|
|
7
|
+
* Ordered by count, highest first, then by value.
|
|
8
|
+
*/ export { };
|
|
9
|
+
|
|
10
|
+
//# sourceMappingURL=EntityFacets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/types/EntityFacets.ts"],"sourcesContent":["import type { Entity } from './Entity.js';\nimport type { EntityFiltering } from './EntityFiltering.js';\n\n/**\n * One member to count the values of, under its own filtering.\n *\n * Each facet carries its own filtering rather than sharing one, because the\n * counts a shop shows are usually **disjunctive**: a brand facet is counted\n * under every filter except the brand choice itself, so choosing one brand\n * does not drop every other brand to zero. Which filters a facet leaves out is\n * the caller's to say — the counter counts what it is asked to.\n */\nexport interface EntityFacetQuery<TEntity extends Entity = Entity> {\n readonly member: keyof TEntity & string;\n readonly filtering?: EntityFiltering<TEntity>[];\n}\n\n/** One value a member holds, and how many records hold it. */\nexport interface EntityFacetValue {\n readonly value: string | number | boolean;\n readonly count: number;\n}\n\n/**\n * The values each queried member holds, keyed by the member as it was asked\n * for. Every queried member is present; one no record holds is an empty list.\n * A value held by no record is absent rather than counted `0`, since the\n * counter cannot know which values the caller offers.\n *\n * Ordered by count, highest first, then by value.\n */\nexport type EntityFacetCounts = Readonly<\n Record<string, readonly EntityFacetValue[]>\n>;\n"],"names":[],"mappings":"AAuBA;;;;;;;CAOC,GACD,WAEE"}
|
package/package.json
CHANGED