@entifix/mongo 0.1.0-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/LICENSE +21 -0
- package/dist/adapters/filter-translator.d.ts +19 -0
- package/dist/adapters/filter-translator.d.ts.map +1 -0
- package/dist/adapters/filter-translator.js +142 -0
- package/dist/adapters/filter-translator.js.map +1 -0
- package/dist/adapters/make-mongo-link-resolver.d.ts +14 -0
- package/dist/adapters/make-mongo-link-resolver.d.ts.map +1 -0
- package/dist/adapters/make-mongo-link-resolver.js +31 -0
- package/dist/adapters/make-mongo-link-resolver.js.map +1 -0
- package/dist/adapters/make-mongo-repository.d.ts +19 -0
- package/dist/adapters/make-mongo-repository.d.ts.map +1 -0
- package/dist/adapters/make-mongo-repository.js +122 -0
- package/dist/adapters/make-mongo-repository.js.map +1 -0
- package/dist/inbox/store.d.ts +50 -0
- package/dist/inbox/store.d.ts.map +1 -0
- package/dist/inbox/store.js +86 -0
- package/dist/inbox/store.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/mongo-database/mongo-database.d.ts +52 -0
- package/dist/mongo-database/mongo-database.d.ts.map +1 -0
- package/dist/mongo-database/mongo-database.js +52 -0
- package/dist/mongo-database/mongo-database.js.map +1 -0
- package/dist/mongo-database/mongo-health-probe.d.ts +28 -0
- package/dist/mongo-database/mongo-health-probe.d.ts.map +1 -0
- package/dist/mongo-database/mongo-health-probe.js +39 -0
- package/dist/mongo-database/mongo-health-probe.js.map +1 -0
- package/dist/mongo-database/mongo-tenant-resolver.d.ts +16 -0
- package/dist/mongo-database/mongo-tenant-resolver.d.ts.map +1 -0
- package/dist/mongo-database/mongo-tenant-resolver.js +40 -0
- package/dist/mongo-database/mongo-tenant-resolver.js.map +1 -0
- package/dist/outbox/metrics.d.ts +46 -0
- package/dist/outbox/metrics.d.ts.map +1 -0
- package/dist/outbox/metrics.js +63 -0
- package/dist/outbox/metrics.js.map +1 -0
- package/dist/outbox/relay.d.ts +89 -0
- package/dist/outbox/relay.d.ts.map +1 -0
- package/dist/outbox/relay.js +119 -0
- package/dist/outbox/relay.js.map +1 -0
- package/dist/outbox/store.d.ts +84 -0
- package/dist/outbox/store.d.ts.map +1 -0
- package/dist/outbox/store.js +242 -0
- package/dist/outbox/store.js.map +1 -0
- package/dist/transactions/index.d.ts +16 -0
- package/dist/transactions/index.d.ts.map +1 -0
- package/dist/transactions/index.js +16 -0
- package/dist/transactions/index.js.map +1 -0
- package/package.json +82 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 r10c Technologies
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Entity, EntityFiltering, EntitySorting } from '@entifix/core';
|
|
2
|
+
/** A Mongo query document (`{ field: condition, $and: [...], ... }`). */
|
|
3
|
+
export type MongoFilter = Record<string, unknown>;
|
|
4
|
+
/** A Mongo sort document (`{ field: 1 | -1 }`). */
|
|
5
|
+
export type MongoSort = Record<string, 1 | -1>;
|
|
6
|
+
/**
|
|
7
|
+
* Translates the request's `filtering` (an array of {@link EntityFiltering},
|
|
8
|
+
* each a single filter, an array of filters, or a {@link FilterGroup}) into a
|
|
9
|
+
* single Mongo query document. Every top-level entry is combined with `$and`;
|
|
10
|
+
* an empty/absent filtering yields the match-all `{}`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function translateFiltering<TEntity extends Entity>(filtering?: EntityFiltering<TEntity>[]): MongoFilter;
|
|
13
|
+
/**
|
|
14
|
+
* Translates the request's `sorting` (an array of {@link EntitySorting} records,
|
|
15
|
+
* each keyed by numeric priority) into a Mongo sort document, preserving the
|
|
16
|
+
* numeric ordering within each record.
|
|
17
|
+
*/
|
|
18
|
+
export declare function translateSorting<TEntity extends Entity>(sorting?: EntitySorting<TEntity>[]): MongoSort;
|
|
19
|
+
//# sourceMappingURL=filter-translator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-translator.d.ts","sourceRoot":"","sources":["../../src/adapters/filter-translator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EAEN,eAAe,EACf,aAAa,EAEd,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"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const BINARY_OPERATORS = {
|
|
2
|
+
eq: '$eq',
|
|
3
|
+
ne: '$ne',
|
|
4
|
+
gt: '$gt',
|
|
5
|
+
gte: '$gte',
|
|
6
|
+
lt: '$lt',
|
|
7
|
+
lte: '$lte'
|
|
8
|
+
};
|
|
9
|
+
/** Escapes a string so it is matched literally inside a Mongo `$regex`. */ function escapeRegex(value) {
|
|
10
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
11
|
+
}
|
|
12
|
+
function isFilterGroup(node) {
|
|
13
|
+
// A group carries `values` + a logic operator and, unlike the array filter,
|
|
14
|
+
// has no `property`.
|
|
15
|
+
return !('property' in node) && (node.operator === 'and' || node.operator === 'or');
|
|
16
|
+
}
|
|
17
|
+
function translateNode(node) {
|
|
18
|
+
if (isFilterGroup(node)) {
|
|
19
|
+
const clause = node.operator === 'or' ? '$or' : '$and';
|
|
20
|
+
return {
|
|
21
|
+
[clause]: node.values.map(translateNode)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const field = String(node.property);
|
|
25
|
+
switch(node.operator){
|
|
26
|
+
case 'eq':
|
|
27
|
+
case 'ne':
|
|
28
|
+
case 'gt':
|
|
29
|
+
case 'gte':
|
|
30
|
+
case 'lt':
|
|
31
|
+
case 'lte':
|
|
32
|
+
return {
|
|
33
|
+
[field]: {
|
|
34
|
+
[BINARY_OPERATORS[node.operator]]: node.value
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
case 'in':
|
|
38
|
+
return {
|
|
39
|
+
[field]: {
|
|
40
|
+
$in: node.values
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
case 'nin':
|
|
44
|
+
return {
|
|
45
|
+
[field]: {
|
|
46
|
+
$nin: node.values
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
case 'between':
|
|
50
|
+
return {
|
|
51
|
+
[field]: {
|
|
52
|
+
$gte: node.start,
|
|
53
|
+
$lte: node.end
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
case 'nbetween':
|
|
57
|
+
return {
|
|
58
|
+
[field]: {
|
|
59
|
+
$not: {
|
|
60
|
+
$gte: node.start,
|
|
61
|
+
$lte: node.end
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
case 'like':
|
|
66
|
+
return {
|
|
67
|
+
[field]: {
|
|
68
|
+
$regex: escapeRegex(node.value),
|
|
69
|
+
$options: 'i'
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
case 'nlike':
|
|
73
|
+
return {
|
|
74
|
+
[field]: {
|
|
75
|
+
$not: {
|
|
76
|
+
$regex: escapeRegex(node.value),
|
|
77
|
+
$options: 'i'
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
case 'isNull':
|
|
82
|
+
return {
|
|
83
|
+
[field]: {
|
|
84
|
+
$eq: null
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
case 'isNotNull':
|
|
88
|
+
return {
|
|
89
|
+
[field]: {
|
|
90
|
+
$ne: null
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
default:
|
|
94
|
+
{
|
|
95
|
+
// Exhaustiveness guard — a new operator must be handled explicitly.
|
|
96
|
+
const _never = node;
|
|
97
|
+
return _never;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Translates the request's `filtering` (an array of {@link EntityFiltering},
|
|
103
|
+
* each a single filter, an array of filters, or a {@link FilterGroup}) into a
|
|
104
|
+
* single Mongo query document. Every top-level entry is combined with `$and`;
|
|
105
|
+
* an empty/absent filtering yields the match-all `{}`.
|
|
106
|
+
*/ export function translateFiltering(filtering) {
|
|
107
|
+
if (!filtering || filtering.length === 0) {
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
const clauses = [];
|
|
111
|
+
for (const entry of filtering){
|
|
112
|
+
if (Array.isArray(entry)) {
|
|
113
|
+
clauses.push(...entry.map(translateNode));
|
|
114
|
+
} else {
|
|
115
|
+
clauses.push(translateNode(entry));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (clauses.length === 0) return {};
|
|
119
|
+
if (clauses.length === 1) return clauses[0];
|
|
120
|
+
return {
|
|
121
|
+
$and: clauses
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Translates the request's `sorting` (an array of {@link EntitySorting} records,
|
|
126
|
+
* each keyed by numeric priority) into a Mongo sort document, preserving the
|
|
127
|
+
* numeric ordering within each record.
|
|
128
|
+
*/ export function translateSorting(sorting) {
|
|
129
|
+
const sort = {};
|
|
130
|
+
if (!sorting) return sort;
|
|
131
|
+
for (const record of sorting){
|
|
132
|
+
for (const priority of Object.keys(record).map(Number).sort((a, b)=>a - b)){
|
|
133
|
+
const entry = record[priority];
|
|
134
|
+
if (entry) {
|
|
135
|
+
sort[String(entry.property)] = entry.type === 'desc' ? -1 : 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return sort;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
//# sourceMappingURL=filter-translator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/filter-translator.ts"],"sourcesContent":["import type {\n Entity,\n EntityFilter,\n EntityFiltering,\n EntitySorting,\n FilterGroup,\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\n/** Escapes a string so it is matched literally inside a Mongo `$regex`. */\nfunction escapeRegex(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\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 return { [field]: { $regex: escapeRegex(node.value), $options: 'i' } };\n case 'nlike':\n return {\n [field]: { $not: { $regex: escapeRegex(node.value), $options: 'i' } },\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":["BINARY_OPERATORS","eq","ne","gt","gte","lt","lte","escapeRegex","value","replace","isFilterGroup","node","operator","translateNode","clause","values","map","field","String","property","$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":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { EntityLinkResolverTag } from '@entifix/business';
|
|
2
|
+
import { Entity, EntityConstructor } from '@entifix/core';
|
|
3
|
+
import { Context } from 'effect';
|
|
4
|
+
import type { Db } from 'mongodb';
|
|
5
|
+
/**
|
|
6
|
+
* Builds an {@link EntityLinkResolverTag} context backed by Mongo repositories,
|
|
7
|
+
* so a load use-case can materialize not-yet-loaded {@link EntityLink}s on the
|
|
8
|
+
* backend. Mirrors the REST `createEntityLinkResolver`, but each Mongo `get`
|
|
9
|
+
* already has no outstanding requirements, so nothing needs to be provided.
|
|
10
|
+
*
|
|
11
|
+
* `registrations` lists the link-target constructors this resolver can fetch.
|
|
12
|
+
*/
|
|
13
|
+
export declare function makeMongoLinkResolver(db: Db, registrations: ReadonlyArray<EntityConstructor<Entity>>): Context.Context<EntityLinkResolverTag>;
|
|
14
|
+
//# sourceMappingURL=make-mongo-link-resolver.d.ts.map
|
|
@@ -0,0 +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,EAGL,MAAM,EACN,iBAAiB,EAElB,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,CAiCxC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { EntityLinkResolverTag } from '@entifix/business';
|
|
2
|
+
import { EntifixLogicError } from '@entifix/core';
|
|
3
|
+
import { Context, Effect } from 'effect';
|
|
4
|
+
import { makeMongoRepository } from './make-mongo-repository.js';
|
|
5
|
+
/**
|
|
6
|
+
* Builds an {@link EntityLinkResolverTag} context backed by Mongo repositories,
|
|
7
|
+
* so a load use-case can materialize not-yet-loaded {@link EntityLink}s on the
|
|
8
|
+
* backend. Mirrors the REST `createEntityLinkResolver`, but each Mongo `get`
|
|
9
|
+
* already has no outstanding requirements, so nothing needs to be provided.
|
|
10
|
+
*
|
|
11
|
+
* `registrations` lists the link-target constructors this resolver can fetch.
|
|
12
|
+
*/ export function makeMongoLinkResolver(db, registrations) {
|
|
13
|
+
const repositoryByConstructor = new Map(registrations.map((entityConstructor)=>[
|
|
14
|
+
entityConstructor,
|
|
15
|
+
makeMongoRepository(db, entityConstructor)
|
|
16
|
+
]));
|
|
17
|
+
const resolver = {
|
|
18
|
+
resolve (entityConstructor, id) {
|
|
19
|
+
const repository = repositoryByConstructor.get(entityConstructor);
|
|
20
|
+
if (!repository) {
|
|
21
|
+
return Effect.fail(new EntifixLogicError(`No Mongo link resolver registered for entity "${entityConstructor.name}"`, undefined, {
|
|
22
|
+
entity: entityConstructor.name
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
return repository.get(id);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
return Context.make(EntityLinkResolverTag, resolver);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=make-mongo-link-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/make-mongo-link-resolver.ts"],"sourcesContent":["import { EntityLinkResolverTag } from '@entifix/business';\nimport {\n EntifixError,\n EntifixLogicError,\n Entity,\n EntityConstructor,\n EntityLinkResolver,\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(\n new EntifixLogicError(\n `No Mongo link resolver registered for entity \"${entityConstructor.name}\"`,\n undefined,\n { entity: entityConstructor.name },\n ),\n );\n }\n return repository.get(id) as unknown as Effect.Effect<\n never,\n EntifixError\n >;\n },\n };\n\n return Context.make(EntityLinkResolverTag, resolver);\n}\n"],"names":["EntityLinkResolverTag","EntifixLogicError","Context","Effect","makeMongoRepository","makeMongoLinkResolver","db","registrations","repositoryByConstructor","Map","map","entityConstructor","resolver","resolve","id","repository","get","fail","name","undefined","entity","make"],"mappings":"AAAA,SAASA,qBAAqB,QAAQ,oBAAoB;AAC1D,SAEEC,iBAAiB,QAIZ,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,CAChB,IAAIhB,kBACF,CAAC,8CAA8C,EAAEU,kBAAkBO,IAAI,CAAC,CAAC,CAAC,EAC1EC,WACA;oBAAEC,QAAQT,kBAAkBO,IAAI;gBAAC;YAGvC;YACA,OAAOH,WAAWC,GAAG,CAACF;QAIxB;IACF;IAEA,OAAOZ,QAAQmB,IAAI,CAACrB,uBAAuBY;AAC7C"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { EntityRepository } from '@entifix/business';
|
|
2
|
+
import { EntifixError, Entity, EntityConstructor } from '@entifix/core';
|
|
3
|
+
import type { Db } from 'mongodb';
|
|
4
|
+
/**
|
|
5
|
+
* Builds an {@link EntityRepository} for a single entity type, backed by a live
|
|
6
|
+
* mongodb {@link Db}. The `db` and `entityConstructor` are captured in closures,
|
|
7
|
+
* so every method's Effect has no outstanding requirements (`R = never`) and is
|
|
8
|
+
* assignable to the interface's `ConfigurationRepositoryTag` channel.
|
|
9
|
+
*
|
|
10
|
+
* Collection name = the entity's `key` (falling back to its class name). Reads
|
|
11
|
+
* reuse the shared entifix deserializer (rebuilding links from embedded objects
|
|
12
|
+
* or scalar FKs); writes reuse the shared serializer. Mongo failures surface as
|
|
13
|
+
* {@link EntifixConnError}.
|
|
14
|
+
*
|
|
15
|
+
* This is the Mongo mirror of the REST `buildEntityRestAdapter*` builders.
|
|
16
|
+
*/
|
|
17
|
+
export declare function makeMongoRepository<TEntity extends Entity>(db: Db, entityConstructor: EntityConstructor<TEntity>): EntityRepository;
|
|
18
|
+
export type { EntifixError };
|
|
19
|
+
//# sourceMappingURL=make-mongo-repository.d.ts.map
|
|
@@ -0,0 +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,EAIL,YAAY,EACZ,MAAM,EACN,iBAAiB,EAMlB,MAAM,eAAe,CAAC;AAEvB,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAoBlC;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,SAAS,MAAM,EACxD,EAAE,EAAE,EAAE,EACN,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAC5C,gBAAgB,CAoGlB;AAED,YAAY,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { deserializeEntityCollection, deserializeSingleEntity, EntifixConnError, extractMetaEntity, serializeEntity } from '@entifix/core';
|
|
4
|
+
import { Effect } from 'effect';
|
|
5
|
+
import { translateFiltering, translateSorting } from './filter-translator.js';
|
|
6
|
+
/** Projection that drops Mongo's internal `_id` from read results. */ const WITHOUT_MONGO_ID = {
|
|
7
|
+
projection: {
|
|
8
|
+
_id: 0
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
function collectionName(entityConstructor) {
|
|
12
|
+
var _metaEntity_key;
|
|
13
|
+
const metaEntity = extractMetaEntity(entityConstructor);
|
|
14
|
+
return (_metaEntity_key = metaEntity.key) != null ? _metaEntity_key : metaEntity.name;
|
|
15
|
+
}
|
|
16
|
+
function toEntityId(entityOrId) {
|
|
17
|
+
return entityOrId != null && typeof entityOrId === 'object' ? entityOrId.id : entityOrId;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Builds an {@link EntityRepository} for a single entity type, backed by a live
|
|
21
|
+
* mongodb {@link Db}. The `db` and `entityConstructor` are captured in closures,
|
|
22
|
+
* so every method's Effect has no outstanding requirements (`R = never`) and is
|
|
23
|
+
* assignable to the interface's `ConfigurationRepositoryTag` channel.
|
|
24
|
+
*
|
|
25
|
+
* Collection name = the entity's `key` (falling back to its class name). Reads
|
|
26
|
+
* reuse the shared entifix deserializer (rebuilding links from embedded objects
|
|
27
|
+
* or scalar FKs); writes reuse the shared serializer. Mongo failures surface as
|
|
28
|
+
* {@link EntifixConnError}.
|
|
29
|
+
*
|
|
30
|
+
* This is the Mongo mirror of the REST `buildEntityRestAdapter*` builders.
|
|
31
|
+
*/ export function makeMongoRepository(db, entityConstructor) {
|
|
32
|
+
const collection = db.collection(collectionName(entityConstructor));
|
|
33
|
+
const fail = (message, error, details)=>new EntifixConnError(message, error, _extends({
|
|
34
|
+
entity: entityConstructor.name
|
|
35
|
+
}, details));
|
|
36
|
+
const load = (request)=>Effect.gen(function*() {
|
|
37
|
+
var _request_page, _request_pageSize;
|
|
38
|
+
const filter = translateFiltering(request.filtering);
|
|
39
|
+
const sort = translateSorting(request.sorting);
|
|
40
|
+
const page = (_request_page = request.page) != null ? _request_page : 1;
|
|
41
|
+
const pageSize = (_request_pageSize = request.pageSize) != null ? _request_pageSize : 10;
|
|
42
|
+
const skip = (page - 1) * pageSize;
|
|
43
|
+
const docs = yield* Effect.tryPromise({
|
|
44
|
+
try: ()=>collection.find(filter, WITHOUT_MONGO_ID).sort(sort).skip(skip).limit(pageSize).toArray(),
|
|
45
|
+
catch: (error)=>fail('Failed to load entities from MongoDB', error)
|
|
46
|
+
});
|
|
47
|
+
const total = yield* Effect.tryPromise({
|
|
48
|
+
try: ()=>collection.countDocuments(filter),
|
|
49
|
+
catch: (error)=>fail('Failed to count entities in MongoDB', error)
|
|
50
|
+
});
|
|
51
|
+
const items = yield* deserializeEntityCollection(entityConstructor, docs);
|
|
52
|
+
return {
|
|
53
|
+
items: items,
|
|
54
|
+
total,
|
|
55
|
+
request
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
const get = (id)=>Effect.gen(function*() {
|
|
59
|
+
const doc = yield* Effect.tryPromise({
|
|
60
|
+
try: ()=>collection.findOne({
|
|
61
|
+
id
|
|
62
|
+
}, WITHOUT_MONGO_ID),
|
|
63
|
+
catch: (error)=>fail('Failed to read entity from MongoDB', error, {
|
|
64
|
+
id
|
|
65
|
+
})
|
|
66
|
+
});
|
|
67
|
+
const entity = yield* deserializeSingleEntity(entityConstructor, doc);
|
|
68
|
+
if (entity === undefined) {
|
|
69
|
+
return yield* Effect.fail(fail(`Entity not found`, undefined, {
|
|
70
|
+
id
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
return entity;
|
|
74
|
+
});
|
|
75
|
+
const save = (entity)=>Effect.gen(function*() {
|
|
76
|
+
var _entity_id;
|
|
77
|
+
const doc = serializeEntity(entityConstructor, entity);
|
|
78
|
+
// A create arrives without an id, so the store mints one. Entities are
|
|
79
|
+
// addressed by their own `id` field (never Mongo's `_id`, which reads
|
|
80
|
+
// project away), so an inserted document without one would be
|
|
81
|
+
// unreachable by `get` and by any later `replaceOne`.
|
|
82
|
+
const id = (_entity_id = entity.id) != null ? _entity_id : randomUUID();
|
|
83
|
+
yield* Effect.tryPromise({
|
|
84
|
+
// `doc` omits `id` when it was undefined (the serializer drops
|
|
85
|
+
// undefined), hence spreading the resolved id back in.
|
|
86
|
+
try: ()=>collection.replaceOne({
|
|
87
|
+
id
|
|
88
|
+
}, _extends({}, doc, {
|
|
89
|
+
id
|
|
90
|
+
}), {
|
|
91
|
+
upsert: true
|
|
92
|
+
}),
|
|
93
|
+
catch: (error)=>fail('Failed to save entity to MongoDB', error, {
|
|
94
|
+
id
|
|
95
|
+
})
|
|
96
|
+
});
|
|
97
|
+
entity.id = id;
|
|
98
|
+
return entity;
|
|
99
|
+
});
|
|
100
|
+
const del = (entityOrId)=>Effect.gen(function*() {
|
|
101
|
+
const id = toEntityId(entityOrId);
|
|
102
|
+
yield* Effect.tryPromise({
|
|
103
|
+
try: ()=>collection.deleteOne({
|
|
104
|
+
id
|
|
105
|
+
}),
|
|
106
|
+
catch: (error)=>fail('Failed to delete entity from MongoDB', error, {
|
|
107
|
+
id
|
|
108
|
+
})
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
// The interface types each method's requirement channel as
|
|
112
|
+
// `ConfigurationRepositoryTag`; these effects require nothing (`never`), which
|
|
113
|
+
// is assignable. Cast through the interface to keep the generic signatures.
|
|
114
|
+
return {
|
|
115
|
+
load,
|
|
116
|
+
get,
|
|
117
|
+
save,
|
|
118
|
+
delete: del
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
//# sourceMappingURL=make-mongo-repository.js.map
|
|
@@ -0,0 +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 EntifixConnError,\n EntifixError,\n Entity,\n EntityConstructor,\n EntityId,\n EntityLoadRequest,\n EntityPage,\n extractMetaEntity,\n serializeEntity,\n} from '@entifix/core';\nimport { Effect } from 'effect';\nimport type { Db } from 'mongodb';\n\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\nfunction collectionName<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`) and is\n * assignable to the interface's `ConfigurationRepositoryTag` channel.\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 * {@link EntifixConnError}.\n *\n * This is the Mongo mirror of the REST `buildEntityRestAdapter*` builders.\n */\nexport function makeMongoRepository<TEntity extends Entity>(\n db: Db,\n entityConstructor: EntityConstructor<TEntity>,\n): EntityRepository {\n const collection = db.collection(collectionName(entityConstructor));\n\n const fail = (\n message: string,\n error: unknown,\n details?: Record<string, unknown>,\n ) =>\n new EntifixConnError(message, error, {\n entity: entityConstructor.name,\n ...details,\n });\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)\n .sort(sort)\n .skip(skip)\n .limit(pageSize)\n .toArray(),\n catch: error => fail('Failed to load entities from MongoDB', error),\n });\n const total = yield* Effect.tryPromise({\n try: () => collection.countDocuments(filter),\n catch: error => fail('Failed to count entities in MongoDB', 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: () => collection.findOne({ id }, WITHOUT_MONGO_ID),\n catch: error =>\n fail('Failed to read entity from MongoDB', error, { id }),\n });\n const entity = yield* deserializeSingleEntity(entityConstructor, doc);\n if (entity === undefined) {\n return yield* Effect.fail(fail(`Entity not found`, undefined, { id }));\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({ id }, { ...doc, id }, { upsert: true }),\n catch: error => fail('Failed to save entity to MongoDB', error, { id }),\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 }),\n catch: error =>\n fail('Failed to delete entity from MongoDB', error, { id }),\n });\n });\n\n // The interface types each method's requirement channel as\n // `ConfigurationRepositoryTag`; these effects require nothing (`never`), which\n // is assignable. Cast through the interface to keep the generic signatures.\n return {\n load,\n get,\n save,\n delete: del,\n } as unknown as EntityRepository;\n}\n\nexport type { EntifixError };\n"],"names":["randomUUID","deserializeEntityCollection","deserializeSingleEntity","EntifixConnError","extractMetaEntity","serializeEntity","Effect","translateFiltering","translateSorting","WITHOUT_MONGO_ID","projection","_id","collectionName","entityConstructor","metaEntity","key","name","toEntityId","entityOrId","id","makeMongoRepository","db","collection","fail","message","error","details","entity","load","request","gen","filter","filtering","sort","sorting","page","pageSize","skip","docs","tryPromise","try","find","limit","toArray","catch","total","countDocuments","items","get","doc","findOne","undefined","save","replaceOne","upsert","del","deleteOne","delete"],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { EntifixConnError } from '@entifix/core';
|
|
2
|
+
import type { InboxClaim, TransactionInbox } from '@entifix/transactions';
|
|
3
|
+
import { Effect } from 'effect';
|
|
4
|
+
import type { Db } from 'mongodb';
|
|
5
|
+
/**
|
|
6
|
+
* The inbox collection, and it lives in **whichever database the side effect it
|
|
7
|
+
* guards writes to** — not in one central place.
|
|
8
|
+
*
|
|
9
|
+
* That is the whole mechanism, not a deployment preference: a claim is only
|
|
10
|
+
* worth anything if it commits in the same storage transaction as the effect,
|
|
11
|
+
* and two databases cannot be one transaction. So the saga tracker's inbox is in
|
|
12
|
+
* the `saga` database because its fold writes there, exactly as the outbox is
|
|
13
|
+
* per-tenant because the entity is.
|
|
14
|
+
*/
|
|
15
|
+
export declare const INBOX_COLLECTION = "transaction_inbox";
|
|
16
|
+
export declare const isDuplicateKey: (error: unknown) => boolean;
|
|
17
|
+
/**
|
|
18
|
+
* The stored shape of a claim.
|
|
19
|
+
*
|
|
20
|
+
* Exported for the same reason `outboxDocument` is: the claim that matters is
|
|
21
|
+
* **not** written through the port. It has to join the side effect's session to
|
|
22
|
+
* be atomic with it, and a session cannot live in a framework-free contract, so
|
|
23
|
+
* the consumer inserts this document itself. {@link makeMongoInbox} is for a
|
|
24
|
+
* consumer that can claim standalone.
|
|
25
|
+
*/
|
|
26
|
+
export declare const inboxDocument: (consumer: string, eventId: string) => InboxClaim;
|
|
27
|
+
/**
|
|
28
|
+
* The unique index the claim rides, on **`(consumer, eventId)`**.
|
|
29
|
+
*
|
|
30
|
+
* Compound, and that is the half easy to get wrong. Two consumers legitimately
|
|
31
|
+
* process the same event — the tracker's fold and the SSE hub both bind
|
|
32
|
+
* `transaction.*` — so a unique index on `eventId` alone would let whichever
|
|
33
|
+
* claimed first starve every other consumer of every message, while the broker,
|
|
34
|
+
* the queue and the handler all went on reporting success.
|
|
35
|
+
*
|
|
36
|
+
* Called per handle rather than once at boot, matching `ensureOutboxIndexes`:
|
|
37
|
+
* the index must exist before the first claim, not eventually, or two
|
|
38
|
+
* concurrent first deliveries both insert.
|
|
39
|
+
*/
|
|
40
|
+
export declare const ensureInboxIndexes: (db: Db) => Effect.Effect<void, EntifixConnError, never>;
|
|
41
|
+
/**
|
|
42
|
+
* A {@link TransactionInbox} over one database, for one consumer.
|
|
43
|
+
*
|
|
44
|
+
* `consumer` is the subscription's durable work-queue name
|
|
45
|
+
* (`<slice>.<pattern>`) — deterministic, and stable across restarts, which an
|
|
46
|
+
* anonymous broadcast queue's server-generated name is not. That is also why
|
|
47
|
+
* `dedupe: 'inbox'` is only legal on a `work` subscription.
|
|
48
|
+
*/
|
|
49
|
+
export declare const makeMongoInbox: (db: Db, consumer: string) => TransactionInbox;
|
|
50
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/inbox/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC1E,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAElC;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,sBAAsB,CAAC;AAKpD,eAAO,MAAM,cAAc,GAAI,OAAO,OAAO,KAAG,OAGM,CAAC;AAEvD;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GACxB,UAAU,MAAM,EAChB,SAAS,MAAM,KACd,UAID,CAAC;AAEH;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,GAAI,IAAI,EAAE,iDAWrC,CAAC;AAEL;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,IAAI,EAAE,EAAE,UAAU,MAAM,KAAG,gBA2BzD,CAAC"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { EntifixConnError } from '@entifix/core';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* The inbox collection, and it lives in **whichever database the side effect it
|
|
5
|
+
* guards writes to** — not in one central place.
|
|
6
|
+
*
|
|
7
|
+
* That is the whole mechanism, not a deployment preference: a claim is only
|
|
8
|
+
* worth anything if it commits in the same storage transaction as the effect,
|
|
9
|
+
* and two databases cannot be one transaction. So the saga tracker's inbox is in
|
|
10
|
+
* the `saga` database because its fold writes there, exactly as the outbox is
|
|
11
|
+
* per-tenant because the entity is.
|
|
12
|
+
*/ export const INBOX_COLLECTION = 'transaction_inbox';
|
|
13
|
+
/** Mongo's duplicate-key error, which is how a redelivery is recognised. */ const DUPLICATE_KEY = 11000;
|
|
14
|
+
export const isDuplicateKey = (error)=>typeof error === 'object' && error !== null && error.code === DUPLICATE_KEY;
|
|
15
|
+
/**
|
|
16
|
+
* The stored shape of a claim.
|
|
17
|
+
*
|
|
18
|
+
* Exported for the same reason `outboxDocument` is: the claim that matters is
|
|
19
|
+
* **not** written through the port. It has to join the side effect's session to
|
|
20
|
+
* be atomic with it, and a session cannot live in a framework-free contract, so
|
|
21
|
+
* the consumer inserts this document itself. {@link makeMongoInbox} is for a
|
|
22
|
+
* consumer that can claim standalone.
|
|
23
|
+
*/ export const inboxDocument = (consumer, eventId)=>({
|
|
24
|
+
consumer,
|
|
25
|
+
eventId,
|
|
26
|
+
claimedAt: new Date().toISOString()
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* The unique index the claim rides, on **`(consumer, eventId)`**.
|
|
30
|
+
*
|
|
31
|
+
* Compound, and that is the half easy to get wrong. Two consumers legitimately
|
|
32
|
+
* process the same event — the tracker's fold and the SSE hub both bind
|
|
33
|
+
* `transaction.*` — so a unique index on `eventId` alone would let whichever
|
|
34
|
+
* claimed first starve every other consumer of every message, while the broker,
|
|
35
|
+
* the queue and the handler all went on reporting success.
|
|
36
|
+
*
|
|
37
|
+
* Called per handle rather than once at boot, matching `ensureOutboxIndexes`:
|
|
38
|
+
* the index must exist before the first claim, not eventually, or two
|
|
39
|
+
* concurrent first deliveries both insert.
|
|
40
|
+
*/ export const ensureInboxIndexes = (db)=>Effect.tryPromise({
|
|
41
|
+
try: async ()=>{
|
|
42
|
+
await db.collection(INBOX_COLLECTION).createIndex({
|
|
43
|
+
consumer: 1,
|
|
44
|
+
eventId: 1
|
|
45
|
+
}, {
|
|
46
|
+
unique: true
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
catch: (error)=>new EntifixConnError('Failed to ensure inbox indexes', error, {
|
|
50
|
+
database: db.databaseName
|
|
51
|
+
})
|
|
52
|
+
});
|
|
53
|
+
/**
|
|
54
|
+
* A {@link TransactionInbox} over one database, for one consumer.
|
|
55
|
+
*
|
|
56
|
+
* `consumer` is the subscription's durable work-queue name
|
|
57
|
+
* (`<slice>.<pattern>`) — deterministic, and stable across restarts, which an
|
|
58
|
+
* anonymous broadcast queue's server-generated name is not. That is also why
|
|
59
|
+
* `dedupe: 'inbox'` is only legal on a `work` subscription.
|
|
60
|
+
*/ export const makeMongoInbox = (db, consumer)=>{
|
|
61
|
+
const collection = db.collection(INBOX_COLLECTION);
|
|
62
|
+
return {
|
|
63
|
+
claim: (eventId)=>Effect.tryPromise({
|
|
64
|
+
try: async ()=>{
|
|
65
|
+
try {
|
|
66
|
+
await collection.insertOne(inboxDocument(consumer, eventId));
|
|
67
|
+
return 'claimed';
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// Not a failure: the unique index rejected a second claim for this
|
|
70
|
+
// consumer and message, which is precisely how a redelivery is
|
|
71
|
+
// identified.
|
|
72
|
+
if (isDuplicateKey(error)) {
|
|
73
|
+
return 'duplicate';
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
catch: (error)=>new EntifixConnError('Failed to claim inbox entry', error, {
|
|
79
|
+
consumer,
|
|
80
|
+
eventId
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/inbox/store.ts"],"sourcesContent":["import { EntifixConnError } from '@entifix/core';\nimport type { InboxClaim, TransactionInbox } from '@entifix/transactions';\nimport { Effect } from 'effect';\nimport type { Db } from 'mongodb';\n\n/**\n * The inbox collection, and it lives in **whichever database the side effect it\n * guards writes to** — not in one central place.\n *\n * That is the whole mechanism, not a deployment preference: a claim is only\n * worth anything if it commits in the same storage transaction as the effect,\n * and two databases cannot be one transaction. So the saga tracker's inbox is in\n * the `saga` database because its fold writes there, exactly as the outbox is\n * per-tenant because the entity is.\n */\nexport const INBOX_COLLECTION = 'transaction_inbox';\n\n/** Mongo's duplicate-key error, which is how a redelivery is recognised. */\nconst DUPLICATE_KEY = 11000;\n\nexport const isDuplicateKey = (error: unknown): boolean =>\n typeof error === 'object' &&\n error !== null &&\n (error as { code?: unknown }).code === DUPLICATE_KEY;\n\n/**\n * The stored shape of a claim.\n *\n * Exported for the same reason `outboxDocument` is: the claim that matters is\n * **not** written through the port. It has to join the side effect's session to\n * be atomic with it, and a session cannot live in a framework-free contract, so\n * the consumer inserts this document itself. {@link makeMongoInbox} is for a\n * consumer that can claim standalone.\n */\nexport const inboxDocument = (\n consumer: string,\n eventId: string,\n): InboxClaim => ({\n consumer,\n eventId,\n claimedAt: new Date().toISOString(),\n});\n\n/**\n * The unique index the claim rides, on **`(consumer, eventId)`**.\n *\n * Compound, and that is the half easy to get wrong. Two consumers legitimately\n * process the same event — the tracker's fold and the SSE hub both bind\n * `transaction.*` — so a unique index on `eventId` alone would let whichever\n * claimed first starve every other consumer of every message, while the broker,\n * the queue and the handler all went on reporting success.\n *\n * Called per handle rather than once at boot, matching `ensureOutboxIndexes`:\n * the index must exist before the first claim, not eventually, or two\n * concurrent first deliveries both insert.\n */\nexport const ensureInboxIndexes = (db: Db) =>\n Effect.tryPromise({\n try: async () => {\n await db\n .collection<InboxClaim>(INBOX_COLLECTION)\n .createIndex({ consumer: 1, eventId: 1 }, { unique: true });\n },\n catch: error =>\n new EntifixConnError('Failed to ensure inbox indexes', error, {\n database: db.databaseName,\n }),\n });\n\n/**\n * A {@link TransactionInbox} over one database, for one consumer.\n *\n * `consumer` is the subscription's durable work-queue name\n * (`<slice>.<pattern>`) — deterministic, and stable across restarts, which an\n * anonymous broadcast queue's server-generated name is not. That is also why\n * `dedupe: 'inbox'` is only legal on a `work` subscription.\n */\nexport const makeMongoInbox = (db: Db, consumer: string): TransactionInbox => {\n const collection = db.collection<InboxClaim>(INBOX_COLLECTION);\n\n return {\n claim: eventId =>\n Effect.tryPromise({\n try: async () => {\n try {\n await collection.insertOne(inboxDocument(consumer, eventId));\n return 'claimed' as const;\n } catch (error) {\n // Not a failure: the unique index rejected a second claim for this\n // consumer and message, which is precisely how a redelivery is\n // identified.\n if (isDuplicateKey(error)) {\n return 'duplicate' as const;\n }\n throw error;\n }\n },\n catch: error =>\n new EntifixConnError('Failed to claim inbox entry', error, {\n consumer,\n eventId,\n }),\n }),\n };\n};\n"],"names":["EntifixConnError","Effect","INBOX_COLLECTION","DUPLICATE_KEY","isDuplicateKey","error","code","inboxDocument","consumer","eventId","claimedAt","Date","toISOString","ensureInboxIndexes","db","tryPromise","try","collection","createIndex","unique","catch","database","databaseName","makeMongoInbox","claim","insertOne"],"mappings":"AAAA,SAASA,gBAAgB,QAAQ,gBAAgB;AAEjD,SAASC,MAAM,QAAQ,SAAS;AAGhC;;;;;;;;;CASC,GACD,OAAO,MAAMC,mBAAmB,oBAAoB;AAEpD,0EAA0E,GAC1E,MAAMC,gBAAgB;AAEtB,OAAO,MAAMC,iBAAiB,CAACC,QAC7B,OAAOA,UAAU,YACjBA,UAAU,QACV,AAACA,MAA6BC,IAAI,KAAKH,cAAc;AAEvD;;;;;;;;CAQC,GACD,OAAO,MAAMI,gBAAgB,CAC3BC,UACAC,UACgB,CAAA;QAChBD;QACAC;QACAC,WAAW,IAAIC,OAAOC,WAAW;IACnC,CAAA,EAAG;AAEH;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,qBAAqB,CAACC,KACjCb,OAAOc,UAAU,CAAC;QAChBC,KAAK;YACH,MAAMF,GACHG,UAAU,CAAaf,kBACvBgB,WAAW,CAAC;gBAAEV,UAAU;gBAAGC,SAAS;YAAE,GAAG;gBAAEU,QAAQ;YAAK;QAC7D;QACAC,OAAOf,CAAAA,QACL,IAAIL,iBAAiB,kCAAkCK,OAAO;gBAC5DgB,UAAUP,GAAGQ,YAAY;YAC3B;IACJ,GAAG;AAEL;;;;;;;CAOC,GACD,OAAO,MAAMC,iBAAiB,CAACT,IAAQN;IACrC,MAAMS,aAAaH,GAAGG,UAAU,CAAaf;IAE7C,OAAO;QACLsB,OAAOf,CAAAA,UACLR,OAAOc,UAAU,CAAC;gBAChBC,KAAK;oBACH,IAAI;wBACF,MAAMC,WAAWQ,SAAS,CAAClB,cAAcC,UAAUC;wBACnD,OAAO;oBACT,EAAE,OAAOJ,OAAO;wBACd,mEAAmE;wBACnE,+DAA+D;wBAC/D,cAAc;wBACd,IAAID,eAAeC,QAAQ;4BACzB,OAAO;wBACT;wBACA,MAAMA;oBACR;gBACF;gBACAe,OAAOf,CAAAA,QACL,IAAIL,iBAAiB,+BAA+BK,OAAO;wBACzDG;wBACAC;oBACF;YACJ;IACJ;AACF,EAAE"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './adapters/filter-translator.js';
|
|
2
|
+
export * from './adapters/make-mongo-link-resolver.js';
|
|
3
|
+
export * from './adapters/make-mongo-repository.js';
|
|
4
|
+
export * from './mongo-database/mongo-database.js';
|
|
5
|
+
export * from './mongo-database/mongo-health-probe.js';
|
|
6
|
+
export * from './mongo-database/mongo-tenant-resolver.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iCAAiC,CAAC;AAChD,cAAc,wCAAwC,CAAC;AACvD,cAAc,qCAAqC,CAAC;AACpD,cAAc,oCAAoC,CAAC;AACnD,cAAc,wCAAwC,CAAC;AACvD,cAAc,2CAA2C,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './adapters/filter-translator.js';
|
|
2
|
+
export * from './adapters/make-mongo-link-resolver.js';
|
|
3
|
+
export * from './adapters/make-mongo-repository.js';
|
|
4
|
+
export * from './mongo-database/mongo-database.js';
|
|
5
|
+
export * from './mongo-database/mongo-health-probe.js';
|
|
6
|
+
export * from './mongo-database/mongo-tenant-resolver.js';
|
|
7
|
+
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './adapters/filter-translator.js';\nexport * from './adapters/make-mongo-link-resolver.js';\nexport * from './adapters/make-mongo-repository.js';\nexport * from './mongo-database/mongo-database.js';\nexport * from './mongo-database/mongo-health-probe.js';\nexport * from './mongo-database/mongo-tenant-resolver.js';\n"],"names":[],"mappings":"AAAA,cAAc,kCAAkC;AAChD,cAAc,yCAAyC;AACvD,cAAc,sCAAsC;AACpD,cAAc,qCAAqC;AACnD,cAAc,yCAAyC;AACvD,cAAc,4CAA4C"}
|