@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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/dist/adapters/filter-translator.d.ts +19 -0
  3. package/dist/adapters/filter-translator.d.ts.map +1 -0
  4. package/dist/adapters/filter-translator.js +142 -0
  5. package/dist/adapters/filter-translator.js.map +1 -0
  6. package/dist/adapters/make-mongo-link-resolver.d.ts +14 -0
  7. package/dist/adapters/make-mongo-link-resolver.d.ts.map +1 -0
  8. package/dist/adapters/make-mongo-link-resolver.js +31 -0
  9. package/dist/adapters/make-mongo-link-resolver.js.map +1 -0
  10. package/dist/adapters/make-mongo-repository.d.ts +19 -0
  11. package/dist/adapters/make-mongo-repository.d.ts.map +1 -0
  12. package/dist/adapters/make-mongo-repository.js +122 -0
  13. package/dist/adapters/make-mongo-repository.js.map +1 -0
  14. package/dist/inbox/store.d.ts +50 -0
  15. package/dist/inbox/store.d.ts.map +1 -0
  16. package/dist/inbox/store.js +86 -0
  17. package/dist/inbox/store.js.map +1 -0
  18. package/dist/index.d.ts +7 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +8 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/mongo-database/mongo-database.d.ts +52 -0
  23. package/dist/mongo-database/mongo-database.d.ts.map +1 -0
  24. package/dist/mongo-database/mongo-database.js +52 -0
  25. package/dist/mongo-database/mongo-database.js.map +1 -0
  26. package/dist/mongo-database/mongo-health-probe.d.ts +28 -0
  27. package/dist/mongo-database/mongo-health-probe.d.ts.map +1 -0
  28. package/dist/mongo-database/mongo-health-probe.js +39 -0
  29. package/dist/mongo-database/mongo-health-probe.js.map +1 -0
  30. package/dist/mongo-database/mongo-tenant-resolver.d.ts +16 -0
  31. package/dist/mongo-database/mongo-tenant-resolver.d.ts.map +1 -0
  32. package/dist/mongo-database/mongo-tenant-resolver.js +40 -0
  33. package/dist/mongo-database/mongo-tenant-resolver.js.map +1 -0
  34. package/dist/outbox/metrics.d.ts +46 -0
  35. package/dist/outbox/metrics.d.ts.map +1 -0
  36. package/dist/outbox/metrics.js +63 -0
  37. package/dist/outbox/metrics.js.map +1 -0
  38. package/dist/outbox/relay.d.ts +89 -0
  39. package/dist/outbox/relay.d.ts.map +1 -0
  40. package/dist/outbox/relay.js +119 -0
  41. package/dist/outbox/relay.js.map +1 -0
  42. package/dist/outbox/store.d.ts +84 -0
  43. package/dist/outbox/store.d.ts.map +1 -0
  44. package/dist/outbox/store.js +242 -0
  45. package/dist/outbox/store.js.map +1 -0
  46. package/dist/transactions/index.d.ts +16 -0
  47. package/dist/transactions/index.d.ts.map +1 -0
  48. package/dist/transactions/index.js +16 -0
  49. package/dist/transactions/index.js.map +1 -0
  50. 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"}
@@ -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"}