@entifix/mongo 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/adapters/filter-translator.d.ts +1 -1
- package/dist/adapters/filter-translator.d.ts.map +1 -1
- package/dist/adapters/filter-translator.js +6 -5
- package/dist/adapters/filter-translator.js.map +1 -1
- package/dist/adapters/make-mongo-facet-counter.d.ts +18 -0
- package/dist/adapters/make-mongo-facet-counter.d.ts.map +1 -0
- package/dist/adapters/make-mongo-facet-counter.js +76 -0
- package/dist/adapters/make-mongo-facet-counter.js.map +1 -0
- package/dist/adapters/make-mongo-link-resolver.d.ts.map +1 -1
- package/dist/adapters/make-mongo-link-resolver.js +2 -4
- package/dist/adapters/make-mongo-link-resolver.js.map +1 -1
- package/dist/adapters/make-mongo-repository.d.ts +5 -3
- package/dist/adapters/make-mongo-repository.d.ts.map +1 -1
- package/dist/adapters/make-mongo-repository.js +13 -20
- package/dist/adapters/make-mongo-repository.js.map +1 -1
- package/dist/adapters/mongo-facet-counter-factory.d.ts +18 -0
- package/dist/adapters/mongo-facet-counter-factory.d.ts.map +1 -0
- package/dist/adapters/mongo-facet-counter-factory.js +29 -0
- package/dist/adapters/mongo-facet-counter-factory.js.map +1 -0
- package/dist/adapters/mongo-repository-factory.d.ts.map +1 -1
- package/dist/adapters/mongo-repository-factory.js +2 -4
- package/dist/adapters/mongo-repository-factory.js.map +1 -1
- package/dist/errors/translate-mongo-error.d.ts +30 -0
- package/dist/errors/translate-mongo-error.d.ts.map +1 -0
- package/dist/errors/translate-mongo-error.js +74 -0
- package/dist/errors/translate-mongo-error.js.map +1 -0
- package/dist/inbox/store.d.ts +1 -2
- package/dist/inbox/store.d.ts.map +1 -1
- package/dist/inbox/store.js +3 -8
- package/dist/inbox/store.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/mongo-database/mongo-database.d.ts +3 -3
- package/dist/mongo-database/mongo-database.d.ts.map +1 -1
- package/dist/mongo-database/mongo-database.js +2 -2
- package/dist/mongo-database/mongo-database.js.map +1 -1
- package/dist/mongo-database/mongo-tenant-directory.d.ts.map +1 -1
- package/dist/mongo-database/mongo-tenant-directory.js +2 -4
- package/dist/mongo-database/mongo-tenant-directory.js.map +1 -1
- package/dist/mongo-database/mongo-tenant-resolver.d.ts.map +1 -1
- package/dist/mongo-database/mongo-tenant-resolver.js +18 -7
- package/dist/mongo-database/mongo-tenant-resolver.js.map +1 -1
- package/dist/outbox/relay.d.ts +50 -9
- package/dist/outbox/relay.d.ts.map +1 -1
- package/dist/outbox/relay.js +67 -26
- package/dist/outbox/relay.js.map +1 -1
- package/dist/outbox/store.d.ts +3 -3
- package/dist/outbox/store.d.ts.map +1 -1
- package/dist/outbox/store.js +8 -22
- package/dist/outbox/store.js.map +1 -1
- package/dist/saga/store.d.ts +3 -4
- package/dist/saga/store.d.ts.map +1 -1
- package/dist/saga/store.js +10 -15
- package/dist/saga/store.js.map +1 -1
- package/dist/unit-of-work/command-inbox.d.ts +2 -3
- package/dist/unit-of-work/command-inbox.d.ts.map +1 -1
- package/dist/unit-of-work/command-inbox.js +3 -7
- package/dist/unit-of-work/command-inbox.js.map +1 -1
- package/dist/unit-of-work/mongo-unit-of-work.d.ts.map +1 -1
- package/dist/unit-of-work/mongo-unit-of-work.js +30 -26
- package/dist/unit-of-work/mongo-unit-of-work.js.map +1 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -12,10 +12,10 @@ pnpm add @entifix/mongo effect
|
|
|
12
12
|
|
|
13
13
|
## Entry points
|
|
14
14
|
|
|
15
|
-
| Import | What it holds
|
|
16
|
-
| ----------------------------- |
|
|
17
|
-
| `@entifix/mongo` | Repository (optionally inside a session), repository factories (`MongoRepositoryFactoryLayer`, `MongoTenantRepositoryFactoryLayer`), link resolver, query translation, client and database Layers, tenant resolver and tenant directory, health probe
|
|
18
|
-
| `@entifix/mongo/transactions` | The unit of work (`makeMongoUnitOfWork`, `MongoUnitOfWorkLayer`) and its command inbox, the transactional outbox and inbox, the relays that drain the outbox (one database, or every organization's) and the inline `drainOutboxNow`, and the saga store (`makeMongoSagaStore`, `MongoSagaStoreLayer`, `mongoSagaStoreLayer({ database })`)
|
|
15
|
+
| Import | What it holds |
|
|
16
|
+
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
17
|
+
| `@entifix/mongo` | Repository (optionally inside a session), repository factories (`MongoRepositoryFactoryLayer`, `MongoTenantRepositoryFactoryLayer`), facet counter (`makeMongoFacetCounter`, one `$facet` aggregation) and its factories (`MongoFacetCounterFactoryLayer`, `MongoTenantFacetCounterFactoryLayer`), link resolver, query translation, client and database Layers, tenant resolver and tenant directory, health probe |
|
|
18
|
+
| `@entifix/mongo/transactions` | The unit of work (`makeMongoUnitOfWork`, `MongoUnitOfWorkLayer`) and its command inbox, the transactional outbox and inbox, the relays that drain the outbox (one database, or every organization's) and the inline `drainOutboxNow`, and the saga store (`makeMongoSagaStore`, `MongoSagaStoreLayer`, `mongoSagaStoreLayer({ database })`) |
|
|
19
19
|
|
|
20
20
|
## Optional peers
|
|
21
21
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Entity, type EntityFiltering, type EntitySorting } from '@entifix/core';
|
|
2
2
|
/** A Mongo query document (`{ field: condition, $and: [...], ... }`). */
|
|
3
3
|
export type MongoFilter = Record<string, unknown>;
|
|
4
4
|
/** A Mongo sort document (`{ field: 1 | -1 }`). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filter-translator.d.ts","sourceRoot":"","sources":["../../src/adapters/filter-translator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"filter-translator.d.ts","sourceRoot":"","sources":["../../src/adapters/filter-translator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,MAAM,EAEX,KAAK,eAAe,EACpB,KAAK,aAAa,EAGnB,MAAM,eAAe,CAAC;AAEvB,yEAAyE;AACzE,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAClD,mDAAmD;AACnD,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAsE/C;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,SAAS,MAAM,EACvD,SAAS,CAAC,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,GACrC,WAAW,CAiBb;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,SAAS,MAAM,EACrD,OAAO,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,EAAE,GACjC,SAAS,CAgBX"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { foldedPattern } from '@entifix/core';
|
|
1
2
|
const BINARY_OPERATORS = {
|
|
2
3
|
eq: '$eq',
|
|
3
4
|
ne: '$ne',
|
|
@@ -6,9 +7,6 @@ const BINARY_OPERATORS = {
|
|
|
6
7
|
lt: '$lt',
|
|
7
8
|
lte: '$lte'
|
|
8
9
|
};
|
|
9
|
-
/** Escapes a string so it is matched literally inside a Mongo `$regex`. */ function escapeRegex(value) {
|
|
10
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
11
|
-
}
|
|
12
10
|
function isFilterGroup(node) {
|
|
13
11
|
// A group carries `values` + a logic operator and, unlike the array filter,
|
|
14
12
|
// has no `property`.
|
|
@@ -63,9 +61,12 @@ function translateNode(node) {
|
|
|
63
61
|
}
|
|
64
62
|
};
|
|
65
63
|
case 'like':
|
|
64
|
+
// A stored value cannot be folded in a `find`, and a collation does not
|
|
65
|
+
// reach `$regex` — so the query is widened instead: each letter becomes
|
|
66
|
+
// the class of its accented forms, both cases listed (`fold.ts`).
|
|
66
67
|
return {
|
|
67
68
|
[field]: {
|
|
68
|
-
$regex:
|
|
69
|
+
$regex: foldedPattern(node.value),
|
|
69
70
|
$options: 'i'
|
|
70
71
|
}
|
|
71
72
|
};
|
|
@@ -73,7 +74,7 @@ function translateNode(node) {
|
|
|
73
74
|
return {
|
|
74
75
|
[field]: {
|
|
75
76
|
$not: {
|
|
76
|
-
$regex:
|
|
77
|
+
$regex: foldedPattern(node.value),
|
|
77
78
|
$options: 'i'
|
|
78
79
|
}
|
|
79
80
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adapters/filter-translator.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/filter-translator.ts"],"sourcesContent":["import {\n type Entity,\n type EntityFilter,\n type EntityFiltering,\n type EntitySorting,\n type FilterGroup,\n foldedPattern,\n} from '@entifix/core';\n\n/** A Mongo query document (`{ field: condition, $and: [...], ... }`). */\nexport type MongoFilter = Record<string, unknown>;\n/** A Mongo sort document (`{ field: 1 | -1 }`). */\nexport type MongoSort = Record<string, 1 | -1>;\n\nconst BINARY_OPERATORS: Record<string, string> = {\n eq: '$eq',\n ne: '$ne',\n gt: '$gt',\n gte: '$gte',\n lt: '$lt',\n lte: '$lte',\n};\n\nfunction isFilterGroup<TEntity extends Entity>(\n node: EntityFilter<TEntity> | FilterGroup<TEntity>,\n): node is FilterGroup<TEntity> {\n // A group carries `values` + a logic operator and, unlike the array filter,\n // has no `property`.\n return (\n !('property' in node) && (node.operator === 'and' || node.operator === 'or')\n );\n}\n\nfunction translateNode<TEntity extends Entity>(\n node: EntityFilter<TEntity> | FilterGroup<TEntity>,\n): MongoFilter {\n if (isFilterGroup(node)) {\n const clause = node.operator === 'or' ? '$or' : '$and';\n return { [clause]: node.values.map(translateNode) };\n }\n\n const field = String(node.property);\n\n switch (node.operator) {\n case 'eq':\n case 'ne':\n case 'gt':\n case 'gte':\n case 'lt':\n case 'lte':\n return { [field]: { [BINARY_OPERATORS[node.operator]]: node.value } };\n case 'in':\n return { [field]: { $in: node.values } };\n case 'nin':\n return { [field]: { $nin: node.values } };\n case 'between':\n return { [field]: { $gte: node.start, $lte: node.end } };\n case 'nbetween':\n return { [field]: { $not: { $gte: node.start, $lte: node.end } } };\n case 'like':\n // A stored value cannot be folded in a `find`, and a collation does not\n // reach `$regex` — so the query is widened instead: each letter becomes\n // the class of its accented forms, both cases listed (`fold.ts`).\n return { [field]: { $regex: foldedPattern(node.value), $options: 'i' } };\n case 'nlike':\n return {\n [field]: {\n $not: { $regex: foldedPattern(node.value), $options: 'i' },\n },\n };\n case 'isNull':\n return { [field]: { $eq: null } };\n case 'isNotNull':\n return { [field]: { $ne: null } };\n default: {\n // Exhaustiveness guard — a new operator must be handled explicitly.\n const _never: never = node;\n return _never;\n }\n }\n}\n\n/**\n * Translates the request's `filtering` (an array of {@link EntityFiltering},\n * each a single filter, an array of filters, or a {@link FilterGroup}) into a\n * single Mongo query document. Every top-level entry is combined with `$and`;\n * an empty/absent filtering yields the match-all `{}`.\n */\nexport function translateFiltering<TEntity extends Entity>(\n filtering?: EntityFiltering<TEntity>[],\n): MongoFilter {\n if (!filtering || filtering.length === 0) {\n return {};\n }\n\n const clauses: MongoFilter[] = [];\n for (const entry of filtering) {\n if (Array.isArray(entry)) {\n clauses.push(...entry.map(translateNode));\n } else {\n clauses.push(translateNode(entry));\n }\n }\n\n if (clauses.length === 0) return {};\n if (clauses.length === 1) return clauses[0];\n return { $and: clauses };\n}\n\n/**\n * Translates the request's `sorting` (an array of {@link EntitySorting} records,\n * each keyed by numeric priority) into a Mongo sort document, preserving the\n * numeric ordering within each record.\n */\nexport function translateSorting<TEntity extends Entity>(\n sorting?: EntitySorting<TEntity>[],\n): MongoSort {\n const sort: MongoSort = {};\n if (!sorting) return sort;\n\n for (const record of sorting) {\n for (const priority of Object.keys(record)\n .map(Number)\n .sort((a, b) => a - b)) {\n const entry = record[priority];\n if (entry) {\n sort[String(entry.property)] = entry.type === 'desc' ? -1 : 1;\n }\n }\n }\n\n return sort;\n}\n"],"names":["foldedPattern","BINARY_OPERATORS","eq","ne","gt","gte","lt","lte","isFilterGroup","node","operator","translateNode","clause","values","map","field","String","property","value","$in","$nin","$gte","start","$lte","end","$not","$regex","$options","$eq","$ne","_never","translateFiltering","filtering","length","clauses","entry","Array","isArray","push","$and","translateSorting","sorting","sort","record","priority","Object","keys","Number","a","b","type"],"mappings":"AAAA,SAMEA,aAAa,QACR,gBAAgB;AAOvB,MAAMC,mBAA2C;IAC/CC,IAAI;IACJC,IAAI;IACJC,IAAI;IACJC,KAAK;IACLC,IAAI;IACJC,KAAK;AACP;AAEA,SAASC,cACPC,IAAkD;IAElD,4EAA4E;IAC5E,qBAAqB;IACrB,OACE,CAAE,CAAA,cAAcA,IAAG,KAAOA,CAAAA,KAAKC,QAAQ,KAAK,SAASD,KAAKC,QAAQ,KAAK,IAAG;AAE9E;AAEA,SAASC,cACPF,IAAkD;IAElD,IAAID,cAAcC,OAAO;QACvB,MAAMG,SAASH,KAAKC,QAAQ,KAAK,OAAO,QAAQ;QAChD,OAAO;YAAE,CAACE,OAAO,EAAEH,KAAKI,MAAM,CAACC,GAAG,CAACH;QAAe;IACpD;IAEA,MAAMI,QAAQC,OAAOP,KAAKQ,QAAQ;IAElC,OAAQR,KAAKC,QAAQ;QACnB,KAAK;QACL,KAAK;QACL,KAAK;QACL,KAAK;QACL,KAAK;QACL,KAAK;YACH,OAAO;gBAAE,CAACK,MAAM,EAAE;oBAAE,CAACd,gBAAgB,CAACQ,KAAKC,QAAQ,CAAC,CAAC,EAAED,KAAKS,KAAK;gBAAC;YAAE;QACtE,KAAK;YACH,OAAO;gBAAE,CAACH,MAAM,EAAE;oBAAEI,KAAKV,KAAKI,MAAM;gBAAC;YAAE;QACzC,KAAK;YACH,OAAO;gBAAE,CAACE,MAAM,EAAE;oBAAEK,MAAMX,KAAKI,MAAM;gBAAC;YAAE;QAC1C,KAAK;YACH,OAAO;gBAAE,CAACE,MAAM,EAAE;oBAAEM,MAAMZ,KAAKa,KAAK;oBAAEC,MAAMd,KAAKe,GAAG;gBAAC;YAAE;QACzD,KAAK;YACH,OAAO;gBAAE,CAACT,MAAM,EAAE;oBAAEU,MAAM;wBAAEJ,MAAMZ,KAAKa,KAAK;wBAAEC,MAAMd,KAAKe,GAAG;oBAAC;gBAAE;YAAE;QACnE,KAAK;YACH,wEAAwE;YACxE,wEAAwE;YACxE,kEAAkE;YAClE,OAAO;gBAAE,CAACT,MAAM,EAAE;oBAAEW,QAAQ1B,cAAcS,KAAKS,KAAK;oBAAGS,UAAU;gBAAI;YAAE;QACzE,KAAK;YACH,OAAO;gBACL,CAACZ,MAAM,EAAE;oBACPU,MAAM;wBAAEC,QAAQ1B,cAAcS,KAAKS,KAAK;wBAAGS,UAAU;oBAAI;gBAC3D;YACF;QACF,KAAK;YACH,OAAO;gBAAE,CAACZ,MAAM,EAAE;oBAAEa,KAAK;gBAAK;YAAE;QAClC,KAAK;YACH,OAAO;gBAAE,CAACb,MAAM,EAAE;oBAAEc,KAAK;gBAAK;YAAE;QAClC;YAAS;gBACP,oEAAoE;gBACpE,MAAMC,SAAgBrB;gBACtB,OAAOqB;YACT;IACF;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASC,mBACdC,SAAsC;IAEtC,IAAI,CAACA,aAAaA,UAAUC,MAAM,KAAK,GAAG;QACxC,OAAO,CAAC;IACV;IAEA,MAAMC,UAAyB,EAAE;IACjC,KAAK,MAAMC,SAASH,UAAW;QAC7B,IAAII,MAAMC,OAAO,CAACF,QAAQ;YACxBD,QAAQI,IAAI,IAAIH,MAAMrB,GAAG,CAACH;QAC5B,OAAO;YACLuB,QAAQI,IAAI,CAAC3B,cAAcwB;QAC7B;IACF;IAEA,IAAID,QAAQD,MAAM,KAAK,GAAG,OAAO,CAAC;IAClC,IAAIC,QAAQD,MAAM,KAAK,GAAG,OAAOC,OAAO,CAAC,EAAE;IAC3C,OAAO;QAAEK,MAAML;IAAQ;AACzB;AAEA;;;;CAIC,GACD,OAAO,SAASM,iBACdC,OAAkC;IAElC,MAAMC,OAAkB,CAAC;IACzB,IAAI,CAACD,SAAS,OAAOC;IAErB,KAAK,MAAMC,UAAUF,QAAS;QAC5B,KAAK,MAAMG,YAAYC,OAAOC,IAAI,CAACH,QAChC7B,GAAG,CAACiC,QACJL,IAAI,CAAC,CAACM,GAAGC,IAAMD,IAAIC,GAAI;YACxB,MAAMd,QAAQQ,MAAM,CAACC,SAAS;YAC9B,IAAIT,OAAO;gBACTO,IAAI,CAAC1B,OAAOmB,MAAMlB,QAAQ,EAAE,GAAGkB,MAAMe,IAAI,KAAK,SAAS,CAAC,IAAI;YAC9D;QACF;IACF;IAEA,OAAOR;AACT"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EntityFacetCounter } from '@entifix/business';
|
|
2
|
+
import { type Entity, type EntityConstructor } from '@entifix/core';
|
|
3
|
+
import type { ClientSession, Db } from 'mongodb';
|
|
4
|
+
/**
|
|
5
|
+
* Builds an {@link EntityFacetCounter} for one entity over a live mongodb
|
|
6
|
+
* {@link Db}: every facet in **one** aggregation, a `$facet` whose branches
|
|
7
|
+
* each `$match` their own filtering, so a disjunctive panel costs one round
|
|
8
|
+
* trip however many facets it shows.
|
|
9
|
+
*
|
|
10
|
+
* Filtering goes through the same translator the repository's `load` uses, so a
|
|
11
|
+
* count and the page beside it can never disagree about what a filter means.
|
|
12
|
+
* A value that is not a string, number or boolean is dropped, as in core's
|
|
13
|
+
* in-memory counter.
|
|
14
|
+
*/
|
|
15
|
+
export declare function makeMongoFacetCounter<TEntity extends Entity>(db: Db, entityConstructor: EntityConstructor<TEntity>, options?: {
|
|
16
|
+
readonly session?: ClientSession;
|
|
17
|
+
}): EntityFacetCounter;
|
|
18
|
+
//# sourceMappingURL=make-mongo-facet-counter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"make-mongo-facet-counter.d.ts","sourceRoot":"","sources":["../../src/adapters/make-mongo-facet-counter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAEL,KAAK,MAAM,EACX,KAAK,iBAAiB,EAIvB,MAAM,eAAe,CAAC;AAEvB,OAAO,KAAK,EAAE,aAAa,EAAE,EAAE,EAAY,MAAM,SAAS,CAAC;AA+B3D;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,SAAS,MAAM,EAC1D,EAAE,EAAE,EAAE,EACN,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAA;CAAE,GAC7C,kBAAkB,CA2CpB"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { compareStoredValues } from '@entifix/core';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
import { translateMongoError } from '../errors/translate-mongo-error.js';
|
|
4
|
+
import { translateFiltering } from './filter-translator.js';
|
|
5
|
+
import { mongoCollectionName } from './make-mongo-repository.js';
|
|
6
|
+
const isFacetValue = (value)=>typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
|
|
7
|
+
/** Count first, highest first; a tie by value — the order core's counter keeps. */ const byCountThenValue = (left, right)=>right.count - left.count || compareStoredValues(left.value, right.value);
|
|
8
|
+
/**
|
|
9
|
+
* One `$facet` branch: the member's own filtering, then one row per value.
|
|
10
|
+
*
|
|
11
|
+
* `$unwind` is in every branch, not only an array member's: on a scalar it
|
|
12
|
+
* passes the document through, and on a missing or `null` value it drops it —
|
|
13
|
+
* which is the "a missing value is not counted" rule, for free.
|
|
14
|
+
*/ const branchFor = ({ member, filtering })=>[
|
|
15
|
+
{
|
|
16
|
+
$match: translateFiltering(filtering)
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
$unwind: `$${member}`
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
$group: {
|
|
23
|
+
_id: `$${member}`,
|
|
24
|
+
count: {
|
|
25
|
+
$sum: 1
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Builds an {@link EntityFacetCounter} for one entity over a live mongodb
|
|
32
|
+
* {@link Db}: every facet in **one** aggregation, a `$facet` whose branches
|
|
33
|
+
* each `$match` their own filtering, so a disjunctive panel costs one round
|
|
34
|
+
* trip however many facets it shows.
|
|
35
|
+
*
|
|
36
|
+
* Filtering goes through the same translator the repository's `load` uses, so a
|
|
37
|
+
* count and the page beside it can never disagree about what a filter means.
|
|
38
|
+
* A value that is not a string, number or boolean is dropped, as in core's
|
|
39
|
+
* in-memory counter.
|
|
40
|
+
*/ export function makeMongoFacetCounter(db, entityConstructor, options) {
|
|
41
|
+
const collection = db.collection(mongoCollectionName(entityConstructor));
|
|
42
|
+
const inSession = (options == null ? void 0 : options.session) ? {
|
|
43
|
+
session: options.session
|
|
44
|
+
} : {};
|
|
45
|
+
const count = (queries)=>Effect.gen(function*() {
|
|
46
|
+
// A `$facet` with no branch is a server error; asking for nothing is
|
|
47
|
+
// not.
|
|
48
|
+
if (queries.length === 0) return {};
|
|
49
|
+
const [result] = yield* Effect.tryPromise({
|
|
50
|
+
try: ()=>collection.aggregate([
|
|
51
|
+
{
|
|
52
|
+
$facet: Object.fromEntries(queries.map((query)=>[
|
|
53
|
+
query.member,
|
|
54
|
+
branchFor(query)
|
|
55
|
+
]))
|
|
56
|
+
}
|
|
57
|
+
], inSession).toArray(),
|
|
58
|
+
catch: (error)=>translateMongoError(error)
|
|
59
|
+
});
|
|
60
|
+
const counts = {};
|
|
61
|
+
for (const { member } of queries){
|
|
62
|
+
var _ref;
|
|
63
|
+
const rows = (_ref = result == null ? void 0 : result[member]) != null ? _ref : [];
|
|
64
|
+
counts[member] = rows.filter((row)=>isFacetValue(row['_id'])).map((row)=>({
|
|
65
|
+
value: row['_id'],
|
|
66
|
+
count: row['count']
|
|
67
|
+
})).sort(byCountThenValue);
|
|
68
|
+
}
|
|
69
|
+
return counts;
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
count
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=make-mongo-facet-counter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/make-mongo-facet-counter.ts"],"sourcesContent":["import type { EntityFacetCounter } from '@entifix/business';\nimport {\n compareStoredValues,\n type Entity,\n type EntityConstructor,\n type EntityFacetCounts,\n type EntityFacetQuery,\n type EntityFacetValue,\n} from '@entifix/core';\nimport { Effect } from 'effect';\nimport type { ClientSession, Db, Document } from 'mongodb';\n\nimport { translateMongoError } from '../errors/translate-mongo-error.js';\nimport { translateFiltering } from './filter-translator.js';\nimport { mongoCollectionName } from './make-mongo-repository.js';\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 — the order core's counter keeps. */\nconst byCountThenValue = (left: EntityFacetValue, right: EntityFacetValue) =>\n right.count - left.count || compareStoredValues(left.value, right.value);\n\n/**\n * One `$facet` branch: the member's own filtering, then one row per value.\n *\n * `$unwind` is in every branch, not only an array member's: on a scalar it\n * passes the document through, and on a missing or `null` value it drops it —\n * which is the \"a missing value is not counted\" rule, for free.\n */\nconst branchFor = <TEntity extends Entity>({\n member,\n filtering,\n}: EntityFacetQuery<TEntity>): Document[] => [\n { $match: translateFiltering(filtering) },\n { $unwind: `$${member}` },\n { $group: { _id: `$${member}`, count: { $sum: 1 } } },\n];\n\n/**\n * Builds an {@link EntityFacetCounter} for one entity over a live mongodb\n * {@link Db}: every facet in **one** aggregation, a `$facet` whose branches\n * each `$match` their own filtering, so a disjunctive panel costs one round\n * trip however many facets it shows.\n *\n * Filtering goes through the same translator the repository's `load` uses, so a\n * count and the page beside it can never disagree about what a filter means.\n * A value that is not a string, number or boolean is dropped, as in core's\n * in-memory counter.\n */\nexport function makeMongoFacetCounter<TEntity extends Entity>(\n db: Db,\n entityConstructor: EntityConstructor<TEntity>,\n options?: { readonly session?: ClientSession },\n): EntityFacetCounter {\n const collection = db.collection(mongoCollectionName(entityConstructor));\n const inSession = options?.session ? { session: options.session } : {};\n\n const count = <T extends Entity>(queries: readonly EntityFacetQuery<T>[]) =>\n Effect.gen(function* () {\n // A `$facet` with no branch is a server error; asking for nothing is\n // not.\n if (queries.length === 0) return {} satisfies EntityFacetCounts;\n\n const [result] = yield* Effect.tryPromise({\n try: () =>\n collection\n .aggregate(\n [\n {\n $facet: Object.fromEntries(\n queries.map(query => [query.member, branchFor(query)]),\n ),\n },\n ],\n inSession,\n )\n .toArray(),\n catch: error => translateMongoError(error),\n });\n\n const counts: Record<string, readonly EntityFacetValue[]> = {};\n for (const { member } of queries) {\n const rows = ((result as Document | undefined)?.[member] ??\n []) as Document[];\n counts[member] = rows\n .filter(row => isFacetValue(row['_id']))\n .map(row => ({\n value: row['_id'] as EntityFacetValue['value'],\n count: row['count'] as number,\n }))\n .sort(byCountThenValue);\n }\n return counts satisfies EntityFacetCounts;\n });\n\n return { count };\n}\n"],"names":["compareStoredValues","Effect","translateMongoError","translateFiltering","mongoCollectionName","isFacetValue","value","byCountThenValue","left","right","count","branchFor","member","filtering","$match","$unwind","$group","_id","$sum","makeMongoFacetCounter","db","entityConstructor","options","collection","inSession","session","queries","gen","length","result","tryPromise","try","aggregate","$facet","Object","fromEntries","map","query","toArray","catch","error","counts","rows","filter","row","sort"],"mappings":"AACA,SACEA,mBAAmB,QAMd,gBAAgB;AACvB,SAASC,MAAM,QAAQ,SAAS;AAGhC,SAASC,mBAAmB,QAAQ,qCAAqC;AACzE,SAASC,kBAAkB,QAAQ,yBAAyB;AAC5D,SAASC,mBAAmB,QAAQ,6BAA6B;AAEjE,MAAMC,eAAe,CAACC,QACpB,OAAOA,UAAU,YACjB,OAAOA,UAAU,YACjB,OAAOA,UAAU;AAEnB,iFAAiF,GACjF,MAAMC,mBAAmB,CAACC,MAAwBC,QAChDA,MAAMC,KAAK,GAAGF,KAAKE,KAAK,IAAIV,oBAAoBQ,KAAKF,KAAK,EAAEG,MAAMH,KAAK;AAEzE;;;;;;CAMC,GACD,MAAMK,YAAY,CAAyB,EACzCC,MAAM,EACNC,SAAS,EACiB,GAAiB;QAC3C;YAAEC,QAAQX,mBAAmBU;QAAW;QACxC;YAAEE,SAAS,CAAC,CAAC,EAAEH,QAAQ;QAAC;QACxB;YAAEI,QAAQ;gBAAEC,KAAK,CAAC,CAAC,EAAEL,QAAQ;gBAAEF,OAAO;oBAAEQ,MAAM;gBAAE;YAAE;QAAE;KACrD;AAED;;;;;;;;;;CAUC,GACD,OAAO,SAASC,sBACdC,EAAM,EACNC,iBAA6C,EAC7CC,OAA8C;IAE9C,MAAMC,aAAaH,GAAGG,UAAU,CAACnB,oBAAoBiB;IACrD,MAAMG,YAAYF,CAAAA,2BAAAA,QAASG,OAAO,IAAG;QAAEA,SAASH,QAAQG,OAAO;IAAC,IAAI,CAAC;IAErE,MAAMf,QAAQ,CAAmBgB,UAC/BzB,OAAO0B,GAAG,CAAC;YACT,qEAAqE;YACrE,OAAO;YACP,IAAID,QAAQE,MAAM,KAAK,GAAG,OAAO,CAAC;YAElC,MAAM,CAACC,OAAO,GAAG,OAAO5B,OAAO6B,UAAU,CAAC;gBACxCC,KAAK,IACHR,WACGS,SAAS,CACR;wBACE;4BACEC,QAAQC,OAAOC,WAAW,CACxBT,QAAQU,GAAG,CAACC,CAAAA,QAAS;oCAACA,MAAMzB,MAAM;oCAAED,UAAU0B;iCAAO;wBAEzD;qBACD,EACDb,WAEDc,OAAO;gBACZC,OAAOC,CAAAA,QAAStC,oBAAoBsC;YACtC;YAEA,MAAMC,SAAsD,CAAC;YAC7D,KAAK,MAAM,EAAE7B,MAAM,EAAE,IAAIc,QAAS;;gBAChC,MAAMgB,eAASb,0BAAD,AAACA,MAAiC,CAACjB,OAAO,mBACtD,EAAE;gBACJ6B,MAAM,CAAC7B,OAAO,GAAG8B,KACdC,MAAM,CAACC,CAAAA,MAAOvC,aAAauC,GAAG,CAAC,MAAM,GACrCR,GAAG,CAACQ,CAAAA,MAAQ,CAAA;wBACXtC,OAAOsC,GAAG,CAAC,MAAM;wBACjBlC,OAAOkC,GAAG,CAAC,QAAQ;oBACrB,CAAA,GACCC,IAAI,CAACtC;YACV;YACA,OAAOkC;QACT;IAEF,OAAO;QAAE/B;IAAM;AACjB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"make-mongo-link-resolver.d.ts","sourceRoot":"","sources":["../../src/adapters/make-mongo-link-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,
|
|
1
|
+
{"version":3,"file":"make-mongo-link-resolver.d.ts","sourceRoot":"","sources":["../../src/adapters/make-mongo-link-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EACL,MAAM,EACN,iBAAiB,EAIlB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,OAAO,EAAU,MAAM,QAAQ,CAAC;AACzC,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAIlC;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,EAAE,EACN,aAAa,EAAE,aAAa,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,GACtD,OAAO,CAAC,OAAO,CAAC,qBAAqB,CAAC,CA2BxC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EntityLinkResolverTag } from '@entifix/business';
|
|
2
|
-
import {
|
|
2
|
+
import { Internal } from '@entifix/core';
|
|
3
3
|
import { Context, Effect } from 'effect';
|
|
4
4
|
import { makeMongoRepository } from './make-mongo-repository.js';
|
|
5
5
|
/**
|
|
@@ -18,9 +18,7 @@ import { makeMongoRepository } from './make-mongo-repository.js';
|
|
|
18
18
|
resolve (entityConstructor, id) {
|
|
19
19
|
const repository = repositoryByConstructor.get(entityConstructor);
|
|
20
20
|
if (!repository) {
|
|
21
|
-
return Effect.fail(new
|
|
22
|
-
entity: entityConstructor.name
|
|
23
|
-
}));
|
|
21
|
+
return Effect.fail(new Internal());
|
|
24
22
|
}
|
|
25
23
|
return repository.get(id);
|
|
26
24
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adapters/make-mongo-link-resolver.ts"],"sourcesContent":["import { EntityLinkResolverTag } from '@entifix/business';\nimport {\n
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/make-mongo-link-resolver.ts"],"sourcesContent":["import { EntityLinkResolverTag } from '@entifix/business';\nimport {\n Entity,\n EntityConstructor,\n EntityLinkResolver,\n Internal,\n type TransitionalFailure,\n} from '@entifix/core';\nimport { Context, Effect } from 'effect';\nimport type { Db } from 'mongodb';\n\nimport { makeMongoRepository } from './make-mongo-repository.js';\n\n/**\n * Builds an {@link EntityLinkResolverTag} context backed by Mongo repositories,\n * so a load use-case can materialize not-yet-loaded {@link EntityLink}s on the\n * backend. Mirrors the REST `createEntityLinkResolver`, but each Mongo `get`\n * already has no outstanding requirements, so nothing needs to be provided.\n *\n * `registrations` lists the link-target constructors this resolver can fetch.\n */\nexport function makeMongoLinkResolver(\n db: Db,\n registrations: ReadonlyArray<EntityConstructor<Entity>>,\n): Context.Context<EntityLinkResolverTag> {\n const repositoryByConstructor = new Map(\n registrations.map(\n entityConstructor =>\n [\n entityConstructor,\n makeMongoRepository(db, entityConstructor),\n ] as const,\n ),\n );\n\n const resolver: EntityLinkResolver = {\n resolve(entityConstructor, id) {\n const repository = repositoryByConstructor.get(\n entityConstructor as EntityConstructor<Entity>,\n );\n if (!repository) {\n return Effect.fail(new Internal());\n }\n return repository.get(id) as unknown as Effect.Effect<\n never,\n TransitionalFailure\n >;\n },\n };\n\n return Context.make(EntityLinkResolverTag, resolver);\n}\n"],"names":["EntityLinkResolverTag","Internal","Context","Effect","makeMongoRepository","makeMongoLinkResolver","db","registrations","repositoryByConstructor","Map","map","entityConstructor","resolver","resolve","id","repository","get","fail","make"],"mappings":"AAAA,SAASA,qBAAqB,QAAQ,oBAAoB;AAC1D,SAIEC,QAAQ,QAEH,gBAAgB;AACvB,SAASC,OAAO,EAAEC,MAAM,QAAQ,SAAS;AAGzC,SAASC,mBAAmB,QAAQ,6BAA6B;AAEjE;;;;;;;CAOC,GACD,OAAO,SAASC,sBACdC,EAAM,EACNC,aAAuD;IAEvD,MAAMC,0BAA0B,IAAIC,IAClCF,cAAcG,GAAG,CACfC,CAAAA,oBACE;YACEA;YACAP,oBAAoBE,IAAIK;SACzB;IAIP,MAAMC,WAA+B;QACnCC,SAAQF,iBAAiB,EAAEG,EAAE;YAC3B,MAAMC,aAAaP,wBAAwBQ,GAAG,CAC5CL;YAEF,IAAI,CAACI,YAAY;gBACf,OAAOZ,OAAOc,IAAI,CAAC,IAAIhB;YACzB;YACA,OAAOc,WAAWC,GAAG,CAACF;QAIxB;IACF;IAEA,OAAOZ,QAAQgB,IAAI,CAAClB,uBAAuBY;AAC7C"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { EntityRepository } from '@entifix/business';
|
|
2
|
-
import {
|
|
2
|
+
import { Entity, EntityConstructor } from '@entifix/core';
|
|
3
3
|
import type { ClientSession, Db } from 'mongodb';
|
|
4
|
+
/** The collection an entity's records live in: its `key`, else its class name. */
|
|
5
|
+
export declare function mongoCollectionName<TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>): string;
|
|
4
6
|
/**
|
|
5
7
|
* Builds an {@link EntityRepository} for a single entity type, backed by a live
|
|
6
8
|
* mongodb {@link Db}. The `db` and `entityConstructor` are captured in closures,
|
|
@@ -10,7 +12,8 @@ import type { ClientSession, Db } from 'mongodb';
|
|
|
10
12
|
* Collection name = the entity's `key` (falling back to its class name). Reads
|
|
11
13
|
* reuse the shared entifix deserializer (rebuilding links from embedded objects
|
|
12
14
|
* or scalar FKs); writes reuse the shared serializer. Mongo failures surface as
|
|
13
|
-
*
|
|
15
|
+
* categorised failures — `NotFound`, `DuplicateKey`, `Unavailable` and the
|
|
16
|
+
* rest — through `translateMongoError`.
|
|
14
17
|
*
|
|
15
18
|
* This is the Mongo mirror of the REST `buildEntityRestAdapter*` builders.
|
|
16
19
|
*
|
|
@@ -21,5 +24,4 @@ import type { ClientSession, Db } from 'mongodb';
|
|
|
21
24
|
export declare function makeMongoRepository<TEntity extends Entity>(db: Db, entityConstructor: EntityConstructor<TEntity>, options?: {
|
|
22
25
|
readonly session?: ClientSession;
|
|
23
26
|
}): EntityRepository;
|
|
24
|
-
export type { EntifixError };
|
|
25
27
|
//# sourceMappingURL=make-mongo-repository.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"make-mongo-repository.d.ts","sourceRoot":"","sources":["../../src/adapters/make-mongo-repository.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,
|
|
1
|
+
{"version":3,"file":"make-mongo-repository.d.ts","sourceRoot":"","sources":["../../src/adapters/make-mongo-repository.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAGL,MAAM,EACN,iBAAiB,EAOlB,MAAM,eAAe,CAAC;AAEvB,OAAO,KAAK,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAQjD,kFAAkF;AAClF,wBAAgB,mBAAmB,CAAC,OAAO,SAAS,MAAM,EACxD,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAC5C,MAAM,CAGR;AAQD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,SAAS,MAAM,EACxD,EAAE,EAAE,EAAE,EACN,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAA;CAAE,GAC7C,gBAAgB,CAgGlB"}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { deserializeEntityCollection, deserializeSingleEntity,
|
|
3
|
+
import { deserializeEntityCollection, deserializeSingleEntity, extractMetaEntity, NotFound, serializeEntity } from '@entifix/core';
|
|
4
4
|
import { Effect } from 'effect';
|
|
5
|
+
import { translateMongoError } from '../errors/translate-mongo-error.js';
|
|
5
6
|
import { translateFiltering, translateSorting } from './filter-translator.js';
|
|
6
7
|
/** Projection that drops Mongo's internal `_id` from read results. */ const WITHOUT_MONGO_ID = {
|
|
7
8
|
projection: {
|
|
8
9
|
_id: 0
|
|
9
10
|
}
|
|
10
11
|
};
|
|
11
|
-
function
|
|
12
|
+
/** The collection an entity's records live in: its `key`, else its class name. */ export function mongoCollectionName(entityConstructor) {
|
|
12
13
|
var _metaEntity_key;
|
|
13
14
|
const metaEntity = extractMetaEntity(entityConstructor);
|
|
14
15
|
return (_metaEntity_key = metaEntity.key) != null ? _metaEntity_key : metaEntity.name;
|
|
@@ -25,7 +26,8 @@ function toEntityId(entityOrId) {
|
|
|
25
26
|
* Collection name = the entity's `key` (falling back to its class name). Reads
|
|
26
27
|
* reuse the shared entifix deserializer (rebuilding links from embedded objects
|
|
27
28
|
* or scalar FKs); writes reuse the shared serializer. Mongo failures surface as
|
|
28
|
-
*
|
|
29
|
+
* categorised failures — `NotFound`, `DuplicateKey`, `Unavailable` and the
|
|
30
|
+
* rest — through `translateMongoError`.
|
|
29
31
|
*
|
|
30
32
|
* This is the Mongo mirror of the REST `buildEntityRestAdapter*` builders.
|
|
31
33
|
*
|
|
@@ -33,14 +35,11 @@ function toEntityId(entityOrId) {
|
|
|
33
35
|
* work's handle — so its writes commit with the unit and its reads see what the
|
|
34
36
|
* unit has already written.
|
|
35
37
|
*/ export function makeMongoRepository(db, entityConstructor, options) {
|
|
36
|
-
const collection = db.collection(
|
|
38
|
+
const collection = db.collection(mongoCollectionName(entityConstructor));
|
|
37
39
|
// Spread into every call, so a repository with no session passes none.
|
|
38
40
|
const inSession = (options == null ? void 0 : options.session) ? {
|
|
39
41
|
session: options.session
|
|
40
42
|
} : {};
|
|
41
|
-
const fail = (message, error, details)=>new EntifixConnError(message, error, _extends({
|
|
42
|
-
entity: entityConstructor.name
|
|
43
|
-
}, details));
|
|
44
43
|
const load = (request)=>Effect.gen(function*() {
|
|
45
44
|
var _request_page, _request_pageSize;
|
|
46
45
|
const filter = translateFiltering(request.filtering);
|
|
@@ -50,11 +49,11 @@ function toEntityId(entityOrId) {
|
|
|
50
49
|
const skip = (page - 1) * pageSize;
|
|
51
50
|
const docs = yield* Effect.tryPromise({
|
|
52
51
|
try: ()=>collection.find(filter, _extends({}, WITHOUT_MONGO_ID, inSession)).sort(sort).skip(skip).limit(pageSize).toArray(),
|
|
53
|
-
catch: (error)=>
|
|
52
|
+
catch: (error)=>translateMongoError(error)
|
|
54
53
|
});
|
|
55
54
|
const total = yield* Effect.tryPromise({
|
|
56
55
|
try: ()=>collection.countDocuments(filter, inSession),
|
|
57
|
-
catch: (error)=>
|
|
56
|
+
catch: (error)=>translateMongoError(error)
|
|
58
57
|
});
|
|
59
58
|
const items = yield* deserializeEntityCollection(entityConstructor, docs);
|
|
60
59
|
return {
|
|
@@ -68,14 +67,12 @@ function toEntityId(entityOrId) {
|
|
|
68
67
|
try: ()=>collection.findOne({
|
|
69
68
|
id
|
|
70
69
|
}, _extends({}, WITHOUT_MONGO_ID, inSession)),
|
|
71
|
-
catch: (error)=>
|
|
72
|
-
id
|
|
73
|
-
})
|
|
70
|
+
catch: (error)=>translateMongoError(error)
|
|
74
71
|
});
|
|
75
72
|
const entity = yield* deserializeSingleEntity(entityConstructor, doc);
|
|
76
73
|
if (entity === undefined) {
|
|
77
|
-
return yield* Effect.fail(
|
|
78
|
-
|
|
74
|
+
return yield* Effect.fail(new NotFound({
|
|
75
|
+
entity: extractMetaEntity(entityConstructor).key
|
|
79
76
|
}));
|
|
80
77
|
}
|
|
81
78
|
return entity;
|
|
@@ -98,9 +95,7 @@ function toEntityId(entityOrId) {
|
|
|
98
95
|
}), _extends({
|
|
99
96
|
upsert: true
|
|
100
97
|
}, inSession)),
|
|
101
|
-
catch: (error)=>
|
|
102
|
-
id
|
|
103
|
-
})
|
|
98
|
+
catch: (error)=>translateMongoError(error)
|
|
104
99
|
});
|
|
105
100
|
entity.id = id;
|
|
106
101
|
return entity;
|
|
@@ -111,9 +106,7 @@ function toEntityId(entityOrId) {
|
|
|
111
106
|
try: ()=>collection.deleteOne({
|
|
112
107
|
id
|
|
113
108
|
}, inSession),
|
|
114
|
-
catch: (error)=>
|
|
115
|
-
id
|
|
116
|
-
})
|
|
109
|
+
catch: (error)=>translateMongoError(error)
|
|
117
110
|
});
|
|
118
111
|
});
|
|
119
112
|
// Cast through the interface to keep its generic signatures: each method here
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adapters/make-mongo-repository.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type { EntityRepository } from '@entifix/business';\nimport {\n deserializeEntityCollection,\n deserializeSingleEntity,\n
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/make-mongo-repository.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type { EntityRepository } from '@entifix/business';\nimport {\n deserializeEntityCollection,\n deserializeSingleEntity,\n Entity,\n EntityConstructor,\n EntityId,\n EntityLoadRequest,\n EntityPage,\n extractMetaEntity,\n NotFound,\n serializeEntity,\n} from '@entifix/core';\nimport { Effect } from 'effect';\nimport type { ClientSession, Db } from 'mongodb';\n\nimport { translateMongoError } from '../errors/translate-mongo-error.js';\nimport { translateFiltering, translateSorting } from './filter-translator.js';\n\n/** Projection that drops Mongo's internal `_id` from read results. */\nconst WITHOUT_MONGO_ID = { projection: { _id: 0 } } as const;\n\n/** The collection an entity's records live in: its `key`, else its class name. */\nexport function mongoCollectionName<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n): string {\n const metaEntity = extractMetaEntity(entityConstructor);\n return metaEntity.key ?? metaEntity.name;\n}\n\nfunction toEntityId(entityOrId: EntityId | Entity): EntityId {\n return entityOrId != null && typeof entityOrId === 'object'\n ? (entityOrId as Entity).id\n : (entityOrId as EntityId);\n}\n\n/**\n * Builds an {@link EntityRepository} for a single entity type, backed by a live\n * mongodb {@link Db}. The `db` and `entityConstructor` are captured in closures,\n * so every method's Effect has no outstanding requirements (`R = never`), which\n * is exactly what the interface asks.\n *\n * Collection name = the entity's `key` (falling back to its class name). Reads\n * reuse the shared entifix deserializer (rebuilding links from embedded objects\n * or scalar FKs); writes reuse the shared serializer. Mongo failures surface as\n * categorised failures — `NotFound`, `DuplicateKey`, `Unavailable` and the\n * rest — through `translateMongoError`.\n *\n * This is the Mongo mirror of the REST `buildEntityRestAdapter*` builders.\n *\n * Pass `session` to make the repository part of a transaction — a unit of\n * work's handle — so its writes commit with the unit and its reads see what the\n * unit has already written.\n */\nexport function makeMongoRepository<TEntity extends Entity>(\n db: Db,\n entityConstructor: EntityConstructor<TEntity>,\n options?: { readonly session?: ClientSession },\n): EntityRepository {\n const collection = db.collection(mongoCollectionName(entityConstructor));\n // Spread into every call, so a repository with no session passes none.\n const inSession = options?.session ? { session: options.session } : {};\n\n const load = <T extends Entity>(request: EntityLoadRequest<T>) =>\n Effect.gen(function* () {\n const filter = translateFiltering(request.filtering);\n const sort = translateSorting(request.sorting);\n const page = request.page ?? 1;\n const pageSize = request.pageSize ?? 10;\n const skip = (page - 1) * pageSize;\n\n const docs = yield* Effect.tryPromise({\n try: () =>\n collection\n .find(filter, { ...WITHOUT_MONGO_ID, ...inSession })\n .sort(sort)\n .skip(skip)\n .limit(pageSize)\n .toArray(),\n catch: error => translateMongoError(error),\n });\n const total = yield* Effect.tryPromise({\n try: () => collection.countDocuments(filter, inSession),\n catch: error => translateMongoError(error),\n });\n\n const items = yield* deserializeEntityCollection(entityConstructor, docs);\n\n return {\n items: items as unknown as T[],\n total,\n request,\n } satisfies EntityPage<T>;\n });\n\n const get = <T extends Entity>(id: EntityId) =>\n Effect.gen(function* () {\n const doc = yield* Effect.tryPromise({\n try: () =>\n collection.findOne({ id }, { ...WITHOUT_MONGO_ID, ...inSession }),\n catch: error => translateMongoError(error),\n });\n const entity = yield* deserializeSingleEntity(entityConstructor, doc);\n if (entity === undefined) {\n return yield* Effect.fail(\n new NotFound({ entity: extractMetaEntity(entityConstructor).key }),\n );\n }\n return entity as unknown as T;\n });\n\n const save = <T extends Entity>(entity: T) =>\n Effect.gen(function* () {\n const doc = serializeEntity(\n entityConstructor,\n entity as unknown as TEntity,\n );\n // A create arrives without an id, so the store mints one. Entities are\n // addressed by their own `id` field (never Mongo's `_id`, which reads\n // project away), so an inserted document without one would be\n // unreachable by `get` and by any later `replaceOne`.\n const id = entity.id ?? randomUUID();\n yield* Effect.tryPromise({\n // `doc` omits `id` when it was undefined (the serializer drops\n // undefined), hence spreading the resolved id back in.\n try: () =>\n collection.replaceOne(\n { id },\n { ...doc, id },\n { upsert: true, ...inSession },\n ),\n catch: error => translateMongoError(error),\n });\n entity.id = id;\n return entity;\n });\n\n const del = <T extends Entity>(entityOrId: EntityId | T) =>\n Effect.gen(function* () {\n const id = toEntityId(entityOrId);\n yield* Effect.tryPromise({\n try: () => collection.deleteOne({ id }, inSession),\n catch: error => translateMongoError(error),\n });\n });\n\n // Cast through the interface to keep its generic signatures: each method here\n // is written against this entity's type, the interface against any entity's.\n return {\n load,\n get,\n save,\n delete: del,\n } as unknown as EntityRepository;\n}\n"],"names":["randomUUID","deserializeEntityCollection","deserializeSingleEntity","extractMetaEntity","NotFound","serializeEntity","Effect","translateMongoError","translateFiltering","translateSorting","WITHOUT_MONGO_ID","projection","_id","mongoCollectionName","entityConstructor","metaEntity","key","name","toEntityId","entityOrId","id","makeMongoRepository","db","options","collection","inSession","session","load","request","gen","filter","filtering","sort","sorting","page","pageSize","skip","docs","tryPromise","try","find","limit","toArray","catch","error","total","countDocuments","items","get","doc","findOne","entity","undefined","fail","save","replaceOne","upsert","del","deleteOne","delete"],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { EntityFacetCounterFactoryTag, TenantDatabaseResolverTag } from '@entifix/business';
|
|
2
|
+
import { Layer } from 'effect';
|
|
3
|
+
import { MongoDatabaseTag } from '../mongo-database/mongo-database.js';
|
|
4
|
+
/**
|
|
5
|
+
* Facet counters over the service's one database, whoever the request is for —
|
|
6
|
+
* `MongoRepositoryFactoryLayer`'s counterpart, bound beside it.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MongoFacetCounterFactoryLayer: Layer.Layer<EntityFacetCounterFactoryTag, never, MongoDatabaseTag>;
|
|
9
|
+
/**
|
|
10
|
+
* Facet counters over the requesting organization's own database, resolved
|
|
11
|
+
* per request through the host's {@link TenantDatabaseResolverTag}.
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ A request that names no organization is **refused**, never counted from a
|
|
14
|
+
* default: on a per-organization store a default database is some other
|
|
15
|
+
* organization's, and its counts would say how much stock a competitor holds.
|
|
16
|
+
*/
|
|
17
|
+
export declare const MongoTenantFacetCounterFactoryLayer: Layer.Layer<EntityFacetCounterFactoryTag, never, TenantDatabaseResolverTag>;
|
|
18
|
+
//# sourceMappingURL=mongo-facet-counter-factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mongo-facet-counter-factory.d.ts","sourceRoot":"","sources":["../../src/adapters/mongo-facet-counter-factory.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAE5B,yBAAyB,EAC1B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AAGvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAGvE;;;GAGG;AACH,eAAO,MAAM,6BAA6B,oEASzC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,mCAAmC,6EAc/C,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { EntityFacetCounterFactoryTag, TenantDatabaseResolverTag } from '@entifix/business';
|
|
2
|
+
import { Internal } from '@entifix/core';
|
|
3
|
+
import { Effect, Layer } from 'effect';
|
|
4
|
+
import { MongoDatabaseTag } from '../mongo-database/mongo-database.js';
|
|
5
|
+
import { makeMongoFacetCounter } from './make-mongo-facet-counter.js';
|
|
6
|
+
/**
|
|
7
|
+
* Facet counters over the service's one database, whoever the request is for —
|
|
8
|
+
* `MongoRepositoryFactoryLayer`'s counterpart, bound beside it.
|
|
9
|
+
*/ export const MongoFacetCounterFactoryLayer = Layer.effect(EntityFacetCounterFactoryTag, Effect.gen(function*() {
|
|
10
|
+
const db = yield* MongoDatabaseTag;
|
|
11
|
+
return {
|
|
12
|
+
for: (entityConstructor)=>Effect.succeed(makeMongoFacetCounter(db, entityConstructor))
|
|
13
|
+
};
|
|
14
|
+
}));
|
|
15
|
+
/**
|
|
16
|
+
* Facet counters over the requesting organization's own database, resolved
|
|
17
|
+
* per request through the host's {@link TenantDatabaseResolverTag}.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ A request that names no organization is **refused**, never counted from a
|
|
20
|
+
* default: on a per-organization store a default database is some other
|
|
21
|
+
* organization's, and its counts would say how much stock a competitor holds.
|
|
22
|
+
*/ export const MongoTenantFacetCounterFactoryLayer = Layer.effect(EntityFacetCounterFactoryTag, Effect.gen(function*() {
|
|
23
|
+
const resolver = yield* TenantDatabaseResolverTag;
|
|
24
|
+
return {
|
|
25
|
+
for: (entityConstructor, scope)=>scope.organizationId === undefined ? Effect.fail(new Internal()) : Effect.map(resolver.forOrganization(scope.organizationId), (db)=>makeMongoFacetCounter(db, entityConstructor))
|
|
26
|
+
};
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=mongo-facet-counter-factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/mongo-facet-counter-factory.ts"],"sourcesContent":["import {\n EntityFacetCounterFactoryTag,\n type TenantDatabaseResolver,\n TenantDatabaseResolverTag,\n} from '@entifix/business';\nimport { Internal } from '@entifix/core';\nimport { Effect, Layer } from 'effect';\nimport type { Db } from 'mongodb';\n\nimport { MongoDatabaseTag } from '../mongo-database/mongo-database.js';\nimport { makeMongoFacetCounter } from './make-mongo-facet-counter.js';\n\n/**\n * Facet counters over the service's one database, whoever the request is for —\n * `MongoRepositoryFactoryLayer`'s counterpart, bound beside it.\n */\nexport const MongoFacetCounterFactoryLayer = Layer.effect(\n EntityFacetCounterFactoryTag,\n Effect.gen(function* () {\n const db = yield* MongoDatabaseTag;\n return {\n for: entityConstructor =>\n Effect.succeed(makeMongoFacetCounter(db, entityConstructor)),\n };\n }),\n);\n\n/**\n * Facet counters over the requesting organization's own database, resolved\n * per request through the host's {@link TenantDatabaseResolverTag}.\n *\n * ⚠️ A request that names no organization is **refused**, never counted from a\n * default: on a per-organization store a default database is some other\n * organization's, and its counts would say how much stock a competitor holds.\n */\nexport const MongoTenantFacetCounterFactoryLayer = Layer.effect(\n EntityFacetCounterFactoryTag,\n Effect.gen(function* () {\n const resolver =\n (yield* TenantDatabaseResolverTag) as TenantDatabaseResolver<Db>;\n return {\n for: (entityConstructor, scope) =>\n scope.organizationId === undefined\n ? Effect.fail(new Internal())\n : Effect.map(resolver.forOrganization(scope.organizationId), db =>\n makeMongoFacetCounter(db, entityConstructor),\n ),\n };\n }),\n);\n"],"names":["EntityFacetCounterFactoryTag","TenantDatabaseResolverTag","Internal","Effect","Layer","MongoDatabaseTag","makeMongoFacetCounter","MongoFacetCounterFactoryLayer","effect","gen","db","for","entityConstructor","succeed","MongoTenantFacetCounterFactoryLayer","resolver","scope","organizationId","undefined","fail","map","forOrganization"],"mappings":"AAAA,SACEA,4BAA4B,EAE5BC,yBAAyB,QACpB,oBAAoB;AAC3B,SAASC,QAAQ,QAAQ,gBAAgB;AACzC,SAASC,MAAM,EAAEC,KAAK,QAAQ,SAAS;AAGvC,SAASC,gBAAgB,QAAQ,sCAAsC;AACvE,SAASC,qBAAqB,QAAQ,gCAAgC;AAEtE;;;CAGC,GACD,OAAO,MAAMC,gCAAgCH,MAAMI,MAAM,CACvDR,8BACAG,OAAOM,GAAG,CAAC;IACT,MAAMC,KAAK,OAAOL;IAClB,OAAO;QACLM,KAAKC,CAAAA,oBACHT,OAAOU,OAAO,CAACP,sBAAsBI,IAAIE;IAC7C;AACF,IACA;AAEF;;;;;;;CAOC,GACD,OAAO,MAAME,sCAAsCV,MAAMI,MAAM,CAC7DR,8BACAG,OAAOM,GAAG,CAAC;IACT,MAAMM,WACH,OAAOd;IACV,OAAO;QACLU,KAAK,CAACC,mBAAmBI,QACvBA,MAAMC,cAAc,KAAKC,YACrBf,OAAOgB,IAAI,CAAC,IAAIjB,cAChBC,OAAOiB,GAAG,CAACL,SAASM,eAAe,CAACL,MAAMC,cAAc,GAAGP,CAAAA,KACzDJ,sBAAsBI,IAAIE;IAEpC;AACF,IACA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mongo-repository-factory.d.ts","sourceRoot":"","sources":["../../src/adapters/mongo-repository-factory.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAE1B,yBAAyB,EAC1B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AAGvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAGvE;;GAEG;AACH,eAAO,MAAM,2BAA2B,kEASvC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,iCAAiC,
|
|
1
|
+
{"version":3,"file":"mongo-repository-factory.d.ts","sourceRoot":"","sources":["../../src/adapters/mongo-repository-factory.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAE1B,yBAAyB,EAC1B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AAGvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAGvE;;GAEG;AACH,eAAO,MAAM,2BAA2B,kEASvC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,iCAAiC,2EAc7C,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EntityRepositoryFactoryTag, TenantDatabaseResolverTag } from '@entifix/business';
|
|
2
|
-
import {
|
|
2
|
+
import { Internal } from '@entifix/core';
|
|
3
3
|
import { Effect, Layer } from 'effect';
|
|
4
4
|
import { MongoDatabaseTag } from '../mongo-database/mongo-database.js';
|
|
5
5
|
import { makeMongoRepository } from './make-mongo-repository.js';
|
|
@@ -21,9 +21,7 @@ import { makeMongoRepository } from './make-mongo-repository.js';
|
|
|
21
21
|
*/ export const MongoTenantRepositoryFactoryLayer = Layer.effect(EntityRepositoryFactoryTag, Effect.gen(function*() {
|
|
22
22
|
const resolver = yield* TenantDatabaseResolverTag;
|
|
23
23
|
return {
|
|
24
|
-
for: (entityConstructor, scope)=>scope.organizationId === undefined ? Effect.fail(new
|
|
25
|
-
entity: entityConstructor.name
|
|
26
|
-
})) : Effect.map(resolver.forOrganization(scope.organizationId), (db)=>makeMongoRepository(db, entityConstructor))
|
|
24
|
+
for: (entityConstructor, scope)=>scope.organizationId === undefined ? Effect.fail(new Internal()) : Effect.map(resolver.forOrganization(scope.organizationId), (db)=>makeMongoRepository(db, entityConstructor))
|
|
27
25
|
};
|
|
28
26
|
}));
|
|
29
27
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adapters/mongo-repository-factory.ts"],"sourcesContent":["import {\n EntityRepositoryFactoryTag,\n type TenantDatabaseResolver,\n TenantDatabaseResolverTag,\n} from '@entifix/business';\nimport {
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/mongo-repository-factory.ts"],"sourcesContent":["import {\n EntityRepositoryFactoryTag,\n type TenantDatabaseResolver,\n TenantDatabaseResolverTag,\n} from '@entifix/business';\nimport { Internal } from '@entifix/core';\nimport { Effect, Layer } from 'effect';\nimport type { Db } from 'mongodb';\n\nimport { MongoDatabaseTag } from '../mongo-database/mongo-database.js';\nimport { makeMongoRepository } from './make-mongo-repository.js';\n\n/**\n * Repositories over the service's one database, whoever the request is for.\n */\nexport const MongoRepositoryFactoryLayer = Layer.effect(\n EntityRepositoryFactoryTag,\n Effect.gen(function* () {\n const db = yield* MongoDatabaseTag;\n return {\n for: entityConstructor =>\n Effect.succeed(makeMongoRepository(db, entityConstructor)),\n };\n }),\n);\n\n/**\n * Repositories over the requesting organization's own database, resolved per\n * request through the host's {@link TenantDatabaseResolverTag}.\n *\n * ⚠️ A request that names no organization is **refused**, never served from a\n * default: on a per-organization store a default database is some other\n * organization's.\n */\nexport const MongoTenantRepositoryFactoryLayer = Layer.effect(\n EntityRepositoryFactoryTag,\n Effect.gen(function* () {\n const resolver =\n (yield* TenantDatabaseResolverTag) as TenantDatabaseResolver<Db>;\n return {\n for: (entityConstructor, scope) =>\n scope.organizationId === undefined\n ? Effect.fail(new Internal())\n : Effect.map(resolver.forOrganization(scope.organizationId), db =>\n makeMongoRepository(db, entityConstructor),\n ),\n };\n }),\n);\n"],"names":["EntityRepositoryFactoryTag","TenantDatabaseResolverTag","Internal","Effect","Layer","MongoDatabaseTag","makeMongoRepository","MongoRepositoryFactoryLayer","effect","gen","db","for","entityConstructor","succeed","MongoTenantRepositoryFactoryLayer","resolver","scope","organizationId","undefined","fail","map","forOrganization"],"mappings":"AAAA,SACEA,0BAA0B,EAE1BC,yBAAyB,QACpB,oBAAoB;AAC3B,SAASC,QAAQ,QAAQ,gBAAgB;AACzC,SAASC,MAAM,EAAEC,KAAK,QAAQ,SAAS;AAGvC,SAASC,gBAAgB,QAAQ,sCAAsC;AACvE,SAASC,mBAAmB,QAAQ,6BAA6B;AAEjE;;CAEC,GACD,OAAO,MAAMC,8BAA8BH,MAAMI,MAAM,CACrDR,4BACAG,OAAOM,GAAG,CAAC;IACT,MAAMC,KAAK,OAAOL;IAClB,OAAO;QACLM,KAAKC,CAAAA,oBACHT,OAAOU,OAAO,CAACP,oBAAoBI,IAAIE;IAC3C;AACF,IACA;AAEF;;;;;;;CAOC,GACD,OAAO,MAAME,oCAAoCV,MAAMI,MAAM,CAC3DR,4BACAG,OAAOM,GAAG,CAAC;IACT,MAAMM,WACH,OAAOd;IACV,OAAO;QACLU,KAAK,CAACC,mBAAmBI,QACvBA,MAAMC,cAAc,KAAKC,YACrBf,OAAOgB,IAAI,CAAC,IAAIjB,cAChBC,OAAOiB,GAAG,CAACL,SAASM,eAAe,CAACL,MAAMC,cAAc,GAAGP,CAAAA,KACzDJ,oBAAoBI,IAAIE;IAElC;AACF,IACA"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type StoreFailure, type StoreFault } from '@entifix/core';
|
|
2
|
+
/**
|
|
3
|
+
* What a Mongo driver error means, as a categorised failure
|
|
4
|
+
* ([ADR 0013](../../../../../docs/adr/0013-failures-are-typed-values-decided-at-the-shell.md)).
|
|
5
|
+
*
|
|
6
|
+
* Read by **name, code and label**, never by class: the driver's error classes
|
|
7
|
+
* are not what a consumer can match on, and a second copy of the driver in one
|
|
8
|
+
* process would answer `instanceof` with `false`.
|
|
9
|
+
*/
|
|
10
|
+
export interface MongoErrorContext {
|
|
11
|
+
/** What the failure names as its dependency; `mongo` when absent. */
|
|
12
|
+
readonly dependency?: string;
|
|
13
|
+
/**
|
|
14
|
+
* What a duplicate key means here. On a declared uniqueness it is a
|
|
15
|
+
* `conflict` — the caller asked for something that already exists. On a
|
|
16
|
+
* claim or a race (an inbox, a command claim, a saga claim) it is
|
|
17
|
+
* `contention` — someone else got there first, and retrying wins or replays.
|
|
18
|
+
*/
|
|
19
|
+
readonly duplicate?: 'conflict' | 'contention';
|
|
20
|
+
}
|
|
21
|
+
/** Whether a server label is on `error` or in its cause chain. */
|
|
22
|
+
export declare const mongoErrorLabelled: (error: unknown, label: string) => boolean;
|
|
23
|
+
export declare const translateMongoError: (error: unknown, context?: MongoErrorContext) => StoreFailure;
|
|
24
|
+
/**
|
|
25
|
+
* A driver error as the store's own fault, for the bookkeeping a mechanism
|
|
26
|
+
* does around a caller's work — a claim, a commit, a remembered answer. A
|
|
27
|
+
* duplicate key there means a race lost, never an answer for the caller.
|
|
28
|
+
*/
|
|
29
|
+
export declare const translateMongoFault: (error: unknown, context?: Omit<MongoErrorContext, "duplicate">) => StoreFault;
|
|
30
|
+
//# sourceMappingURL=translate-mongo-error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translate-mongo-error.d.ts","sourceRoot":"","sources":["../../src/errors/translate-mongo-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,YAAY,EACjB,KAAK,UAAU,EAGhB,MAAM,eAAe,CAAC;AAEvB;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,qEAAqE;IACrE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,UAAU,GAAG,YAAY,CAAC;CAChD;AAqBD,kEAAkE;AAClE,eAAO,MAAM,kBAAkB,GAAI,OAAO,OAAO,EAAE,OAAO,MAAM,KAAG,OAOlE,CAAC;AAwBF,eAAO,MAAM,mBAAmB,GAC9B,OAAO,OAAO,EACd,UAAS,iBAAsB,KAC9B,YA+BF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC9B,OAAO,OAAO,EACd,UAAS,IAAI,CAAC,iBAAiB,EAAE,WAAW,CAAM,KACjD,UAMF,CAAC"}
|