@entifix/testing-unit 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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/dist/builders/entity-builder.d.ts +26 -0
  4. package/dist/builders/entity-builder.d.ts.map +1 -0
  5. package/dist/builders/entity-builder.js +30 -0
  6. package/dist/builders/entity-builder.js.map +1 -0
  7. package/dist/contracts/entity-repository.contract.d.ts +36 -0
  8. package/dist/contracts/entity-repository.contract.d.ts.map +1 -0
  9. package/dist/contracts/entity-repository.contract.js +273 -0
  10. package/dist/contracts/entity-repository.contract.js.map +1 -0
  11. package/dist/contracts/index.d.ts +3 -0
  12. package/dist/contracts/index.d.ts.map +1 -0
  13. package/dist/contracts/index.js +4 -0
  14. package/dist/contracts/index.js.map +1 -0
  15. package/dist/contracts/transaction-ports.contract.d.ts +44 -0
  16. package/dist/contracts/transaction-ports.contract.d.ts.map +1 -0
  17. package/dist/contracts/transaction-ports.contract.js +203 -0
  18. package/dist/contracts/transaction-ports.contract.js.map +1 -0
  19. package/dist/doubles/configuration.d.ts +36 -0
  20. package/dist/doubles/configuration.d.ts.map +1 -0
  21. package/dist/doubles/configuration.js +31 -0
  22. package/dist/doubles/configuration.js.map +1 -0
  23. package/dist/doubles/entity-repository.d.ts +24 -0
  24. package/dist/doubles/entity-repository.d.ts.map +1 -0
  25. package/dist/doubles/entity-repository.js +159 -0
  26. package/dist/doubles/entity-repository.js.map +1 -0
  27. package/dist/doubles/link-resolver.d.ts +17 -0
  28. package/dist/doubles/link-resolver.d.ts.map +1 -0
  29. package/dist/doubles/link-resolver.js +34 -0
  30. package/dist/doubles/link-resolver.js.map +1 -0
  31. package/dist/doubles/transaction-ports.d.ts +70 -0
  32. package/dist/doubles/transaction-ports.d.ts.map +1 -0
  33. package/dist/doubles/transaction-ports.js +145 -0
  34. package/dist/doubles/transaction-ports.js.map +1 -0
  35. package/dist/drivers/fake-amqp.d.ts +91 -0
  36. package/dist/drivers/fake-amqp.d.ts.map +1 -0
  37. package/dist/drivers/fake-amqp.js +164 -0
  38. package/dist/drivers/fake-amqp.js.map +1 -0
  39. package/dist/drivers/fake-mongo.d.ts +59 -0
  40. package/dist/drivers/fake-mongo.d.ts.map +1 -0
  41. package/dist/drivers/fake-mongo.js +603 -0
  42. package/dist/drivers/fake-mongo.js.map +1 -0
  43. package/dist/drivers/fake-redis.d.ts +32 -0
  44. package/dist/drivers/fake-redis.d.ts.map +1 -0
  45. package/dist/drivers/fake-redis.js +164 -0
  46. package/dist/drivers/fake-redis.js.map +1 -0
  47. package/dist/drivers/fake-sql.d.ts +58 -0
  48. package/dist/drivers/fake-sql.d.ts.map +1 -0
  49. package/dist/drivers/fake-sql.js +102 -0
  50. package/dist/drivers/fake-sql.js.map +1 -0
  51. package/dist/drivers/index.d.ts +5 -0
  52. package/dist/drivers/index.d.ts.map +1 -0
  53. package/dist/drivers/index.js +6 -0
  54. package/dist/drivers/index.js.map +1 -0
  55. package/dist/effect/run.d.ts +34 -0
  56. package/dist/effect/run.d.ts.map +1 -0
  57. package/dist/effect/run.js +48 -0
  58. package/dist/effect/run.js.map +1 -0
  59. package/dist/http/entity-handlers.d.ts +37 -0
  60. package/dist/http/entity-handlers.d.ts.map +1 -0
  61. package/dist/http/entity-handlers.js +92 -0
  62. package/dist/http/entity-handlers.js.map +1 -0
  63. package/dist/http/index.d.ts +3 -0
  64. package/dist/http/index.d.ts.map +1 -0
  65. package/dist/http/index.js +4 -0
  66. package/dist/http/index.js.map +1 -0
  67. package/dist/http/server.d.ts +19 -0
  68. package/dist/http/server.d.ts.map +1 -0
  69. package/dist/http/server.js +31 -0
  70. package/dist/http/server.js.map +1 -0
  71. package/dist/index.d.ts +7 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +8 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/react/index.d.ts +3 -0
  76. package/dist/react/index.d.ts.map +1 -0
  77. package/dist/react/index.js +4 -0
  78. package/dist/react/index.js.map +1 -0
  79. package/dist/react/render.d.ts +30 -0
  80. package/dist/react/render.d.ts.map +1 -0
  81. package/dist/react/render.js +38 -0
  82. package/dist/react/render.js.map +1 -0
  83. package/dist/react/ui-preferences.d.ts +17 -0
  84. package/dist/react/ui-preferences.d.ts.map +1 -0
  85. package/dist/react/ui-preferences.js +29 -0
  86. package/dist/react/ui-preferences.js.map +1 -0
  87. package/package.json +122 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/drivers/fake-mongo.ts"],"sourcesContent":["/**\n * A fake of the mongodb driver, not of the repository port.\n *\n * It sits one level *below* `makeMongoRepository`, so the adapter's own code —\n * the filter translation, the projection, the upsert rule, the error mapping —\n * actually runs and is measured by coverage. A fake at the port level would\n * skip all of it.\n *\n * Only the surface the adapter uses is implemented; anything else is a\n * deliberate `TypeError` rather than a silent no-op.\n */\n\n/** The subset of a Mongo query document the adapter produces. */\ntype QueryDocument = Record<string, unknown>;\ntype Document = Record<string, unknown>;\n\nconst isPlainObject = (value: unknown): value is Document =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst compare = (left: unknown, right: unknown): number => {\n if (left == null && right == null) return 0;\n if (left == null) return -1;\n if (right == null) return 1;\n if (typeof left === 'number' && typeof right === 'number')\n return left - right;\n if (left instanceof Date && right instanceof Date) {\n return left.getTime() - right.getTime();\n }\n return String(left).localeCompare(String(right));\n};\n\n/** Evaluates one `{ $op: value }` condition against a document field. */\nconst matchesCondition = (actual: unknown, condition: unknown): boolean => {\n if (!isPlainObject(condition)) {\n return actual === condition;\n }\n\n return Object.entries(condition).every(([operator, operand]) => {\n switch (operator) {\n case '$eq':\n return actual === operand;\n case '$ne':\n return actual !== operand;\n case '$gt':\n return compare(actual, operand) > 0;\n case '$gte':\n return compare(actual, operand) >= 0;\n case '$lt':\n return compare(actual, operand) < 0;\n case '$lte':\n return compare(actual, operand) <= 0;\n case '$in':\n return (operand as unknown[]).includes(actual);\n // Absence, not falsiness. A settlement ledger keys \"unpaid\" on a `runId`\n // that is not there at all, and `undefined` is what a document without\n // the field reads as — so `{ $exists: false }` must be a different test\n // from `{ $eq: undefined }` would be if the serializer ever wrote one.\n case '$exists':\n return (actual !== undefined) === (operand as boolean);\n case '$nin':\n return !(operand as unknown[]).includes(actual);\n case '$not':\n return !matchesCondition(actual, operand);\n case '$regex':\n return new RegExp(\n operand as string,\n (condition['$options'] as string) ?? '',\n ).test(String(actual));\n case '$options':\n // Handled together with `$regex`.\n return true;\n default:\n throw new TypeError(`fake mongo: unsupported operator \"${operator}\"`);\n }\n });\n};\n\n/**\n * Evaluates one node of an aggregation expression — the `$expr` half of a\n * query, which is a different language from the operators above: it compares\n * two *fields of the same document* rather than a field against a constant.\n *\n * Only the shape a conditional stock write sends is implemented — a\n * `$subtract` of two field paths compared with `$gte` — and anything else is a\n * `TypeError`. That is the same rule the rest of this file follows: a fake that\n * quietly evaluated an expression it does not understand would answer `true`\n * for a document the real server would have refused, which turns an\n * oversell-prevention test into a test that asserts nothing.\n */\nconst evaluateExpression = (doc: Document, node: unknown): unknown => {\n if (typeof node === 'string' && node.startsWith('$')) {\n return doc[node.slice(1)];\n }\n if (!isPlainObject(node)) {\n return node;\n }\n\n const entries = Object.entries(node);\n if (entries.length !== 1) {\n throw new TypeError(\n `fake mongo: $expr node must have exactly one operator, got ${entries.length}`,\n );\n }\n const [operator, operand] = entries[0];\n const operands = (operand as unknown[]).map(child =>\n evaluateExpression(doc, child),\n );\n\n switch (operator) {\n case '$subtract':\n return (operands[0] as number) - (operands[1] as number);\n case '$add':\n return (operands[0] as number) + (operands[1] as number);\n case '$gte':\n return compare(operands[0], operands[1]) >= 0;\n case '$gt':\n return compare(operands[0], operands[1]) > 0;\n case '$lte':\n return compare(operands[0], operands[1]) <= 0;\n case '$lt':\n return compare(operands[0], operands[1]) < 0;\n case '$eq':\n return operands[0] === operands[1];\n default:\n throw new TypeError(\n `fake mongo: unsupported $expr operator \"${operator}\"`,\n );\n }\n};\n\n/**\n * Every value a field path names, the way the server reads one.\n *\n * A dotted path descends, and **an array on the way down is walked\n * element-wise** — which is what makes `{'items.vendorId': x}` match an order\n * with a line for that vendor rather than nothing at all. Without it a query\n * into an embedded collection silently answers empty here while working against\n * a real server, which is worse than an unsupported-operator throw: the spec\n * passes and asserts the opposite of what it claims.\n *\n * The array itself is kept as a candidate beside its elements, because the\n * server matches both — `{ tags: 'a' }` matches `{ tags: ['a'] }` and\n * `{ tags: 'a' }` alike.\n */\nconst valuesAt = (doc: Document, field: string): unknown[] => {\n let current: unknown[] = [doc];\n for (const segment of field.split('.')) {\n const next: unknown[] = [];\n for (const value of current) {\n const container = Array.isArray(value) ? value : [value];\n for (const element of container) {\n if (isPlainObject(element)) {\n next.push(element[segment]);\n }\n }\n }\n current = next;\n }\n return current.flatMap(value =>\n Array.isArray(value) ? [value, ...value] : [value],\n );\n};\n\n/** Operators that exclude a document when a single value matches them. */\nconst NEGATIVE_OPERATORS = new Set(['$ne', '$nin', '$not']);\n\nconst isNegativeCondition = (condition: unknown): boolean =>\n isPlainObject(condition) &&\n Object.keys(condition).some(operator => NEGATIVE_OPERATORS.has(operator));\n\nconst matches = (doc: Document, query: QueryDocument): boolean =>\n Object.entries(query).every(([field, condition]) => {\n if (field === '$and') {\n return (condition as QueryDocument[]).every(child => matches(doc, child));\n }\n if (field === '$or') {\n return (condition as QueryDocument[]).some(child => matches(doc, child));\n }\n if (field === '$expr') {\n return evaluateExpression(doc, condition) === true;\n }\n const candidates = valuesAt(doc, field);\n if (candidates.length === 0) {\n return matchesCondition(undefined, condition);\n }\n // A positive condition matches when **any** value at the path satisfies it;\n // a negative one only when **every** value does. That asymmetry is the\n // server's: `{ 'items.vendorId': { $ne: 'a' } }` must exclude an order that\n // has a line for `a`, not include it because some other line differs.\n return isNegativeCondition(condition)\n ? candidates.every(value => matchesCondition(value, condition))\n : candidates.some(value => matchesCondition(value, condition));\n });\n\n/**\n * The error a real server raises when a write violates a unique index.\n *\n * The `code` matters and the message does not: callers recognise a duplicate\n * key by `error.code === 11000` (or the same code inside `writeErrors`), and a\n * fake that threw a plain `Error` would send a retry path that exists precisely\n * for this case down the \"unexpected failure\" branch instead.\n */\nclass FakeDuplicateKeyError extends Error {\n readonly code = 11000;\n\n constructor(collection: string, key: Document) {\n super(\n `fake mongo: E11000 duplicate key error collection: ${collection} dup key: ${JSON.stringify(key)}`,\n );\n this.name = 'MongoServerError';\n }\n}\n\nexport interface FakeMongoCollection {\n /** The documents currently stored, as the driver would return them. */\n readonly documents: Document[];\n}\n\nexport interface FakeMongoDb {\n /** Replaces a collection's contents. */\n seed(collection: string, documents: Document[]): void;\n /** Reads a collection's current contents. */\n read(collection: string): Document[];\n /**\n * Makes every subsequent driver call reject with `error`, so the adapter's\n * `EntifixConnError` mapping is reachable. Pass `undefined` to restore.\n */\n failWith(error: unknown): void;\n /**\n * Makes only `operation` reject, leaving the rest working — the way a read\n * that succeeds and a count that then fails reaches a distinct error branch.\n */\n failOn(operation: string, error: unknown): void;\n /** Every operation performed, for assertions about what the adapter did. */\n readonly operations: ReadonlyArray<{ collection: string; op: string }>;\n /**\n * Opens a session whose `withTransaction` rolls the whole store back when its\n * body throws. What a service hands to `client.startSession()`; see the\n * implementation for why it is atomicity and not isolation.\n */\n startSession(): {\n withTransaction(body: () => Promise<unknown>): Promise<unknown>;\n endSession(): Promise<void>;\n readonly hasEnded: boolean;\n };\n /** The object to hand to `makeMongoRepository` in place of a real `Db`. */\n readonly db: unknown;\n}\n\n/**\n * Builds an in-memory stand-in for a mongodb `Db`.\n *\n * `db` is typed `unknown` on purpose: casting it at the call site keeps this\n * package free of a hard dependency on the `mongodb` types while remaining\n * honest that it is not a real driver.\n */\nexport const makeFakeMongoDb = (\n seed: Record<string, Document[]> = {},\n): FakeMongoDb => {\n const collections = new Map<string, Document[]>(\n Object.entries(seed).map(([name, docs]) => [\n name,\n docs.map(d => ({ ...d })),\n ]),\n );\n const operations: Array<{ collection: string; op: string }> = [];\n let failure: unknown;\n const failuresByOperation = new Map<string, unknown>();\n\n const documentsOf = (name: string): Document[] => {\n if (!collections.has(name)) collections.set(name, []);\n return collections.get(name) as Document[];\n };\n\n /**\n * The unique indexes `createIndex` has been asked for, per collection, as the\n * field lists they key on.\n *\n * Recording them is not bookkeeping: `updateOne` with `upsert` is atomic per\n * *document*, so two concurrent upserts for the same key each fail to see the\n * other's uncommitted insert and each create a row. The index is what makes\n * the fold singular, and a fake that accepted `createIndex` as a no-op would\n * report a service as covered while the property the index exists for went\n * untested (ADR 0010).\n */\n const uniqueIndexes = new Map<string, string[][]>();\n\n const uniqueIndexesOf = (name: string): string[][] =>\n uniqueIndexes.get(name) ?? [];\n\n /**\n * Refuses `candidate` if it would duplicate an existing document under one of\n * the collection's unique indexes.\n *\n * `ignored` is the document being replaced by an update, which must not count\n * as its own duplicate. A key whose value is `undefined` on the candidate is\n * skipped: a real index treats a missing field as `null` and would collide,\n * but nothing in this repo indexes an optional member, and guessing wrong\n * here would fail writes the server accepts.\n */\n const assertUnique = (\n name: string,\n candidate: Document,\n ignored?: Document,\n ): void => {\n for (const fields of uniqueIndexesOf(name)) {\n if (fields.some(field => candidate[field] === undefined)) continue;\n const clash = documentsOf(name).find(\n doc =>\n doc !== ignored &&\n fields.every(field => doc[field] === candidate[field]),\n );\n if (clash !== undefined) {\n throw new FakeDuplicateKeyError(\n name,\n Object.fromEntries(fields.map(field => [field, candidate[field]])),\n );\n }\n }\n };\n\n const record = <TValue>(name: string, op: string, produce: () => TValue) => {\n operations.push({ collection: name, op });\n const scoped = failuresByOperation.get(op);\n if (scoped !== undefined) return Promise.reject(scoped);\n if (failure !== undefined) return Promise.reject(failure);\n // `try`/`catch` rather than `Promise.resolve(produce())`: a real driver\n // returns a promise and *rejects*, so a fake that threw synchronously would\n // need different handling at every call site than the thing it stands in for.\n try {\n return Promise.resolve(produce());\n } catch (error) {\n return Promise.reject(error);\n }\n };\n\n /** Applies `{ projection: { _id: 0 } }`, the only projection the adapter uses. */\n const project = (doc: Document, options?: { projection?: Document }) => {\n const copy = { ...doc };\n if (options?.projection && options.projection['_id'] === 0) {\n delete copy['_id'];\n }\n return copy;\n };\n\n const collection = (name: string) => ({\n find: (query: QueryDocument = {}, options?: { projection?: Document }) => {\n let sortSpec: Record<string, 1 | -1> = {};\n let skipCount = 0;\n let limitCount = Infinity;\n\n const cursor = {\n sort(spec: Record<string, 1 | -1>) {\n sortSpec = spec;\n return cursor;\n },\n skip(count: number) {\n skipCount = count;\n return cursor;\n },\n limit(count: number) {\n limitCount = count;\n return cursor;\n },\n toArray: () =>\n record(name, 'find', () => {\n const matched = documentsOf(name).filter(doc =>\n matches(doc, query),\n );\n const sorted = Object.entries(sortSpec).reduceRight(\n (items, [field, direction]) =>\n [...items].sort(\n (left, right) =>\n compare(left[field], right[field]) * direction,\n ),\n matched,\n );\n return sorted\n .slice(skipCount, skipCount + limitCount)\n .map(doc => project(doc, options));\n }),\n };\n return cursor;\n },\n\n findOne: (query: QueryDocument, options?: { projection?: Document }) =>\n record(name, 'findOne', () => {\n const found = documentsOf(name).find(doc => matches(doc, query));\n return found === undefined ? null : project(found, options);\n }),\n\n countDocuments: (query: QueryDocument = {}) =>\n record(\n name,\n 'countDocuments',\n () => documentsOf(name).filter(doc => matches(doc, query)).length,\n ),\n\n // Used by the services' own seeding, which the e2e `mock` profile runs for\n // real so both profiles serve the same catalog.\n insertMany: (documents: Document[]) =>\n record(name, 'insertMany', () => {\n const stored = documentsOf(name);\n for (const document of documents) {\n assertUnique(name, document);\n stored.push({ ...document });\n }\n return { insertedCount: documents.length };\n }),\n\n /**\n * A single insert — how an append-only ledger grows, and how a hold is\n * written beside the counter it moved.\n *\n * The options argument is accepted and ignored except for its `session`,\n * which the transaction wrapper already handles: a caller that passes one\n * is inside `withTransaction`, and this fake's rollback is a snapshot of\n * the whole store rather than anything per-operation.\n */\n insertOne: (document: Document) =>\n record(name, 'insertOne', () => {\n assertUnique(name, document);\n documentsOf(name).push({ ...document });\n return { acknowledged: true, insertedId: document['id'] ?? null };\n }),\n\n replaceOne: (\n query: QueryDocument,\n replacement: Document,\n options?: { upsert?: boolean },\n ) =>\n record(name, 'replaceOne', () => {\n const documents = documentsOf(name);\n const index = documents.findIndex(doc => matches(doc, query));\n if (index === -1) {\n if (!options?.upsert) return { matchedCount: 0, upsertedCount: 0 };\n documents.push({ ...replacement });\n return { matchedCount: 0, upsertedCount: 1 };\n }\n documents[index] = { ...replacement };\n return { matchedCount: 1, upsertedCount: 0 };\n }),\n\n /**\n * Partial update. `$set`, `$addToSet`, `$inc` and `$setOnInsert` are\n * implemented — the operators the adapters and the stock ledger use — and\n * **anything else throws**, because a fake that silently accepted `$unset`\n * without applying it would let a spec pass on an update that never\n * happened.\n *\n * `$addToSet` is what links an identifier onto `user-identity.identifiers`\n * without duplicating it on a repair run; supporting it here rather than\n * ignoring it is the difference between the e2e proving the link and merely\n * not noticing its absence.\n *\n * `$inc` with `upsert` is how a quantity moves. It matters that it is a\n * real increment and not a read-modify-write even here: the whole rule it\n * implements is that an absolute-value write loses updates, so a fake that\n * computed the new total from a value it had already read would model the\n * bug rather than the fix.\n */\n updateOne: (\n query: QueryDocument,\n update: {\n $set?: Document;\n $addToSet?: Document;\n $inc?: Document;\n $setOnInsert?: Document;\n },\n options?: { upsert?: boolean },\n ) =>\n record(name, 'updateOne', () => {\n const supported = ['$set', '$addToSet', '$inc', '$setOnInsert'];\n const unsupported = Object.keys(update).filter(\n operator => !supported.includes(operator),\n );\n if (unsupported.length > 0) {\n throw new Error(\n `fake-mongo: updateOne does not implement ${unsupported.join(', ')}`,\n );\n }\n\n // A real server rejects an update whose operators both name one field,\n // and the message is the one this repro'd against: `$setOnInsert` that\n // also named a field `$inc` touches. Reproducing the refusal is what\n // keeps the fake from blessing an update Mongo would refuse.\n const conflicting = Object.keys(update.$setOnInsert ?? {}).filter(\n field => field in (update.$inc ?? {}) || field in (update.$set ?? {}),\n );\n if (conflicting.length > 0) {\n throw new Error(\n `fake-mongo: updateOne has conflicting paths in $setOnInsert and $inc/$set: ${conflicting.join(', ')}`,\n );\n }\n\n /** `$set`, `$addToSet` and `$inc`, which apply on both branches. */\n const applyOperators = (base: Document): Document => {\n const updated = { ...base, ...(update.$set ?? {}) };\n for (const [field, value] of Object.entries(update.$addToSet ?? {})) {\n const current = updated[field];\n const array = Array.isArray(current) ? current : [];\n if (!array.includes(value)) {\n updated[field] = [...array, value];\n }\n }\n for (const [field, delta] of Object.entries(update.$inc ?? {})) {\n // An absent field is created at the increment's value, which is\n // exactly why `$setOnInsert` must not also name it.\n const current = updated[field];\n updated[field] =\n (typeof current === 'number' ? current : 0) + (delta as number);\n }\n return updated;\n };\n\n const documents = documentsOf(name);\n const index = documents.findIndex(doc => matches(doc, query));\n\n if (index === -1) {\n if (options?.upsert !== true) {\n return { matchedCount: 0, modifiedCount: 0, upsertedCount: 0 };\n }\n // The upserted document starts from the query's *equality* fields,\n // the way a real server seeds one — an operator condition contributes\n // nothing, which is why a `$expr`-guarded upsert would insert a row\n // missing the very field it filtered on.\n const seedFields = Object.fromEntries(\n Object.entries(query).filter(\n ([field, condition]) =>\n !field.startsWith('$') && !isPlainObject(condition),\n ),\n );\n const inserted = applyOperators({\n ...seedFields,\n ...(update.$setOnInsert ?? {}),\n });\n assertUnique(name, inserted);\n documents.push(inserted);\n return { matchedCount: 0, modifiedCount: 0, upsertedCount: 1 };\n }\n\n const updated = applyOperators(documents[index]);\n assertUnique(name, updated, documents[index]);\n documents[index] = updated;\n return { matchedCount: 1, modifiedCount: 1, upsertedCount: 0 };\n }),\n\n /**\n * `$set` across every matching document, and the count of what changed.\n *\n * ⚠️ **The count is what a caller branches on.** A settlement run claims its\n * ledger lines with this and reads `modifiedCount === 0` as \"another replica\n * took them all\" — so a fake returning a fixed `1`, or the match count\n * rather than the modified count, would make the losing replica behave like\n * the winning one and the conditional claim would test nothing.\n *\n * `$set` only. Anything else throws, for the reason `updateOne` gives:\n * silently accepting an operator it does not apply lets a spec pass on an\n * update that never happened.\n */\n updateMany: (query: QueryDocument, update: { $set?: Document }) =>\n record(name, 'updateMany', () => {\n const unsupported = Object.keys(update).filter(\n operator => operator !== '$set',\n );\n if (unsupported.length > 0) {\n throw new Error(\n `fake-mongo: updateMany does not implement ${unsupported.join(', ')}`,\n );\n }\n\n const documents = documentsOf(name);\n let modifiedCount = 0;\n documents.forEach((doc, index) => {\n if (!matches(doc, query)) return;\n const updated = { ...doc, ...(update.$set ?? {}) };\n assertUnique(name, updated, doc);\n documents[index] = updated;\n modifiedCount += 1;\n });\n return { matchedCount: modifiedCount, modifiedCount };\n }),\n\n /**\n * Update one document and hand back the result, upserting when asked.\n *\n * ⚠️ **`returnDocument: 'after'` is the only mode implemented, and the\n * default here is `'after'` rather than the driver's `'before'`** — because\n * the callers that need this primitive at all need the merged document. A\n * two-message join upserts its own half and then asks \"is the pair complete\n * now?\", which is a question only the *after* state answers. `'before'`\n * throws rather than quietly returning the wrong half.\n *\n * Implemented over {@link updateOne} so the upsert seeding rule, the\n * conflicting-path refusal and the unique-index enforcement are the same\n * ones, rather than a second copy that could drift from them.\n */\n findOneAndUpdate: (\n query: QueryDocument,\n update: {\n $set?: Document;\n $addToSet?: Document;\n $inc?: Document;\n $setOnInsert?: Document;\n },\n options?: { upsert?: boolean; returnDocument?: 'before' | 'after' },\n ) =>\n record(name, 'findOneAndUpdate', async () => {\n if (options?.returnDocument === 'before') {\n throw new Error(\n 'fake-mongo: findOneAndUpdate only implements returnDocument: \"after\"',\n );\n }\n // Where the document is *before* the write, because afterwards the\n // query may no longer find it: an upsert's operator conditions need not\n // hold of the row it just inserted — `{ folded: { $exists: false } }`\n // matches nothing once `folded` has been set. `documentsOf` hands back\n // the live array, which `updateOne` mutates in place or pushes onto, so\n // the position is enough to identify it either way.\n const documents = documentsOf(name);\n const index = documents.findIndex(doc => matches(doc, query));\n\n const result = await collection(name).updateOne(query, update, {\n upsert: options?.upsert === true,\n });\n if (result.matchedCount === 0 && result.upsertedCount === 0) {\n return null;\n }\n\n return index === -1\n ? documents[documents.length - 1]\n : documents[index];\n }),\n\n deleteOne: (query: QueryDocument) =>\n record(name, 'deleteOne', () => {\n const documents = documentsOf(name);\n const index = documents.findIndex(doc => matches(doc, query));\n if (index === -1) return { deletedCount: 0 };\n documents.splice(index, 1);\n return { deletedCount: 1 };\n }),\n\n /**\n * Declares an index, and **enforces it when it is unique**.\n *\n * Idempotent, like the real one: a service that ensures its index per\n * request rather than at boot — because a tenant database only appears on\n * first write — calls this on every write.\n */\n createIndex: (spec: Document, options?: Document) =>\n record(name, 'createIndex', () => {\n if (options?.['unique'] === true) {\n const fields = Object.keys(spec);\n const existing = uniqueIndexes.get(name) ?? [];\n const already = existing.some(\n candidate =>\n candidate.length === fields.length &&\n candidate.every((field, at) => field === fields[at]),\n );\n if (!already) {\n uniqueIndexes.set(name, [...existing, fields]);\n }\n }\n return { spec, options };\n }),\n });\n\n /**\n * A session whose `withTransaction` really rolls back.\n *\n * ⚠️ **It is atomicity, not isolation, and the difference decides where a\n * test may live.** The store is a single-threaded object graph, so nothing\n * runs concurrently with the body and a \"race\" here resolves in the order it\n * was written: a concurrency property — two writers contending for the last\n * unit — is green under this fake no matter what the code does, and belongs\n * in a `*.live.spec.ts` against real Mongo (ADR 0010).\n *\n * What it *does* model is the abort path, which is worth having: a body that\n * throws leaves the store exactly as it found it, so a conditional write that\n * moves a counter and then fails to write its row cannot leave the counter\n * moved. That is a real bug shape, and one a fake without rollback would hide\n * by committing half of it.\n */\n const startSession = () => {\n let open = true;\n return {\n withTransaction: async (body: () => Promise<unknown>) => {\n const snapshot = new Map(\n [...collections].map(([name, docs]) => [\n name,\n docs.map(doc => ({ ...doc })),\n ]),\n );\n try {\n return await body();\n } catch (error) {\n collections.clear();\n for (const [name, docs] of snapshot) collections.set(name, docs);\n throw error;\n }\n },\n endSession: () => {\n open = false;\n return Promise.resolve();\n },\n get hasEnded() {\n return !open;\n },\n };\n };\n\n return {\n seed: (name, documents) => {\n collections.set(\n name,\n documents.map(doc => ({ ...doc })),\n );\n },\n read: name => documentsOf(name).map(doc => ({ ...doc })),\n failWith: error => {\n failure = error;\n },\n failOn: (operation, error) => {\n failuresByOperation.set(operation, error);\n },\n get operations() {\n return operations;\n },\n startSession,\n db: { collection },\n };\n};\n"],"names":["isPlainObject","value","Array","isArray","compare","left","right","Date","getTime","String","localeCompare","matchesCondition","actual","condition","Object","entries","every","operator","operand","includes","undefined","RegExp","test","TypeError","evaluateExpression","doc","node","startsWith","slice","length","operands","map","child","valuesAt","field","current","segment","split","next","container","element","push","flatMap","NEGATIVE_OPERATORS","Set","isNegativeCondition","keys","some","has","matches","query","candidates","FakeDuplicateKeyError","Error","collection","key","JSON","stringify","code","name","makeFakeMongoDb","seed","collections","Map","docs","d","operations","failure","failuresByOperation","documentsOf","set","get","uniqueIndexes","uniqueIndexesOf","assertUnique","candidate","ignored","fields","clash","find","fromEntries","record","op","produce","scoped","Promise","reject","resolve","error","project","options","copy","projection","sortSpec","skipCount","limitCount","Infinity","cursor","sort","spec","skip","count","limit","toArray","matched","filter","sorted","reduceRight","items","direction","findOne","found","countDocuments","insertMany","documents","stored","document","insertedCount","insertOne","acknowledged","insertedId","replaceOne","replacement","index","findIndex","upsert","matchedCount","upsertedCount","updateOne","update","supported","unsupported","join","conflicting","$setOnInsert","$inc","$set","applyOperators","base","updated","$addToSet","array","delta","modifiedCount","seedFields","inserted","updateMany","forEach","findOneAndUpdate","returnDocument","result","deleteOne","deletedCount","splice","createIndex","existing","already","at","startSession","open","withTransaction","body","snapshot","clear","endSession","hasEnded","read","failWith","failOn","operation","db"],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * A fake of the ioredis client, not of the lock/sequence ports.
3
+ *
4
+ * `makeRedisLockService`, `makeRedisSequenceService`, the session store, the
5
+ * one-time token store and the attempt limiter all run unchanged on top of it,
6
+ * so their real behaviour — `SET NX PX`, the bounded retry loop, the
7
+ * compare-and-delete release script, `INCR`, `GETDEL` — is what the specs
8
+ * exercise.
9
+ *
10
+ * Time does not pass: `EX`/`EXPIRE` are remembered so `TTL` can report them, but
11
+ * nothing here expires on its own. Adapters that must be tested across an expiry
12
+ * drive the clock themselves.
13
+ */
14
+ export interface FakeRedis {
15
+ /** Current keys and values, for assertions about what the adapter wrote. */
16
+ read(key: string): string | undefined;
17
+ /** Pre-set a key so the next `SET NX` finds it taken — i.e. lock contention. */
18
+ hold(key: string, token?: string): void;
19
+ /** Drop a key, releasing contention. */
20
+ free(key: string): void;
21
+ /** Makes every subsequent command reject with `error`. */
22
+ failWith(error: unknown): void;
23
+ /** Every command issued, in order. */
24
+ readonly commands: ReadonlyArray<{
25
+ command: string;
26
+ args: unknown[];
27
+ }>;
28
+ /** The object to pass where an ioredis `Redis` is expected. */
29
+ readonly redis: unknown;
30
+ }
31
+ export declare const makeFakeRedis: () => FakeRedis;
32
+ //# sourceMappingURL=fake-redis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake-redis.d.ts","sourceRoot":"","sources":["../../src/drivers/fake-redis.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,SAAS;IACxB,4EAA4E;IAC5E,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACtC,gFAAgF;IAChF,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,wCAAwC;IACxC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,0DAA0D;IAC1D,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B,sCAAsC;IACtC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC,CAAC;IACvE,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,eAAO,MAAM,aAAa,QAAO,SAiJhC,CAAC"}
@@ -0,0 +1,164 @@
1
+ /**
2
+ * A fake of the ioredis client, not of the lock/sequence ports.
3
+ *
4
+ * `makeRedisLockService`, `makeRedisSequenceService`, the session store, the
5
+ * one-time token store and the attempt limiter all run unchanged on top of it,
6
+ * so their real behaviour — `SET NX PX`, the bounded retry loop, the
7
+ * compare-and-delete release script, `INCR`, `GETDEL` — is what the specs
8
+ * exercise.
9
+ *
10
+ * Time does not pass: `EX`/`EXPIRE` are remembered so `TTL` can report them, but
11
+ * nothing here expires on its own. Adapters that must be tested across an expiry
12
+ * drive the clock themselves.
13
+ */ export const makeFakeRedis = ()=>{
14
+ const store = new Map();
15
+ const sets = new Map();
16
+ /** Seconds last requested per key, so `TTL` can answer something plausible. */ const ttls = new Map();
17
+ const commands = [];
18
+ let failure;
19
+ const record = (command, args, produce)=>{
20
+ commands.push({
21
+ command,
22
+ args
23
+ });
24
+ return failure !== undefined ? Promise.reject(failure) : Promise.resolve(produce());
25
+ };
26
+ const redis = {
27
+ /**
28
+ * Supports the one form the lock adapter issues:
29
+ * `set(key, token, 'PX', ttl, 'NX')`. `NX` is what makes acquisition
30
+ * atomic, so honouring it is the whole point of this fake.
31
+ */ set: (key, value, ...rest)=>record('set', [
32
+ key,
33
+ value,
34
+ ...rest
35
+ ], ()=>{
36
+ const nx = rest.some((argument)=>String(argument).toUpperCase() === 'NX');
37
+ if (nx && store.has(key)) return null;
38
+ store.set(key, value);
39
+ // Remember an `EX <seconds>` so a later `TTL` is not a lie.
40
+ const exIndex = rest.findIndex((argument)=>String(argument).toUpperCase() === 'EX');
41
+ if (exIndex !== -1) ttls.set(key, Number(rest[exIndex + 1]));
42
+ return 'OK';
43
+ }),
44
+ get: (key)=>record('get', [
45
+ key
46
+ ], ()=>{
47
+ var _store_get;
48
+ return (_store_get = store.get(key)) != null ? _store_get : null;
49
+ }),
50
+ /**
51
+ * `TTL`: seconds remaining, `-2` when the key is gone and `-1` when it has
52
+ * no expiry. Time does not actually pass here, so this reports whatever TTL
53
+ * was last set — enough for adapters that branch on "is it still alive".
54
+ */ ttl: (key)=>record('ttl', [
55
+ key
56
+ ], ()=>{
57
+ var _ttls_get;
58
+ if (!store.has(key) && !sets.has(key)) return -2;
59
+ return (_ttls_get = ttls.get(key)) != null ? _ttls_get : -1;
60
+ }),
61
+ /** `GETDEL`: read and remove atomically — a single-use token redemption. */ getdel: (key)=>record('getdel', [
62
+ key
63
+ ], ()=>{
64
+ var _store_get;
65
+ const value = (_store_get = store.get(key)) != null ? _store_get : null;
66
+ store.delete(key);
67
+ ttls.delete(key);
68
+ return value;
69
+ }),
70
+ /** `SCARD`: how many members a set holds. */ scard: (key)=>record('scard', [
71
+ key
72
+ ], ()=>{
73
+ var _ref;
74
+ var _sets_get;
75
+ return (_ref = (_sets_get = sets.get(key)) == null ? void 0 : _sets_get.size) != null ? _ref : 0;
76
+ }),
77
+ del: (key)=>record('del', [
78
+ key
79
+ ], ()=>{
80
+ // A key can hold a string or a set; drop whichever is there so the
81
+ // session store's `del` frees both record keys and index-set keys.
82
+ const hadString = store.delete(key);
83
+ const hadSet = sets.delete(key);
84
+ return hadString || hadSet ? 1 : 0;
85
+ }),
86
+ /** `SADD`: add a member to a set, returning how many were newly added (0/1). */ sadd: (key, member)=>record('sadd', [
87
+ key,
88
+ member
89
+ ], ()=>{
90
+ var _sets_get;
91
+ const set = (_sets_get = sets.get(key)) != null ? _sets_get : new Set();
92
+ const isNew = !set.has(member);
93
+ set.add(member);
94
+ sets.set(key, set);
95
+ return isNew ? 1 : 0;
96
+ }),
97
+ /** `SREM`: remove a member, returning how many were removed (0/1). */ srem: (key, member)=>record('srem', [
98
+ key,
99
+ member
100
+ ], ()=>{
101
+ var _sets_get;
102
+ return ((_sets_get = sets.get(key)) == null ? void 0 : _sets_get.delete(member)) ? 1 : 0;
103
+ }),
104
+ /** `SMEMBERS`: the set's members, or an empty array for a missing set. */ smembers: (key)=>record('smembers', [
105
+ key
106
+ ], ()=>{
107
+ var _sets_get;
108
+ return [
109
+ ...(_sets_get = sets.get(key)) != null ? _sets_get : []
110
+ ];
111
+ }),
112
+ /**
113
+ * `EXPIRE`: TTL is not enforced by this fake, so it only reports whether the
114
+ * key exists (1) or not (0) — which is the branch the session adapter reads
115
+ * to detect a vanished session on `touch`.
116
+ */ expire: (key, seconds)=>record('expire', [
117
+ key,
118
+ seconds
119
+ ], ()=>{
120
+ if (!store.has(key) && !sets.has(key)) return 0;
121
+ ttls.set(key, seconds);
122
+ return 1;
123
+ }),
124
+ incr: (key)=>record('incr', [
125
+ key
126
+ ], ()=>{
127
+ var _store_get;
128
+ const next = Number((_store_get = store.get(key)) != null ? _store_get : '0') + 1;
129
+ store.set(key, String(next));
130
+ return next;
131
+ }),
132
+ /**
133
+ * The release script is a compare-and-delete: free the key only when it
134
+ * still holds our own token. Rather than interpreting Lua, this reproduces
135
+ * that one script's semantics.
136
+ */ eval: (_script, _numKeys, key, token)=>record('eval', [
137
+ key,
138
+ token
139
+ ], ()=>{
140
+ if (store.get(key) !== token) return 0;
141
+ store.delete(key);
142
+ return 1;
143
+ }),
144
+ quit: ()=>record('quit', [], ()=>'OK')
145
+ };
146
+ return {
147
+ read: (key)=>store.get(key),
148
+ hold: (key, token = 'someone-else')=>{
149
+ store.set(key, token);
150
+ },
151
+ free: (key)=>{
152
+ store.delete(key);
153
+ },
154
+ failWith: (error)=>{
155
+ failure = error;
156
+ },
157
+ get commands () {
158
+ return commands;
159
+ },
160
+ redis
161
+ };
162
+ };
163
+
164
+ //# sourceMappingURL=fake-redis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/drivers/fake-redis.ts"],"sourcesContent":["/**\n * A fake of the ioredis client, not of the lock/sequence ports.\n *\n * `makeRedisLockService`, `makeRedisSequenceService`, the session store, the\n * one-time token store and the attempt limiter all run unchanged on top of it,\n * so their real behaviour — `SET NX PX`, the bounded retry loop, the\n * compare-and-delete release script, `INCR`, `GETDEL` — is what the specs\n * exercise.\n *\n * Time does not pass: `EX`/`EXPIRE` are remembered so `TTL` can report them, but\n * nothing here expires on its own. Adapters that must be tested across an expiry\n * drive the clock themselves.\n */\n\nexport interface FakeRedis {\n /** Current keys and values, for assertions about what the adapter wrote. */\n read(key: string): string | undefined;\n /** Pre-set a key so the next `SET NX` finds it taken — i.e. lock contention. */\n hold(key: string, token?: string): void;\n /** Drop a key, releasing contention. */\n free(key: string): void;\n /** Makes every subsequent command reject with `error`. */\n failWith(error: unknown): void;\n /** Every command issued, in order. */\n readonly commands: ReadonlyArray<{ command: string; args: unknown[] }>;\n /** The object to pass where an ioredis `Redis` is expected. */\n readonly redis: unknown;\n}\n\nexport const makeFakeRedis = (): FakeRedis => {\n const store = new Map<string, string>();\n const sets = new Map<string, Set<string>>();\n /** Seconds last requested per key, so `TTL` can answer something plausible. */\n const ttls = new Map<string, number>();\n const commands: Array<{ command: string; args: unknown[] }> = [];\n let failure: unknown;\n\n const record = <TValue>(\n command: string,\n args: unknown[],\n produce: () => TValue,\n ): Promise<TValue> => {\n commands.push({ command, args });\n return failure !== undefined\n ? Promise.reject(failure)\n : Promise.resolve(produce());\n };\n\n const redis = {\n /**\n * Supports the one form the lock adapter issues:\n * `set(key, token, 'PX', ttl, 'NX')`. `NX` is what makes acquisition\n * atomic, so honouring it is the whole point of this fake.\n */\n set: (key: string, value: string, ...rest: unknown[]) =>\n record('set', [key, value, ...rest], () => {\n const nx = rest.some(\n argument => String(argument).toUpperCase() === 'NX',\n );\n if (nx && store.has(key)) return null;\n store.set(key, value);\n // Remember an `EX <seconds>` so a later `TTL` is not a lie.\n const exIndex = rest.findIndex(\n argument => String(argument).toUpperCase() === 'EX',\n );\n if (exIndex !== -1) ttls.set(key, Number(rest[exIndex + 1]));\n return 'OK';\n }),\n\n get: (key: string) => record('get', [key], () => store.get(key) ?? null),\n\n /**\n * `TTL`: seconds remaining, `-2` when the key is gone and `-1` when it has\n * no expiry. Time does not actually pass here, so this reports whatever TTL\n * was last set — enough for adapters that branch on \"is it still alive\".\n */\n ttl: (key: string) =>\n record('ttl', [key], () => {\n if (!store.has(key) && !sets.has(key)) return -2;\n return ttls.get(key) ?? -1;\n }),\n\n /** `GETDEL`: read and remove atomically — a single-use token redemption. */\n getdel: (key: string) =>\n record('getdel', [key], () => {\n const value = store.get(key) ?? null;\n store.delete(key);\n ttls.delete(key);\n return value;\n }),\n\n /** `SCARD`: how many members a set holds. */\n scard: (key: string) =>\n record('scard', [key], () => sets.get(key)?.size ?? 0),\n\n del: (key: string) =>\n record('del', [key], () => {\n // A key can hold a string or a set; drop whichever is there so the\n // session store's `del` frees both record keys and index-set keys.\n const hadString = store.delete(key);\n const hadSet = sets.delete(key);\n return hadString || hadSet ? 1 : 0;\n }),\n\n /** `SADD`: add a member to a set, returning how many were newly added (0/1). */\n sadd: (key: string, member: string) =>\n record('sadd', [key, member], () => {\n const set = sets.get(key) ?? new Set<string>();\n const isNew = !set.has(member);\n set.add(member);\n sets.set(key, set);\n return isNew ? 1 : 0;\n }),\n\n /** `SREM`: remove a member, returning how many were removed (0/1). */\n srem: (key: string, member: string) =>\n record('srem', [key, member], () =>\n sets.get(key)?.delete(member) ? 1 : 0,\n ),\n\n /** `SMEMBERS`: the set's members, or an empty array for a missing set. */\n smembers: (key: string) =>\n record('smembers', [key], () => [...(sets.get(key) ?? [])]),\n\n /**\n * `EXPIRE`: TTL is not enforced by this fake, so it only reports whether the\n * key exists (1) or not (0) — which is the branch the session adapter reads\n * to detect a vanished session on `touch`.\n */\n expire: (key: string, seconds: number) =>\n record('expire', [key, seconds], () => {\n if (!store.has(key) && !sets.has(key)) return 0;\n ttls.set(key, seconds);\n return 1;\n }),\n\n incr: (key: string) =>\n record('incr', [key], () => {\n const next = Number(store.get(key) ?? '0') + 1;\n store.set(key, String(next));\n return next;\n }),\n\n /**\n * The release script is a compare-and-delete: free the key only when it\n * still holds our own token. Rather than interpreting Lua, this reproduces\n * that one script's semantics.\n */\n eval: (_script: string, _numKeys: number, key: string, token: string) =>\n record('eval', [key, token], () => {\n if (store.get(key) !== token) return 0;\n store.delete(key);\n return 1;\n }),\n\n quit: () => record('quit', [], () => 'OK'),\n };\n\n return {\n read: key => store.get(key),\n hold: (key, token = 'someone-else') => {\n store.set(key, token);\n },\n free: key => {\n store.delete(key);\n },\n failWith: error => {\n failure = error;\n },\n get commands() {\n return commands;\n },\n redis,\n };\n};\n"],"names":["makeFakeRedis","store","Map","sets","ttls","commands","failure","record","command","args","produce","push","undefined","Promise","reject","resolve","redis","set","key","value","rest","nx","some","argument","String","toUpperCase","has","exIndex","findIndex","Number","get","ttl","getdel","delete","scard","size","del","hadString","hadSet","sadd","member","Set","isNew","add","srem","smembers","expire","seconds","incr","next","eval","_script","_numKeys","token","quit","read","hold","free","failWith","error"],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * A fake of the `@effect/sql` driver, not of the repository port.
3
+ *
4
+ * It sits one level *below* `makeSqlRepository`, so the adapter's own code — the
5
+ * filter translation, the clause assembly, the upsert rule, the error mapping —
6
+ * actually runs and is measured by coverage. A fake at the port level would skip
7
+ * all of it. Same reasoning as {@link makeFakeMongoDb}.
8
+ *
9
+ * What it does **not** do is execute SQL: it hands back canned rows and records
10
+ * the statement each call compiled to, so a spec asserts on the SQL and
11
+ * parameters the adapter produced. Running the shared
12
+ * `describeEntityRepositoryContract` needs a real engine and belongs against real
13
+ * Postgres, the way the driver connection Layers are covered.
14
+ */
15
+ /** One statement the adapter executed, already compiled. */
16
+ export interface FakeSqlExecution {
17
+ /** The compiled SQL text, parameters replaced by `$1`, `$2`, … */
18
+ readonly sql: string;
19
+ readonly params: ReadonlyArray<unknown>;
20
+ }
21
+ /** Rows to answer with when a statement's SQL contains `match`. */
22
+ export interface FakeSqlResponse {
23
+ readonly match: string;
24
+ readonly rows: ReadonlyArray<Record<string, unknown>>;
25
+ }
26
+ export interface FakeSqlClient {
27
+ /**
28
+ * Answers `rows` for any statement whose compiled SQL contains `match`. Later
29
+ * registrations win, so a spec can override a default set up by a helper.
30
+ */
31
+ respondTo(match: string, rows: ReadonlyArray<Record<string, unknown>>): void;
32
+ /**
33
+ * Makes every subsequent execution fail, so the adapter's `EntifixConnError`
34
+ * mapping is reachable.
35
+ */
36
+ failWith(message: string): void;
37
+ /**
38
+ * Makes only the statements containing `match` fail, leaving the rest working
39
+ * — the way a select that succeeds and a count that then fails reaches a
40
+ * distinct error branch.
41
+ */
42
+ failOn(match: string, message: string): void;
43
+ /** Every statement executed, in order, for assertions about what ran. */
44
+ readonly executions: ReadonlyArray<FakeSqlExecution>;
45
+ /** The most recently executed statement. */
46
+ readonly lastExecution: FakeSqlExecution | undefined;
47
+ /**
48
+ * The object to hand to `makeSqlRepository` in place of a real `SqlClient`.
49
+ *
50
+ * Typed `unknown` on purpose — casting at the call site keeps this fake honest
51
+ * that it implements only the constructor surface the adapters use, not the
52
+ * whole `SqlClient` (no transactions, no reactivity, no streaming).
53
+ */
54
+ readonly client: unknown;
55
+ }
56
+ /** Builds an in-memory stand-in for an `@effect/sql` client. */
57
+ export declare const makeFakeSqlClient: (seed?: ReadonlyArray<FakeSqlResponse>) => FakeSqlClient;
58
+ //# sourceMappingURL=fake-sql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake-sql.d.ts","sourceRoot":"","sources":["../../src/drivers/fake-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,4DAA4D;AAC5D,MAAM,WAAW,gBAAgB;IAC/B,kEAAkE;IAClE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;CACzC;AAED,mEAAmE;AACnE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAC7E;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IACrD,4CAA4C;IAC5C,QAAQ,CAAC,aAAa,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACrD;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAuBD,gEAAgE;AAChE,eAAO,MAAM,iBAAiB,GAC5B,OAAM,aAAa,CAAC,eAAe,CAAM,KACxC,aAiEF,CAAC"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * A fake of the `@effect/sql` driver, not of the repository port.
3
+ *
4
+ * It sits one level *below* `makeSqlRepository`, so the adapter's own code — the
5
+ * filter translation, the clause assembly, the upsert rule, the error mapping —
6
+ * actually runs and is measured by coverage. A fake at the port level would skip
7
+ * all of it. Same reasoning as {@link makeFakeMongoDb}.
8
+ *
9
+ * What it does **not** do is execute SQL: it hands back canned rows and records
10
+ * the statement each call compiled to, so a spec asserts on the SQL and
11
+ * parameters the adapter produced. Running the shared
12
+ * `describeEntityRepositoryContract` needs a real engine and belongs against real
13
+ * Postgres, the way the driver connection Layers are covered.
14
+ */ import { SqlError } from '@effect/sql';
15
+ import * as Statement from '@effect/sql/Statement';
16
+ import { Effect } from 'effect';
17
+ const unsupported = (name)=>{
18
+ throw new TypeError(`fake sql: ${name} is not implemented`);
19
+ };
20
+ /**
21
+ * The Postgres compiler's configuration, mirrored from `@effect/sql-pg` so the
22
+ * fake emits exactly the dialect production does — `$n` placeholders and
23
+ * double-quoted identifiers — without this package depending on the `pg` driver.
24
+ *
25
+ * `sql.updateValues` and the `PgJson` custom segment are deliberate `TypeError`s
26
+ * rather than approximations: no adapter uses either, and a compiler that
27
+ * half-emitted them would let a spec pass on SQL that real Postgres rejects.
28
+ */ const pgCompiler = Statement.makeCompiler({
29
+ dialect: 'pg',
30
+ placeholder: (index)=>`$${index}`,
31
+ onIdentifier: Statement.defaultEscape('"'),
32
+ onRecordUpdate: ()=>unsupported('updateValues'),
33
+ onCustom: ()=>unsupported('custom segments')
34
+ });
35
+ /** Builds an in-memory stand-in for an `@effect/sql` client. */ export const makeFakeSqlClient = (seed = [])=>{
36
+ const responses = [
37
+ ...seed
38
+ ];
39
+ const executions = [];
40
+ const failuresByMatch = [];
41
+ let failure;
42
+ const answer = (sql)=>{
43
+ // Last registration wins, so an override does not have to remove the
44
+ // default it replaces.
45
+ for(let index = responses.length - 1; index >= 0; index -= 1){
46
+ const response = responses[index];
47
+ if (response !== undefined && sql.includes(response.match)) {
48
+ return response.rows;
49
+ }
50
+ }
51
+ return [];
52
+ };
53
+ const execute = (sql, params)=>{
54
+ var _ref;
55
+ executions.push({
56
+ sql,
57
+ params
58
+ });
59
+ const scoped = failuresByMatch.find((entry)=>sql.includes(entry.match));
60
+ const message = (_ref = scoped == null ? void 0 : scoped.message) != null ? _ref : failure;
61
+ if (message !== undefined) {
62
+ return Effect.fail(new SqlError.SqlError({
63
+ cause: new Error(message),
64
+ message
65
+ }));
66
+ }
67
+ return Effect.succeed(answer(sql));
68
+ };
69
+ const connection = {
70
+ execute: (sql, params)=>execute(sql, params),
71
+ executeUnprepared: (sql, params)=>execute(sql, params),
72
+ executeRaw: (sql, params)=>execute(sql, params),
73
+ executeValues: ()=>unsupported('executeValues'),
74
+ executeStream: ()=>unsupported('executeStream')
75
+ };
76
+ return {
77
+ respondTo: (match, rows)=>{
78
+ responses.push({
79
+ match,
80
+ rows
81
+ });
82
+ },
83
+ failWith: (message)=>{
84
+ failure = message;
85
+ },
86
+ failOn: (match, message)=>{
87
+ failuresByMatch.push({
88
+ match,
89
+ message
90
+ });
91
+ },
92
+ get executions () {
93
+ return executions;
94
+ },
95
+ get lastExecution () {
96
+ return executions[executions.length - 1];
97
+ },
98
+ client: Statement.make(Effect.succeed(connection), pgCompiler, [], undefined)
99
+ };
100
+ };
101
+
102
+ //# sourceMappingURL=fake-sql.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/drivers/fake-sql.ts"],"sourcesContent":["/**\n * A fake of the `@effect/sql` driver, not of the repository port.\n *\n * It sits one level *below* `makeSqlRepository`, so the adapter's own code — the\n * filter translation, the clause assembly, the upsert rule, the error mapping —\n * actually runs and is measured by coverage. A fake at the port level would skip\n * all of it. Same reasoning as {@link makeFakeMongoDb}.\n *\n * What it does **not** do is execute SQL: it hands back canned rows and records\n * the statement each call compiled to, so a spec asserts on the SQL and\n * parameters the adapter produced. Running the shared\n * `describeEntityRepositoryContract` needs a real engine and belongs against real\n * Postgres, the way the driver connection Layers are covered.\n */\n\nimport { SqlError } from '@effect/sql';\nimport * as Statement from '@effect/sql/Statement';\nimport { Effect } from 'effect';\n\n/** One statement the adapter executed, already compiled. */\nexport interface FakeSqlExecution {\n /** The compiled SQL text, parameters replaced by `$1`, `$2`, … */\n readonly sql: string;\n readonly params: ReadonlyArray<unknown>;\n}\n\n/** Rows to answer with when a statement's SQL contains `match`. */\nexport interface FakeSqlResponse {\n readonly match: string;\n readonly rows: ReadonlyArray<Record<string, unknown>>;\n}\n\nexport interface FakeSqlClient {\n /**\n * Answers `rows` for any statement whose compiled SQL contains `match`. Later\n * registrations win, so a spec can override a default set up by a helper.\n */\n respondTo(match: string, rows: ReadonlyArray<Record<string, unknown>>): void;\n /**\n * Makes every subsequent execution fail, so the adapter's `EntifixConnError`\n * mapping is reachable.\n */\n failWith(message: string): void;\n /**\n * Makes only the statements containing `match` fail, leaving the rest working\n * — the way a select that succeeds and a count that then fails reaches a\n * distinct error branch.\n */\n failOn(match: string, message: string): void;\n /** Every statement executed, in order, for assertions about what ran. */\n readonly executions: ReadonlyArray<FakeSqlExecution>;\n /** The most recently executed statement. */\n readonly lastExecution: FakeSqlExecution | undefined;\n /**\n * The object to hand to `makeSqlRepository` in place of a real `SqlClient`.\n *\n * Typed `unknown` on purpose — casting at the call site keeps this fake honest\n * that it implements only the constructor surface the adapters use, not the\n * whole `SqlClient` (no transactions, no reactivity, no streaming).\n */\n readonly client: unknown;\n}\n\nconst unsupported = (name: string): never => {\n throw new TypeError(`fake sql: ${name} is not implemented`);\n};\n\n/**\n * The Postgres compiler's configuration, mirrored from `@effect/sql-pg` so the\n * fake emits exactly the dialect production does — `$n` placeholders and\n * double-quoted identifiers — without this package depending on the `pg` driver.\n *\n * `sql.updateValues` and the `PgJson` custom segment are deliberate `TypeError`s\n * rather than approximations: no adapter uses either, and a compiler that\n * half-emitted them would let a spec pass on SQL that real Postgres rejects.\n */\nconst pgCompiler = Statement.makeCompiler({\n dialect: 'pg',\n placeholder: index => `$${index}`,\n onIdentifier: Statement.defaultEscape('\"'),\n onRecordUpdate: () => unsupported('updateValues'),\n onCustom: () => unsupported('custom segments'),\n});\n\n/** Builds an in-memory stand-in for an `@effect/sql` client. */\nexport const makeFakeSqlClient = (\n seed: ReadonlyArray<FakeSqlResponse> = [],\n): FakeSqlClient => {\n const responses: FakeSqlResponse[] = [...seed];\n const executions: FakeSqlExecution[] = [];\n const failuresByMatch: Array<{ match: string; message: string }> = [];\n let failure: string | undefined;\n\n const answer = (sql: string) => {\n // Last registration wins, so an override does not have to remove the\n // default it replaces.\n for (let index = responses.length - 1; index >= 0; index -= 1) {\n const response = responses[index];\n if (response !== undefined && sql.includes(response.match)) {\n return response.rows;\n }\n }\n return [];\n };\n\n const execute = (sql: string, params: ReadonlyArray<unknown>) => {\n executions.push({ sql, params });\n\n const scoped = failuresByMatch.find(entry => sql.includes(entry.match));\n const message = scoped?.message ?? failure;\n if (message !== undefined) {\n return Effect.fail(\n new SqlError.SqlError({ cause: new Error(message), message }),\n );\n }\n return Effect.succeed(answer(sql));\n };\n\n const connection = {\n execute: (sql: string, params: ReadonlyArray<unknown>) =>\n execute(sql, params),\n executeUnprepared: (sql: string, params: ReadonlyArray<unknown>) =>\n execute(sql, params),\n executeRaw: (sql: string, params: ReadonlyArray<unknown>) =>\n execute(sql, params),\n executeValues: () => unsupported('executeValues'),\n executeStream: () => unsupported('executeStream'),\n };\n\n return {\n respondTo: (match, rows) => {\n responses.push({ match, rows });\n },\n failWith: message => {\n failure = message;\n },\n failOn: (match, message) => {\n failuresByMatch.push({ match, message });\n },\n get executions() {\n return executions;\n },\n get lastExecution() {\n return executions[executions.length - 1];\n },\n client: Statement.make(\n Effect.succeed(connection) as never,\n pgCompiler,\n [],\n undefined,\n ),\n };\n};\n"],"names":["SqlError","Statement","Effect","unsupported","name","TypeError","pgCompiler","makeCompiler","dialect","placeholder","index","onIdentifier","defaultEscape","onRecordUpdate","onCustom","makeFakeSqlClient","seed","responses","executions","failuresByMatch","failure","answer","sql","length","response","undefined","includes","match","rows","execute","params","push","scoped","find","entry","message","fail","cause","Error","succeed","connection","executeUnprepared","executeRaw","executeValues","executeStream","respondTo","failWith","failOn","lastExecution","client","make"],"mappings":"AAAA;;;;;;;;;;;;;CAaC,GAED,SAASA,QAAQ,QAAQ,cAAc;AACvC,YAAYC,eAAe,wBAAwB;AACnD,SAASC,MAAM,QAAQ,SAAS;AA8ChC,MAAMC,cAAc,CAACC;IACnB,MAAM,IAAIC,UAAU,CAAC,UAAU,EAAED,KAAK,mBAAmB,CAAC;AAC5D;AAEA;;;;;;;;CAQC,GACD,MAAME,aAAaL,UAAUM,YAAY,CAAC;IACxCC,SAAS;IACTC,aAAaC,CAAAA,QAAS,CAAC,CAAC,EAAEA,OAAO;IACjCC,cAAcV,UAAUW,aAAa,CAAC;IACtCC,gBAAgB,IAAMV,YAAY;IAClCW,UAAU,IAAMX,YAAY;AAC9B;AAEA,8DAA8D,GAC9D,OAAO,MAAMY,oBAAoB,CAC/BC,OAAuC,EAAE;IAEzC,MAAMC,YAA+B;WAAID;KAAK;IAC9C,MAAME,aAAiC,EAAE;IACzC,MAAMC,kBAA6D,EAAE;IACrE,IAAIC;IAEJ,MAAMC,SAAS,CAACC;QACd,qEAAqE;QACrE,uBAAuB;QACvB,IAAK,IAAIZ,QAAQO,UAAUM,MAAM,GAAG,GAAGb,SAAS,GAAGA,SAAS,EAAG;YAC7D,MAAMc,WAAWP,SAAS,CAACP,MAAM;YACjC,IAAIc,aAAaC,aAAaH,IAAII,QAAQ,CAACF,SAASG,KAAK,GAAG;gBAC1D,OAAOH,SAASI,IAAI;YACtB;QACF;QACA,OAAO,EAAE;IACX;IAEA,MAAMC,UAAU,CAACP,KAAaQ;;QAC5BZ,WAAWa,IAAI,CAAC;YAAET;YAAKQ;QAAO;QAE9B,MAAME,SAASb,gBAAgBc,IAAI,CAACC,CAAAA,QAASZ,IAAII,QAAQ,CAACQ,MAAMP,KAAK;QACrE,MAAMQ,kBAAUH,0BAAAA,OAAQG,OAAO,mBAAIf;QACnC,IAAIe,YAAYV,WAAW;YACzB,OAAOvB,OAAOkC,IAAI,CAChB,IAAIpC,SAASA,QAAQ,CAAC;gBAAEqC,OAAO,IAAIC,MAAMH;gBAAUA;YAAQ;QAE/D;QACA,OAAOjC,OAAOqC,OAAO,CAAClB,OAAOC;IAC/B;IAEA,MAAMkB,aAAa;QACjBX,SAAS,CAACP,KAAaQ,SACrBD,QAAQP,KAAKQ;QACfW,mBAAmB,CAACnB,KAAaQ,SAC/BD,QAAQP,KAAKQ;QACfY,YAAY,CAACpB,KAAaQ,SACxBD,QAAQP,KAAKQ;QACfa,eAAe,IAAMxC,YAAY;QACjCyC,eAAe,IAAMzC,YAAY;IACnC;IAEA,OAAO;QACL0C,WAAW,CAAClB,OAAOC;YACjBX,UAAUc,IAAI,CAAC;gBAAEJ;gBAAOC;YAAK;QAC/B;QACAkB,UAAUX,CAAAA;YACRf,UAAUe;QACZ;QACAY,QAAQ,CAACpB,OAAOQ;YACdhB,gBAAgBY,IAAI,CAAC;gBAAEJ;gBAAOQ;YAAQ;QACxC;QACA,IAAIjB,cAAa;YACf,OAAOA;QACT;QACA,IAAI8B,iBAAgB;YAClB,OAAO9B,UAAU,CAACA,WAAWK,MAAM,GAAG,EAAE;QAC1C;QACA0B,QAAQhD,UAAUiD,IAAI,CACpBhD,OAAOqC,OAAO,CAACC,aACflC,YACA,EAAE,EACFmB;IAEJ;AACF,EAAE"}
@@ -0,0 +1,5 @@
1
+ export * from './fake-amqp.js';
2
+ export * from './fake-mongo.js';
3
+ export * from './fake-redis.js';
4
+ export * from './fake-sql.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/drivers/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC"}
@@ -0,0 +1,6 @@
1
+ export * from './fake-amqp.js';
2
+ export * from './fake-mongo.js';
3
+ export * from './fake-redis.js';
4
+ export * from './fake-sql.js';
5
+
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/drivers/index.ts"],"sourcesContent":["export * from './fake-amqp.js';\nexport * from './fake-mongo.js';\nexport * from './fake-redis.js';\nexport * from './fake-sql.js';\n"],"names":[],"mappings":"AAAA,cAAc,iBAAiB;AAC/B,cAAc,kBAAkB;AAChC,cAAc,kBAAkB;AAChC,cAAc,gBAAgB"}
@@ -0,0 +1,34 @@
1
+ import type { ConfigurationRepositoryTag } from '@entifix/business';
2
+ import { Effect, Exit, type Layer } from 'effect';
3
+ /**
4
+ * Runs an Effect that still requires `TContext`, providing it from `layer`.
5
+ *
6
+ * Use-cases are `Effect.gen` blocks yielding Context tags; every spec would
7
+ * otherwise repeat the same `Effect.runPromise(uc.pipe(Effect.provide(…)))`
8
+ * incantation.
9
+ */
10
+ export declare const runUC: <TValue, TError, TContext>(effect: Effect.Effect<TValue, TError, TContext>, layer: Layer.Layer<TContext>) => Promise<TValue>;
11
+ /**
12
+ * Runs an Effect expected to fail and returns its typed error.
13
+ *
14
+ * Asserting on the error value beats asserting that a promise rejected: it is
15
+ * the difference between "something went wrong" and "it failed with the
16
+ * `EntifixLockError` that the facade turns into a 409".
17
+ */
18
+ export declare const expectFailure: <TValue, TError, TContext>(effect: Effect.Effect<TValue, TError, TContext>, layer: Layer.Layer<TContext>) => Promise<TError>;
19
+ /**
20
+ * Runs a repository Effect.
21
+ *
22
+ * Every `EntityRepository` method declares `ConfigurationRepositoryTag` in its
23
+ * requirement channel — the REST-backed adapters resolve their endpoints
24
+ * through it — so even a repository that needs no configuration has to be given
25
+ * one. `configuration` supplies it for the adapters that do.
26
+ */
27
+ export declare const runRepository: <TValue, TError>(effect: Effect.Effect<TValue, TError, ConfigurationRepositoryTag>, configuration?: Layer.Layer<ConfigurationRepositoryTag>) => Promise<TValue>;
28
+ /** {@link runRepository}, for an Effect expected to fail. */
29
+ export declare const runRepositoryExit: <TValue, TError>(effect: Effect.Effect<TValue, TError, ConfigurationRepositoryTag>, configuration?: Layer.Layer<ConfigurationRepositoryTag>) => Promise<Exit.Exit<TValue, TError>>;
30
+ /** Runs an Effect that requires nothing, returning its value. */
31
+ export declare const run: <TValue, TError>(effect: Effect.Effect<TValue, TError>) => Promise<TValue>;
32
+ /** Runs an Effect that requires nothing and returns its typed error. */
33
+ export declare const runFailure: <TValue, TError>(effect: Effect.Effect<TValue, TError>) => Promise<TError>;
34
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/effect/run.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AAIlD;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,GAAI,MAAM,EAAE,MAAM,EAAE,QAAQ,EAC5C,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAC/C,OAAO,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,KAC3B,OAAO,CAAC,MAAM,CAAqD,CAAC;AAEvE;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAU,MAAM,EAAE,MAAM,EAAE,QAAQ,EAC1D,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAC/C,OAAO,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,KAC3B,OAAO,CAAC,MAAM,CAkBhB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,EAAE,MAAM,EAC1C,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,0BAA0B,CAAC,EACjE,gBAAe,KAAK,CAAC,KAAK,CAAC,0BAA0B,CAA4B,KAChF,OAAO,CAAC,MAAM,CAA6D,CAAC;AAE/E,6DAA6D;AAC7D,eAAO,MAAM,iBAAiB,GAAI,MAAM,EAAE,MAAM,EAC9C,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,0BAA0B,CAAC,EACjE,gBAAe,KAAK,CAAC,KAAK,CAAC,0BAA0B,CAA4B,KAChF,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAC0B,CAAC;AAE/D,iEAAiE;AACjE,eAAO,MAAM,GAAG,GAAI,MAAM,EAAE,MAAM,EAChC,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KACpC,OAAO,CAAC,MAAM,CAA8B,CAAC;AAEhD,wEAAwE;AACxE,eAAO,MAAM,UAAU,GAAU,MAAM,EAAE,MAAM,EAC7C,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KACpC,OAAO,CAAC,MAAM,CAgBhB,CAAC"}
@@ -0,0 +1,48 @@
1
+ import { Effect, Exit } from 'effect';
2
+ import { stubConfigurationLayer } from '../doubles/configuration.js';
3
+ /**
4
+ * Runs an Effect that still requires `TContext`, providing it from `layer`.
5
+ *
6
+ * Use-cases are `Effect.gen` blocks yielding Context tags; every spec would
7
+ * otherwise repeat the same `Effect.runPromise(uc.pipe(Effect.provide(…)))`
8
+ * incantation.
9
+ */ export const runUC = (effect, layer)=>Effect.runPromise(Effect.provide(effect, layer));
10
+ /**
11
+ * Runs an Effect expected to fail and returns its typed error.
12
+ *
13
+ * Asserting on the error value beats asserting that a promise rejected: it is
14
+ * the difference between "something went wrong" and "it failed with the
15
+ * `EntifixLockError` that the facade turns into a 409".
16
+ */ export const expectFailure = async (effect, layer)=>{
17
+ const exit = await Effect.runPromiseExit(Effect.provide(effect, layer));
18
+ if (Exit.isSuccess(exit)) {
19
+ throw new Error(`Expected the effect to fail, but it succeeded with: ${JSON.stringify(exit.value)}`);
20
+ }
21
+ const failure = exit.cause;
22
+ if (failure._tag === 'Fail') {
23
+ return failure.error;
24
+ }
25
+ throw new Error(`Expected a typed failure, but the effect died with: ${String(failure)}`);
26
+ };
27
+ /**
28
+ * Runs a repository Effect.
29
+ *
30
+ * Every `EntityRepository` method declares `ConfigurationRepositoryTag` in its
31
+ * requirement channel — the REST-backed adapters resolve their endpoints
32
+ * through it — so even a repository that needs no configuration has to be given
33
+ * one. `configuration` supplies it for the adapters that do.
34
+ */ export const runRepository = (effect, configuration = stubConfigurationLayer())=>Effect.runPromise(Effect.provide(effect, configuration));
35
+ /** {@link runRepository}, for an Effect expected to fail. */ export const runRepositoryExit = (effect, configuration = stubConfigurationLayer())=>Effect.runPromiseExit(Effect.provide(effect, configuration));
36
+ /** Runs an Effect that requires nothing, returning its value. */ export const run = (effect)=>Effect.runPromise(effect);
37
+ /** Runs an Effect that requires nothing and returns its typed error. */ export const runFailure = async (effect)=>{
38
+ const exit = await Effect.runPromiseExit(effect);
39
+ if (Exit.isSuccess(exit)) {
40
+ throw new Error(`Expected the effect to fail, but it succeeded with: ${JSON.stringify(exit.value)}`);
41
+ }
42
+ if (exit.cause._tag === 'Fail') {
43
+ return exit.cause.error;
44
+ }
45
+ throw new Error(`Expected a typed failure, but the effect died with: ${String(exit.cause)}`);
46
+ };
47
+
48
+ //# sourceMappingURL=run.js.map