@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/outbox/relay.ts"],"sourcesContent":["import { ShutdownRegistryTag } from '@entifix/business';\nimport {\n type EventBus,\n EventBusTag,\n type OutboxStats,\n type TransactionOutbox,\n} from '@entifix/transactions';\nimport { Context, Duration, Effect, Either, Fiber } from 'effect';\n\nimport { MongoDatabaseTag } from '../mongo-database/mongo-database.js';\nimport { recordOutboxStats } from './metrics.js';\nimport { ensureOutboxIndexes, makeMongoOutbox } from './store.js';\n\n/** How often the sweep looks for entries the fast path did not carry. */\nexport const SWEEP_INTERVAL = Duration.seconds(15);\n/** Entries drained per pass, per database. */\nexport const BATCH = 100;\n\n/**\n * Publish attempts an outbox entry gets before it is quarantined, from\n * config-service.\n *\n * Configuration rather than a constant because it is a genuine operational\n * tunable: raising it while a flaky broker settles is a config edit, and the\n * next sweep reads the new value. That is the opposite of a subscription's\n * `maxAttempts`, which becomes an immutable queue argument and therefore stays\n * a literal beside its declaration.\n *\n * ⚠️ **Read it with `getNumber`, never a cast.** A value of `'five'` casts to\n * `NaN`, every `attempts >= maxAttempts` comparison goes false, and nothing is\n * ever quarantined — a relay that looks healthy while its head never moves.\n */\nexport class OutboxMaxAttempts extends Context.Tag('OutboxMaxAttempts')<\n OutboxMaxAttempts,\n number\n>() {}\n\n/** What {@link drainOutbox} needs beyond the ports it publishes through. */\nexport interface DrainOptions {\n readonly maxAttempts: number;\n /** The database being drained; carried so a quarantine log names it. */\n readonly database: string;\n}\n\n/**\n * Publish one batch of pending entries, oldest first, and answer how many went.\n *\n * Two orderings are load-bearing here and neither is obvious:\n *\n * - A **still-deliverable** failure returns immediately rather than continuing.\n * The entries behind this one keep their order relative to it, which is what\n * stops a terminal event overtaking the `accepted` for the same transaction.\n * - A **quarantined** failure continues. That is ADR 0030's deliberate\n * skip-so-the-head-moves: an entry that can never publish must stop blocking\n * every entry behind it forever.\n */\nexport const drainOutbox = (\n outbox: TransactionOutbox,\n bus: EventBus,\n options: DrainOptions,\n) =>\n Effect.gen(function* () {\n const entries = yield* outbox.pending(BATCH);\n let sent = 0;\n for (const entry of entries) {\n const outcome = yield* Effect.either(bus.publish(entry.event));\n\n if (Either.isLeft(outcome)) {\n const attempts = entry.attempts + 1;\n const quarantine = attempts >= options.maxAttempts;\n\n yield* outbox.recordFailure(entry, outcome.left.message, quarantine);\n\n if (!quarantine) {\n return sent;\n }\n\n yield* Effect.logError('outbox entry quarantined').pipe(\n Effect.annotateLogs({\n database: options.database,\n eventId: entry.eventId,\n eventName: entry.event.name,\n attempts,\n lastError: outcome.left.message,\n }),\n );\n continue;\n }\n\n yield* outbox.markSent(entry);\n sent += 1;\n }\n return sent;\n });\n\n/**\n * Sample a sweep's depth and age.\n *\n * Defaults to {@link recordOutboxStats}, and a caller overrides it only to\n * assert what was sampled. There is no \"no sink\" option on purpose: an outbox\n * whose depth nothing reports is the failure the gauges exist to make visible.\n */\nexport type OutboxStatsSink = (\n database: string,\n stats: OutboxStats,\n) => Effect.Effect<void>;\n\n/**\n * Drain the one database this service owns, then sample its depth.\n *\n * The single-database counterpart to marketplace-admin's tenant walk. A\n * platform-plane store is one named database, so there is nothing to enumerate\n * and `listDatabases` — which needs a `MongoClient` and cluster-wide rights —\n * never comes into it.\n *\n * Stats are sampled **after** the drain, so the gauge reports what is still\n * waiting rather than what was waiting when the pass began.\n *\n * Every failure is caught and logged. This runs inside `Effect.forever`, and an\n * error that escapes ends the daemon silently — a relay that stopped is\n * indistinguishable from a relay with nothing to do.\n */\nexport const sweepOutbox = (\n outbox: TransactionOutbox,\n bus: EventBus,\n options: DrainOptions & { readonly onStats?: OutboxStatsSink },\n) =>\n Effect.gen(function* () {\n yield* drainOutbox(outbox, bus, options);\n const sink = options.onStats ?? recordOutboxStats;\n yield* sink(options.database, yield* outbox.stats());\n }).pipe(\n Effect.catchAll(error =>\n Effect.logError('outbox sweep failed').pipe(\n Effect.annotateLogs({\n database: options.database,\n error: String(error),\n }),\n ),\n ),\n );\n\n/**\n * Start the relay for a service that owns exactly one database.\n *\n * ⚠️ **The shutdown hook is `flush`, not `stop-intake`, and the phase is the\n * whole point.** `AmqpEventBusLayer` registers `stop-intake` to cancel consumers\n * and drain deliveries; this runs after that, so the last sweep publishes into a\n * connection that is still open. Registered the other way round it would try to\n * publish through a channel already closing, and the entries it was holding\n * would sit until the next boot.\n *\n * The daemon is `delay |> forever`, so the first sweep is one interval away\n * rather than at boot — the write path drains inline, and a sweep racing the\n * index bootstrap on a cold database only produces noise.\n */\nexport const startOutboxRelay = (options?: {\n readonly onStats?: OutboxStatsSink;\n}) =>\n Effect.gen(function* () {\n const db = yield* MongoDatabaseTag;\n const bus = yield* EventBusTag;\n const maxAttempts = yield* OutboxMaxAttempts;\n const shutdown = yield* ShutdownRegistryTag;\n\n yield* ensureOutboxIndexes(db);\n const outbox = makeMongoOutbox(db);\n\n const sweepOnce = sweepOutbox(outbox, bus, {\n maxAttempts,\n database: db.databaseName,\n ...(options?.onStats ? { onStats: options.onStats } : {}),\n });\n\n const daemon = yield* Effect.forkDaemon(\n sweepOnce.pipe(Effect.delay(SWEEP_INTERVAL), Effect.forever),\n );\n\n yield* shutdown.register({\n name: 'outbox-relay',\n phase: 'flush',\n run: Fiber.interrupt(daemon).pipe(Effect.andThen(sweepOnce)),\n });\n });\n"],"names":["ShutdownRegistryTag","EventBusTag","Context","Duration","Effect","Either","Fiber","MongoDatabaseTag","recordOutboxStats","ensureOutboxIndexes","makeMongoOutbox","SWEEP_INTERVAL","seconds","BATCH","OutboxMaxAttempts","Tag","drainOutbox","outbox","bus","options","gen","entries","pending","sent","entry","outcome","either","publish","event","isLeft","attempts","quarantine","maxAttempts","recordFailure","left","message","logError","pipe","annotateLogs","database","eventId","eventName","name","lastError","markSent","sweepOutbox","sink","onStats","stats","catchAll","error","String","startOutboxRelay","db","shutdown","sweepOnce","databaseName","daemon","forkDaemon","delay","forever","register","phase","run","interrupt","andThen"],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { type DomainEvent, EntifixConnError } from '@entifix/core';
|
|
2
|
+
import type { OutboxEntry, TransactionOutbox } from '@entifix/transactions';
|
|
3
|
+
import { Effect } from 'effect';
|
|
4
|
+
import type { Db } from 'mongodb';
|
|
5
|
+
/**
|
|
6
|
+
* The outbox collection, and it lives in the **same database as the entity it
|
|
7
|
+
* describes** — never in a shared control-plane store.
|
|
8
|
+
*
|
|
9
|
+
* Two reasons, and the second only shows up later. Same database means the
|
|
10
|
+
* entity write and its event are one single-database transaction, which stays a
|
|
11
|
+
* single-*shard* transaction if this ever shards; a control-plane outbox would
|
|
12
|
+
* be cross-database from the first commit. And an outbox holds event payloads:
|
|
13
|
+
* a `TransactionEvent` carries no tenant data, but `catalog.published` carries a
|
|
14
|
+
* whole offering and `order.placed` a whole receipt, and neither may land in the
|
|
15
|
+
* control plane on its way to the bus.
|
|
16
|
+
*
|
|
17
|
+
* That rule is why this lives here rather than in one service. It applies
|
|
18
|
+
* identically to a per-organization `tenant_<id>` and to a single platform-plane
|
|
19
|
+
* database, and the second publisher is what turned a copy into a seam
|
|
20
|
+
* ([ADR 0028](../../../../../../docs/adr/0028-the-transaction-id-is-the-clients-and-its-event-ships-with-the-write.md)).
|
|
21
|
+
*/
|
|
22
|
+
export declare const OUTBOX_COLLECTION = "transaction_outbox";
|
|
23
|
+
/**
|
|
24
|
+
* The stored shape of an outbox entry.
|
|
25
|
+
*
|
|
26
|
+
* Exported because the `completed` entry is **not** written through the port:
|
|
27
|
+
* it has to join the entity write's session to be atomic with it, and a session
|
|
28
|
+
* cannot live in a framework-free contract. The handler inserts this document
|
|
29
|
+
* itself; everything else goes through {@link makeMongoOutbox}.
|
|
30
|
+
*/
|
|
31
|
+
export declare const outboxDocument: (event: DomainEvent) => OutboxEntry;
|
|
32
|
+
/**
|
|
33
|
+
* Puts a **quarantined** entry back in the queue, and touches nothing else.
|
|
34
|
+
*
|
|
35
|
+
* The rebuild walk's repair for the one failure ADR 0048 measured: with the
|
|
36
|
+
* broker down long enough, the relay spends `outbox.maxAttempts` on an
|
|
37
|
+
* announcement and quarantines it (ADR 0030's deliberate skip-so-the-head-moves),
|
|
38
|
+
* after which the tenant store reads one thing and the storefront shows another
|
|
39
|
+
* with nothing reporting it. Re-emitting cannot help on its own — the event id is
|
|
40
|
+
* `<offeringId>:<publishedAt>` and the unique index answers `duplicate`.
|
|
41
|
+
*
|
|
42
|
+
* ⚠️ **`quarantined: true` is in the filter, not the update.** An entry that is
|
|
43
|
+
* merely pending keeps its `attempts`, and one already `sent` is never re-sent —
|
|
44
|
+
* which is what keeps this from quietly becoming "re-announce everything", a
|
|
45
|
+
* walk that would re-publish the whole fleet on every boot and un-quarantine
|
|
46
|
+
* genuine poison each time.
|
|
47
|
+
*
|
|
48
|
+
* Not a `TransactionOutbox` method: the port is framework-free and shared by
|
|
49
|
+
* every publisher, and this is a repair specific to the rebuild. `outboxDocument`
|
|
50
|
+
* is exported beside it for the same class of reason.
|
|
51
|
+
*
|
|
52
|
+
* Answers whether an entry was actually revived.
|
|
53
|
+
*/
|
|
54
|
+
export declare const reviveQuarantined: (db: Db, eventId: string) => Effect.Effect<boolean, EntifixConnError, never>;
|
|
55
|
+
/**
|
|
56
|
+
* Ensures the two indexes the outbox depends on.
|
|
57
|
+
*
|
|
58
|
+
* The unique one is load-bearing rather than defensive: it *is* the idempotency
|
|
59
|
+
* check behind the client-generated transaction id, so a replayed command is
|
|
60
|
+
* rejected by the storage engine rather than by a read-then-write race. It is on
|
|
61
|
+
* `eventId` — `<transactionId>:<step>` for a transaction — so the claim and the
|
|
62
|
+
* bus's deduplication key are one value rather than two that can drift. The
|
|
63
|
+
* partial one keeps the relay's `pending` query cheap once the collection fills
|
|
64
|
+
* with sent entries; it filters on `quarantined` too, so an entry past its
|
|
65
|
+
* ceiling leaves the index rather than being read and skipped forever.
|
|
66
|
+
*
|
|
67
|
+
* Mongo rejects a `createIndex` that reuses a key pattern with a different
|
|
68
|
+
* `partialFilterExpression` (`IndexOptionsConflict`), and this runs on every
|
|
69
|
+
* sweep and every create. A database that predates the `quarantined` filter
|
|
70
|
+
* therefore fails here on every pass — which is loud only because the sweep now
|
|
71
|
+
* logs what it catches. The fix is `pnpm run <app>:dev:reset`; nothing runs in
|
|
72
|
+
* production, so there is no migration to write.
|
|
73
|
+
*/
|
|
74
|
+
export declare const ensureOutboxIndexes: (db: Db) => Effect.Effect<void, EntifixConnError, never>;
|
|
75
|
+
/**
|
|
76
|
+
* Mongo-backed {@link TransactionOutbox} over one database.
|
|
77
|
+
*
|
|
78
|
+
* `db` is closed over, so every method's Effect has `R = never` — the same
|
|
79
|
+
* technique `makeMongoRepository` uses. There is no publishing here: the relay
|
|
80
|
+
* composes these methods with the event bus, which is what lets the relay be
|
|
81
|
+
* tested without a broker.
|
|
82
|
+
*/
|
|
83
|
+
export declare const makeMongoOutbox: (db: Db) => TransactionOutbox;
|
|
84
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/outbox/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC5E,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAElC;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,iBAAiB,uBAAuB,CAAC;AAatD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,WAAW,KAAG,WAWlD,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,iBAAiB,GAAI,IAAI,EAAE,EAAE,SAAS,MAAM,oDAyBrD,CAAC;AAEL;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,mBAAmB,GAAI,IAAI,EAAE,iDActC,CAAC;AAEL;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,GAAI,IAAI,EAAE,KAAG,iBA4GxC,CAAC"}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
import { EntifixConnError } from '@entifix/core';
|
|
3
|
+
import { Effect } from 'effect';
|
|
4
|
+
/**
|
|
5
|
+
* The outbox collection, and it lives in the **same database as the entity it
|
|
6
|
+
* describes** — never in a shared control-plane store.
|
|
7
|
+
*
|
|
8
|
+
* Two reasons, and the second only shows up later. Same database means the
|
|
9
|
+
* entity write and its event are one single-database transaction, which stays a
|
|
10
|
+
* single-*shard* transaction if this ever shards; a control-plane outbox would
|
|
11
|
+
* be cross-database from the first commit. And an outbox holds event payloads:
|
|
12
|
+
* a `TransactionEvent` carries no tenant data, but `catalog.published` carries a
|
|
13
|
+
* whole offering and `order.placed` a whole receipt, and neither may land in the
|
|
14
|
+
* control plane on its way to the bus.
|
|
15
|
+
*
|
|
16
|
+
* That rule is why this lives here rather than in one service. It applies
|
|
17
|
+
* identically to a per-organization `tenant_<id>` and to a single platform-plane
|
|
18
|
+
* database, and the second publisher is what turned a copy into a seam
|
|
19
|
+
* ([ADR 0028](../../../../../../docs/adr/0028-the-transaction-id-is-the-clients-and-its-event-ships-with-the-write.md)).
|
|
20
|
+
*/ export const OUTBOX_COLLECTION = 'transaction_outbox';
|
|
21
|
+
/** Drop Mongo's internal `_id` from every read. */ const WITHOUT_MONGO_ID = {
|
|
22
|
+
projection: {
|
|
23
|
+
_id: 0
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/** Mongo's duplicate-key error, which is how a replayed command is recognised. */ const DUPLICATE_KEY = 11000;
|
|
27
|
+
const isDuplicateKey = (error)=>typeof error === 'object' && error !== null && error.code === DUPLICATE_KEY;
|
|
28
|
+
/**
|
|
29
|
+
* The stored shape of an outbox entry.
|
|
30
|
+
*
|
|
31
|
+
* Exported because the `completed` entry is **not** written through the port:
|
|
32
|
+
* it has to join the entity write's session to be atomic with it, and a session
|
|
33
|
+
* cannot live in a framework-free contract. The handler inserts this document
|
|
34
|
+
* itself; everything else goes through {@link makeMongoOutbox}.
|
|
35
|
+
*/ export const outboxDocument = (event)=>({
|
|
36
|
+
eventId: event.id,
|
|
37
|
+
event,
|
|
38
|
+
sent: false,
|
|
39
|
+
// Defaulted here rather than at each write site because there are two of
|
|
40
|
+
// them: the relay's `enqueue`, and the transaction handler's own insert
|
|
41
|
+
// inside the entity's Mongo session. An entry missing `attempts` would sort
|
|
42
|
+
// into `pending` and then fail `attempts + 1` arithmetic on a `undefined`.
|
|
43
|
+
attempts: 0,
|
|
44
|
+
quarantined: false,
|
|
45
|
+
createdAt: event.at
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* Puts a **quarantined** entry back in the queue, and touches nothing else.
|
|
49
|
+
*
|
|
50
|
+
* The rebuild walk's repair for the one failure ADR 0048 measured: with the
|
|
51
|
+
* broker down long enough, the relay spends `outbox.maxAttempts` on an
|
|
52
|
+
* announcement and quarantines it (ADR 0030's deliberate skip-so-the-head-moves),
|
|
53
|
+
* after which the tenant store reads one thing and the storefront shows another
|
|
54
|
+
* with nothing reporting it. Re-emitting cannot help on its own — the event id is
|
|
55
|
+
* `<offeringId>:<publishedAt>` and the unique index answers `duplicate`.
|
|
56
|
+
*
|
|
57
|
+
* ⚠️ **`quarantined: true` is in the filter, not the update.** An entry that is
|
|
58
|
+
* merely pending keeps its `attempts`, and one already `sent` is never re-sent —
|
|
59
|
+
* which is what keeps this from quietly becoming "re-announce everything", a
|
|
60
|
+
* walk that would re-publish the whole fleet on every boot and un-quarantine
|
|
61
|
+
* genuine poison each time.
|
|
62
|
+
*
|
|
63
|
+
* Not a `TransactionOutbox` method: the port is framework-free and shared by
|
|
64
|
+
* every publisher, and this is a repair specific to the rebuild. `outboxDocument`
|
|
65
|
+
* is exported beside it for the same class of reason.
|
|
66
|
+
*
|
|
67
|
+
* Answers whether an entry was actually revived.
|
|
68
|
+
*/ export const reviveQuarantined = (db, eventId)=>Effect.tryPromise({
|
|
69
|
+
try: async ()=>{
|
|
70
|
+
const result = await db.collection(OUTBOX_COLLECTION).updateOne({
|
|
71
|
+
eventId,
|
|
72
|
+
quarantined: true
|
|
73
|
+
}, {
|
|
74
|
+
$set: {
|
|
75
|
+
sent: false,
|
|
76
|
+
quarantined: false,
|
|
77
|
+
attempts: 0
|
|
78
|
+
},
|
|
79
|
+
// The failure that put it here is spent; leaving it would make the
|
|
80
|
+
// next quarantine's log name an error from a different outage.
|
|
81
|
+
$unset: {
|
|
82
|
+
lastError: ''
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return result.modifiedCount === 1;
|
|
86
|
+
},
|
|
87
|
+
catch: (error)=>new EntifixConnError('Failed to revive a quarantined outbox entry', error, {
|
|
88
|
+
eventId,
|
|
89
|
+
db: db.databaseName
|
|
90
|
+
})
|
|
91
|
+
});
|
|
92
|
+
/**
|
|
93
|
+
* Ensures the two indexes the outbox depends on.
|
|
94
|
+
*
|
|
95
|
+
* The unique one is load-bearing rather than defensive: it *is* the idempotency
|
|
96
|
+
* check behind the client-generated transaction id, so a replayed command is
|
|
97
|
+
* rejected by the storage engine rather than by a read-then-write race. It is on
|
|
98
|
+
* `eventId` — `<transactionId>:<step>` for a transaction — so the claim and the
|
|
99
|
+
* bus's deduplication key are one value rather than two that can drift. The
|
|
100
|
+
* partial one keeps the relay's `pending` query cheap once the collection fills
|
|
101
|
+
* with sent entries; it filters on `quarantined` too, so an entry past its
|
|
102
|
+
* ceiling leaves the index rather than being read and skipped forever.
|
|
103
|
+
*
|
|
104
|
+
* Mongo rejects a `createIndex` that reuses a key pattern with a different
|
|
105
|
+
* `partialFilterExpression` (`IndexOptionsConflict`), and this runs on every
|
|
106
|
+
* sweep and every create. A database that predates the `quarantined` filter
|
|
107
|
+
* therefore fails here on every pass — which is loud only because the sweep now
|
|
108
|
+
* logs what it catches. The fix is `pnpm run <app>:dev:reset`; nothing runs in
|
|
109
|
+
* production, so there is no migration to write.
|
|
110
|
+
*/ export const ensureOutboxIndexes = (db)=>Effect.tryPromise({
|
|
111
|
+
try: async ()=>{
|
|
112
|
+
const collection = db.collection(OUTBOX_COLLECTION);
|
|
113
|
+
await collection.createIndex({
|
|
114
|
+
eventId: 1
|
|
115
|
+
}, {
|
|
116
|
+
unique: true
|
|
117
|
+
});
|
|
118
|
+
await collection.createIndex({
|
|
119
|
+
createdAt: 1
|
|
120
|
+
}, {
|
|
121
|
+
partialFilterExpression: {
|
|
122
|
+
sent: false,
|
|
123
|
+
quarantined: false
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
catch: (error)=>new EntifixConnError('Failed to create outbox indexes', error, {
|
|
128
|
+
db: db.databaseName
|
|
129
|
+
})
|
|
130
|
+
});
|
|
131
|
+
/**
|
|
132
|
+
* Mongo-backed {@link TransactionOutbox} over one database.
|
|
133
|
+
*
|
|
134
|
+
* `db` is closed over, so every method's Effect has `R = never` — the same
|
|
135
|
+
* technique `makeMongoRepository` uses. There is no publishing here: the relay
|
|
136
|
+
* composes these methods with the event bus, which is what lets the relay be
|
|
137
|
+
* tested without a broker.
|
|
138
|
+
*/ export const makeMongoOutbox = (db)=>{
|
|
139
|
+
const collection = db.collection(OUTBOX_COLLECTION);
|
|
140
|
+
const fail = (message, error, details)=>new EntifixConnError(message, error, details);
|
|
141
|
+
return {
|
|
142
|
+
enqueue: (event)=>Effect.tryPromise({
|
|
143
|
+
try: async ()=>{
|
|
144
|
+
try {
|
|
145
|
+
await collection.insertOne(outboxDocument(event));
|
|
146
|
+
return 'enqueued';
|
|
147
|
+
} catch (error) {
|
|
148
|
+
// Not a failure: the unique index rejected a second claim for this
|
|
149
|
+
// message id, which is precisely how a retry is identified.
|
|
150
|
+
if (isDuplicateKey(error)) {
|
|
151
|
+
return 'duplicate';
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
catch: (error)=>fail('Failed to enqueue outbox entry', error, {
|
|
157
|
+
eventId: event.id
|
|
158
|
+
})
|
|
159
|
+
}),
|
|
160
|
+
pending: (limit)=>Effect.tryPromise({
|
|
161
|
+
// Oldest first, so `accepted` reaches the bus before the terminal event
|
|
162
|
+
// for the same transaction and the tracker folds them in order.
|
|
163
|
+
try: ()=>collection.find({
|
|
164
|
+
sent: false,
|
|
165
|
+
quarantined: false
|
|
166
|
+
}, WITHOUT_MONGO_ID).sort({
|
|
167
|
+
createdAt: 1
|
|
168
|
+
}).limit(limit).toArray(),
|
|
169
|
+
catch: (error)=>fail('Failed to read pending outbox entries', error, {
|
|
170
|
+
db: db.databaseName
|
|
171
|
+
})
|
|
172
|
+
}),
|
|
173
|
+
markSent: (entry)=>Effect.tryPromise({
|
|
174
|
+
try: ()=>collection.updateOne({
|
|
175
|
+
eventId: entry.eventId
|
|
176
|
+
}, {
|
|
177
|
+
$set: {
|
|
178
|
+
sent: true
|
|
179
|
+
}
|
|
180
|
+
}),
|
|
181
|
+
catch: (error)=>fail('Failed to mark outbox entry sent', error, {
|
|
182
|
+
eventId: entry.eventId
|
|
183
|
+
})
|
|
184
|
+
}).pipe(Effect.asVoid),
|
|
185
|
+
stats: ()=>Effect.tryPromise({
|
|
186
|
+
// Two counts and one `findOne`, rather than reading the entries: the
|
|
187
|
+
// pending set can be arbitrarily large when the relay is stuck, which
|
|
188
|
+
// is exactly the moment this must stay cheap. Both queries ride the
|
|
189
|
+
// partial index on `{ createdAt: 1 }` filtered to the same predicate.
|
|
190
|
+
try: async ()=>{
|
|
191
|
+
const pendingFilter = {
|
|
192
|
+
sent: false,
|
|
193
|
+
quarantined: false
|
|
194
|
+
};
|
|
195
|
+
const [pending, quarantined, oldest] = await Promise.all([
|
|
196
|
+
collection.countDocuments(pendingFilter),
|
|
197
|
+
collection.countDocuments({
|
|
198
|
+
quarantined: true
|
|
199
|
+
}),
|
|
200
|
+
collection.findOne(pendingFilter, {
|
|
201
|
+
sort: {
|
|
202
|
+
createdAt: 1
|
|
203
|
+
},
|
|
204
|
+
projection: {
|
|
205
|
+
_id: 0,
|
|
206
|
+
createdAt: 1
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
]);
|
|
210
|
+
return _extends({
|
|
211
|
+
pending,
|
|
212
|
+
quarantined
|
|
213
|
+
}, oldest === null ? {} : {
|
|
214
|
+
oldestPendingAt: oldest.createdAt
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
catch: (error)=>fail('Failed to read outbox stats', error)
|
|
218
|
+
}),
|
|
219
|
+
recordFailure: (entry, error, quarantine)=>Effect.tryPromise({
|
|
220
|
+
// `$inc` rather than a read-modify-write: two relays can be draining the
|
|
221
|
+
// same tenant (the request's inline drain and the daemon sweep), and an
|
|
222
|
+
// absolute write would let one of them lose the other's attempt and
|
|
223
|
+
// stretch the ceiling indefinitely.
|
|
224
|
+
try: ()=>collection.updateOne({
|
|
225
|
+
eventId: entry.eventId
|
|
226
|
+
}, {
|
|
227
|
+
$inc: {
|
|
228
|
+
attempts: 1
|
|
229
|
+
},
|
|
230
|
+
$set: {
|
|
231
|
+
lastError: error,
|
|
232
|
+
quarantined: quarantine
|
|
233
|
+
}
|
|
234
|
+
}),
|
|
235
|
+
catch: (cause)=>fail('Failed to record an outbox publish failure', cause, {
|
|
236
|
+
eventId: entry.eventId
|
|
237
|
+
})
|
|
238
|
+
}).pipe(Effect.asVoid)
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/outbox/store.ts"],"sourcesContent":["import { type DomainEvent, EntifixConnError } from '@entifix/core';\nimport type { OutboxEntry, TransactionOutbox } from '@entifix/transactions';\nimport { Effect } from 'effect';\nimport type { Db } from 'mongodb';\n\n/**\n * The outbox collection, and it lives in the **same database as the entity it\n * describes** — never in a shared control-plane store.\n *\n * Two reasons, and the second only shows up later. Same database means the\n * entity write and its event are one single-database transaction, which stays a\n * single-*shard* transaction if this ever shards; a control-plane outbox would\n * be cross-database from the first commit. And an outbox holds event payloads:\n * a `TransactionEvent` carries no tenant data, but `catalog.published` carries a\n * whole offering and `order.placed` a whole receipt, and neither may land in the\n * control plane on its way to the bus.\n *\n * That rule is why this lives here rather than in one service. It applies\n * identically to a per-organization `tenant_<id>` and to a single platform-plane\n * database, and the second publisher is what turned a copy into a seam\n * ([ADR 0028](../../../../../../docs/adr/0028-the-transaction-id-is-the-clients-and-its-event-ships-with-the-write.md)).\n */\nexport const OUTBOX_COLLECTION = 'transaction_outbox';\n\n/** Drop Mongo's internal `_id` from every read. */\nconst WITHOUT_MONGO_ID = { projection: { _id: 0 } } as const;\n\n/** Mongo's duplicate-key error, which is how a replayed command is recognised. */\nconst DUPLICATE_KEY = 11000;\n\nconst 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 an outbox entry.\n *\n * Exported because the `completed` entry is **not** written through the port:\n * it has to join the entity write's session to be atomic with it, and a session\n * cannot live in a framework-free contract. The handler inserts this document\n * itself; everything else goes through {@link makeMongoOutbox}.\n */\nexport const outboxDocument = (event: DomainEvent): OutboxEntry => ({\n eventId: event.id,\n event,\n sent: false,\n // Defaulted here rather than at each write site because there are two of\n // them: the relay's `enqueue`, and the transaction handler's own insert\n // inside the entity's Mongo session. An entry missing `attempts` would sort\n // into `pending` and then fail `attempts + 1` arithmetic on a `undefined`.\n attempts: 0,\n quarantined: false,\n createdAt: event.at,\n});\n\n/**\n * Puts a **quarantined** entry back in the queue, and touches nothing else.\n *\n * The rebuild walk's repair for the one failure ADR 0048 measured: with the\n * broker down long enough, the relay spends `outbox.maxAttempts` on an\n * announcement and quarantines it (ADR 0030's deliberate skip-so-the-head-moves),\n * after which the tenant store reads one thing and the storefront shows another\n * with nothing reporting it. Re-emitting cannot help on its own — the event id is\n * `<offeringId>:<publishedAt>` and the unique index answers `duplicate`.\n *\n * ⚠️ **`quarantined: true` is in the filter, not the update.** An entry that is\n * merely pending keeps its `attempts`, and one already `sent` is never re-sent —\n * which is what keeps this from quietly becoming \"re-announce everything\", a\n * walk that would re-publish the whole fleet on every boot and un-quarantine\n * genuine poison each time.\n *\n * Not a `TransactionOutbox` method: the port is framework-free and shared by\n * every publisher, and this is a repair specific to the rebuild. `outboxDocument`\n * is exported beside it for the same class of reason.\n *\n * Answers whether an entry was actually revived.\n */\nexport const reviveQuarantined = (db: Db, eventId: string) =>\n Effect.tryPromise({\n try: async () => {\n const result = await db\n .collection<OutboxEntry>(OUTBOX_COLLECTION)\n .updateOne(\n { eventId, quarantined: true },\n {\n $set: { sent: false, quarantined: false, attempts: 0 },\n // The failure that put it here is spent; leaving it would make the\n // next quarantine's log name an error from a different outage.\n $unset: { lastError: '' },\n },\n );\n return result.modifiedCount === 1;\n },\n catch: error =>\n new EntifixConnError(\n 'Failed to revive a quarantined outbox entry',\n error,\n {\n eventId,\n db: db.databaseName,\n },\n ),\n });\n\n/**\n * Ensures the two indexes the outbox depends on.\n *\n * The unique one is load-bearing rather than defensive: it *is* the idempotency\n * check behind the client-generated transaction id, so a replayed command is\n * rejected by the storage engine rather than by a read-then-write race. It is on\n * `eventId` — `<transactionId>:<step>` for a transaction — so the claim and the\n * bus's deduplication key are one value rather than two that can drift. The\n * partial one keeps the relay's `pending` query cheap once the collection fills\n * with sent entries; it filters on `quarantined` too, so an entry past its\n * ceiling leaves the index rather than being read and skipped forever.\n *\n * Mongo rejects a `createIndex` that reuses a key pattern with a different\n * `partialFilterExpression` (`IndexOptionsConflict`), and this runs on every\n * sweep and every create. A database that predates the `quarantined` filter\n * therefore fails here on every pass — which is loud only because the sweep now\n * logs what it catches. The fix is `pnpm run <app>:dev:reset`; nothing runs in\n * production, so there is no migration to write.\n */\nexport const ensureOutboxIndexes = (db: Db) =>\n Effect.tryPromise({\n try: async () => {\n const collection = db.collection(OUTBOX_COLLECTION);\n await collection.createIndex({ eventId: 1 }, { unique: true });\n await collection.createIndex(\n { createdAt: 1 },\n { partialFilterExpression: { sent: false, quarantined: false } },\n );\n },\n catch: error =>\n new EntifixConnError('Failed to create outbox indexes', error, {\n db: db.databaseName,\n }),\n });\n\n/**\n * Mongo-backed {@link TransactionOutbox} over one database.\n *\n * `db` is closed over, so every method's Effect has `R = never` — the same\n * technique `makeMongoRepository` uses. There is no publishing here: the relay\n * composes these methods with the event bus, which is what lets the relay be\n * tested without a broker.\n */\nexport const makeMongoOutbox = (db: Db): TransactionOutbox => {\n const collection = db.collection<OutboxEntry>(OUTBOX_COLLECTION);\n\n const fail = (\n message: string,\n error: unknown,\n details?: Record<string, unknown>,\n ) => new EntifixConnError(message, error, details);\n\n return {\n enqueue: event =>\n Effect.tryPromise({\n try: async () => {\n try {\n await collection.insertOne(outboxDocument(event));\n return 'enqueued' as const;\n } catch (error) {\n // Not a failure: the unique index rejected a second claim for this\n // message id, which is precisely how a retry is identified.\n if (isDuplicateKey(error)) {\n return 'duplicate' as const;\n }\n throw error;\n }\n },\n catch: error =>\n fail('Failed to enqueue outbox entry', error, { eventId: event.id }),\n }),\n\n pending: limit =>\n Effect.tryPromise({\n // Oldest first, so `accepted` reaches the bus before the terminal event\n // for the same transaction and the tracker folds them in order.\n try: () =>\n collection\n .find({ sent: false, quarantined: false }, WITHOUT_MONGO_ID)\n .sort({ createdAt: 1 })\n .limit(limit)\n .toArray(),\n catch: error =>\n fail('Failed to read pending outbox entries', error, {\n db: db.databaseName,\n }),\n }),\n\n markSent: entry =>\n Effect.tryPromise({\n try: () =>\n collection.updateOne(\n { eventId: entry.eventId },\n { $set: { sent: true } },\n ),\n catch: error =>\n fail('Failed to mark outbox entry sent', error, {\n eventId: entry.eventId,\n }),\n }).pipe(Effect.asVoid),\n\n stats: () =>\n Effect.tryPromise({\n // Two counts and one `findOne`, rather than reading the entries: the\n // pending set can be arbitrarily large when the relay is stuck, which\n // is exactly the moment this must stay cheap. Both queries ride the\n // partial index on `{ createdAt: 1 }` filtered to the same predicate.\n try: async () => {\n const pendingFilter = { sent: false, quarantined: false };\n const [pending, quarantined, oldest] = await Promise.all([\n collection.countDocuments(pendingFilter),\n collection.countDocuments({ quarantined: true }),\n collection.findOne(pendingFilter, {\n sort: { createdAt: 1 },\n projection: { _id: 0, createdAt: 1 },\n }),\n ]);\n\n return {\n pending,\n quarantined,\n // Absent rather than zero when the outbox is empty: an age of zero\n // and \"nothing is waiting\" are opposite conditions, and a gauge\n // that reports 0 for both is indistinguishable from a healthy relay\n // at exactly the moment it is not one.\n ...(oldest === null ? {} : { oldestPendingAt: oldest.createdAt }),\n };\n },\n catch: error => fail('Failed to read outbox stats', error),\n }),\n\n recordFailure: (entry, error, quarantine) =>\n Effect.tryPromise({\n // `$inc` rather than a read-modify-write: two relays can be draining the\n // same tenant (the request's inline drain and the daemon sweep), and an\n // absolute write would let one of them lose the other's attempt and\n // stretch the ceiling indefinitely.\n try: () =>\n collection.updateOne(\n { eventId: entry.eventId },\n {\n $inc: { attempts: 1 },\n $set: { lastError: error, quarantined: quarantine },\n },\n ),\n catch: cause =>\n fail('Failed to record an outbox publish failure', cause, {\n eventId: entry.eventId,\n }),\n }).pipe(Effect.asVoid),\n };\n};\n"],"names":["EntifixConnError","Effect","OUTBOX_COLLECTION","WITHOUT_MONGO_ID","projection","_id","DUPLICATE_KEY","isDuplicateKey","error","code","outboxDocument","event","eventId","id","sent","attempts","quarantined","createdAt","at","reviveQuarantined","db","tryPromise","try","result","collection","updateOne","$set","$unset","lastError","modifiedCount","catch","databaseName","ensureOutboxIndexes","createIndex","unique","partialFilterExpression","makeMongoOutbox","fail","message","details","enqueue","insertOne","pending","limit","find","sort","toArray","markSent","entry","pipe","asVoid","stats","pendingFilter","oldest","Promise","all","countDocuments","findOne","oldestPendingAt","recordFailure","quarantine","$inc","cause"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The half of this package that needs `@entifix/transactions`: the
|
|
3
|
+
* transactional outbox ADR 0028 writes inside the entity's own Mongo
|
|
4
|
+
* transaction, and the inbox that makes a redelivered command idempotent.
|
|
5
|
+
*
|
|
6
|
+
* Behind a subpath so entity CRUD over Mongo does not arrive with the saga
|
|
7
|
+
* engine attached. Package-level dependencies are not per-subpath, so what
|
|
8
|
+
* actually keeps it optional is the `peerDependenciesMeta.optional` entry in
|
|
9
|
+
* the manifest — importing this module without that peer installed is the
|
|
10
|
+
* error, and it is the adopter's to make deliberately.
|
|
11
|
+
*/
|
|
12
|
+
export * from '../inbox/store.js';
|
|
13
|
+
export * from '../outbox/metrics.js';
|
|
14
|
+
export * from '../outbox/relay.js';
|
|
15
|
+
export * from '../outbox/store.js';
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transactions/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The half of this package that needs `@entifix/transactions`: the
|
|
3
|
+
* transactional outbox ADR 0028 writes inside the entity's own Mongo
|
|
4
|
+
* transaction, and the inbox that makes a redelivered command idempotent.
|
|
5
|
+
*
|
|
6
|
+
* Behind a subpath so entity CRUD over Mongo does not arrive with the saga
|
|
7
|
+
* engine attached. Package-level dependencies are not per-subpath, so what
|
|
8
|
+
* actually keeps it optional is the `peerDependenciesMeta.optional` entry in
|
|
9
|
+
* the manifest — importing this module without that peer installed is the
|
|
10
|
+
* error, and it is the adopter's to make deliberately.
|
|
11
|
+
*/ export * from '../inbox/store.js';
|
|
12
|
+
export * from '../outbox/metrics.js';
|
|
13
|
+
export * from '../outbox/relay.js';
|
|
14
|
+
export * from '../outbox/store.js';
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/transactions/index.ts"],"sourcesContent":["/**\n * The half of this package that needs `@entifix/transactions`: the\n * transactional outbox ADR 0028 writes inside the entity's own Mongo\n * transaction, and the inbox that makes a redelivered command idempotent.\n *\n * Behind a subpath so entity CRUD over Mongo does not arrive with the saga\n * engine attached. Package-level dependencies are not per-subpath, so what\n * actually keeps it optional is the `peerDependenciesMeta.optional` entry in\n * the manifest — importing this module without that peer installed is the\n * error, and it is the adopter's to make deliberately.\n */\nexport * from '../inbox/store.js';\nexport * from '../outbox/metrics.js';\nexport * from '../outbox/relay.js';\nexport * from '../outbox/store.js';\n"],"names":[],"mappings":"AAAA;;;;;;;;;;CAUC,GACD,cAAc,oBAAoB;AAClC,cAAc,uBAAuB;AACrC,cAAc,qBAAqB;AACnC,cAAc,qBAAqB"}
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@entifix/mongo",
|
|
3
|
+
"version": "0.1.0-0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/r10c-technologies/entifix.git",
|
|
8
|
+
"directory": "packages/ts/mongo-client"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"module": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
"./package.json": "./package.json",
|
|
19
|
+
".": {
|
|
20
|
+
"@entifix/source": "./src/index.ts",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./transactions": {
|
|
26
|
+
"@entifix/source": "./src/transactions/index.ts",
|
|
27
|
+
"types": "./dist/transactions/index.d.ts",
|
|
28
|
+
"import": "./dist/transactions/index.js",
|
|
29
|
+
"default": "./dist/transactions/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"!**/*.tsbuildinfo",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"nx": {
|
|
38
|
+
"tags": [
|
|
39
|
+
"layer:entifix",
|
|
40
|
+
"scope:shared",
|
|
41
|
+
"entifix:client",
|
|
42
|
+
"runtime:datastore",
|
|
43
|
+
"tier:2"
|
|
44
|
+
],
|
|
45
|
+
"sourceRoot": "packages/ts/mongo-client/src",
|
|
46
|
+
"targets": {
|
|
47
|
+
"build": {
|
|
48
|
+
"executor": "@nx/js:swc",
|
|
49
|
+
"outputs": [
|
|
50
|
+
"{options.outputPath}"
|
|
51
|
+
],
|
|
52
|
+
"options": {
|
|
53
|
+
"outputPath": "packages/ts/mongo-client/dist",
|
|
54
|
+
"main": "packages/ts/mongo-client/src/index.ts",
|
|
55
|
+
"tsConfig": "packages/ts/mongo-client/tsconfig.lib.json",
|
|
56
|
+
"skipTypeCheck": true,
|
|
57
|
+
"stripLeadingPaths": true
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@swc/helpers": "0.5.23",
|
|
64
|
+
"mongodb": "7.5.0",
|
|
65
|
+
"@entifix/business": "0.1.0-0",
|
|
66
|
+
"@entifix/core": "0.1.0-0"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"effect": "3.22.1",
|
|
70
|
+
"@entifix/transactions": "0.1.0-0",
|
|
71
|
+
"@entifix/testing-unit": "0.1.0-0"
|
|
72
|
+
},
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"effect": ">=3.22 <4",
|
|
75
|
+
"@entifix/transactions": "0.1.0-0"
|
|
76
|
+
},
|
|
77
|
+
"peerDependenciesMeta": {
|
|
78
|
+
"@entifix/transactions": {
|
|
79
|
+
"optional": true
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|