@entifix/sql 0.4.2 → 0.6.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.
@@ -5,8 +5,7 @@ import type { SqlClient } from 'effect/sql';
5
5
  * Builds an {@link EntityRepository} for a single entity type, backed by an
6
6
  * `@effect/sql` client. The `sql` client and `entityConstructor` are captured in
7
7
  * closures, so every method's Effect has no outstanding requirements
8
- * (`R = never`) and is assignable to the interface's `ConfigurationRepositoryTag`
9
- * channel.
8
+ * (`R = never`), which is exactly what the interface asks.
10
9
  *
11
10
  * This is the relational mirror of `makeMongoRepository`, and it is short for the
12
11
  * same reason: the entifix serializer is store-agnostic. `serializeEntity` emits
@@ -1 +1 @@
1
- {"version":3,"file":"make-sql-repository.d.ts","sourceRoot":"","sources":["../../src/adapters/make-sql-repository.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,MAAM,EACX,KAAK,iBAAiB,EAMvB,MAAM,eAAe,CAAC;AAEvB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAuB5C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,SAAS,MAAM,EACtD,GAAG,EAAE,SAAS,CAAC,SAAS,EACxB,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAC5C,gBAAgB,CAuJlB;AAED,YAAY,EAAE,YAAY,EAAE,CAAC"}
1
+ {"version":3,"file":"make-sql-repository.d.ts","sourceRoot":"","sources":["../../src/adapters/make-sql-repository.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,MAAM,EACX,KAAK,iBAAiB,EAMvB,MAAM,eAAe,CAAC;AAEvB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAuB5C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,SAAS,MAAM,EACtD,GAAG,EAAE,SAAS,CAAC,SAAS,EACxB,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAC5C,gBAAgB,CAsJlB;AAED,YAAY,EAAE,YAAY,EAAE,CAAC"}
@@ -15,8 +15,7 @@ function toEntityId(entityOrId) {
15
15
  * Builds an {@link EntityRepository} for a single entity type, backed by an
16
16
  * `@effect/sql` client. The `sql` client and `entityConstructor` are captured in
17
17
  * closures, so every method's Effect has no outstanding requirements
18
- * (`R = never`) and is assignable to the interface's `ConfigurationRepositoryTag`
19
- * channel.
18
+ * (`R = never`), which is exactly what the interface asks.
20
19
  *
21
20
  * This is the relational mirror of `makeMongoRepository`, and it is short for the
22
21
  * same reason: the entifix serializer is store-agnostic. `serializeEntity` emits
@@ -118,9 +117,8 @@ function toEntityId(entityOrId) {
118
117
  id
119
118
  })));
120
119
  });
121
- // The interface types each method's requirement channel as
122
- // `ConfigurationRepositoryTag`; these effects require nothing (`never`), which
123
- // is assignable. Cast through the interface to keep the generic signatures.
120
+ // Cast through the interface to keep its generic signatures: each method here
121
+ // is written against this entity's type, the interface against any entity's.
124
122
  return {
125
123
  load,
126
124
  get,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/make-sql-repository.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type { EntityRepository } from '@entifix/business';\nimport {\n deserializeEntityCollection,\n deserializeSingleEntity,\n EntifixConnError,\n type EntifixError,\n type Entity,\n type EntityConstructor,\n type EntityId,\n type EntityLoadRequest,\n type EntityPage,\n extractMetaEntity,\n serializeEntity,\n} from '@entifix/core';\nimport { Effect } from 'effect';\nimport type { SqlClient } from 'effect/sql';\n\nimport {\n translateFiltering,\n translateSorting,\n} from './sql-filter-translator.js';\n\n/** A row as the driver returns it: column names, untransformed. */\ntype SqlRow = Record<string, unknown>;\n\nfunction tableName<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n): string {\n const metaEntity = extractMetaEntity(entityConstructor);\n return metaEntity.key ?? metaEntity.name;\n}\n\nfunction toEntityId(entityOrId: EntityId | Entity): EntityId {\n return entityOrId != null && typeof entityOrId === 'object'\n ? (entityOrId as Entity).id\n : (entityOrId as EntityId);\n}\n\n/**\n * Builds an {@link EntityRepository} for a single entity type, backed by an\n * `@effect/sql` client. The `sql` client and `entityConstructor` are captured in\n * closures, so every method's Effect has no outstanding requirements\n * (`R = never`) and is assignable to the interface's `ConfigurationRepositoryTag`\n * channel.\n *\n * This is the relational mirror of `makeMongoRepository`, and it is short for the\n * same reason: the entifix serializer is store-agnostic. `serializeEntity` emits\n * a **flat record keyed by `alias ?? name`**, which for a scalar entity *is* a\n * table row, and the deserializer consumes the same shape coming back. So the\n * entity's own `alias` metadata is the column mapping — there is no separate\n * table/column mapping layer, and none is needed.\n *\n * Table name = the entity's `key` (falling back to its class name), the same rule\n * the Mongo adapter uses for a collection.\n *\n * **v1 handles flat scalar entities.** Embedded links and `linkCollection`\n * members have no single-column representation, so an entity carrying them needs\n * either a join-aware adapter or `alias`ed scalar foreign keys.\n *\n * Two dialect assumptions, both true of Postgres and stated so a future MySQL or\n * SQLite adapter knows what to override: `ILIKE` for case-insensitive matching,\n * and `COUNT(*)::int` for the total. The client must **not** be configured with\n * name transformation (`transformResultNames`), since `alias` is already the\n * mapping and a second one would fight it.\n */\nexport function makeSqlRepository<TEntity extends Entity>(\n sql: SqlClient.SqlClient,\n entityConstructor: EntityConstructor<TEntity>,\n): EntityRepository {\n const table = sql(tableName(entityConstructor));\n\n const fail = (\n message: string,\n error: unknown,\n details?: Record<string, unknown>,\n ) =>\n new EntifixConnError(message, error, {\n entity: entityConstructor.name,\n ...details,\n });\n\n const load = <T extends Entity>(request: EntityLoadRequest<T>) =>\n Effect.gen(function* () {\n // The translators are framework-free and reject an undeclared member by\n // throwing, so the throw is caught back into the failure channel here —\n // the same crossing `readLoadRequest` makes for the URL codec.\n const clauses = yield* Effect.try({\n try: () => ({\n where: translateFiltering(\n sql,\n entityConstructor,\n request.filtering as EntityLoadRequest<TEntity>['filtering'],\n ),\n orderBy: translateSorting(\n sql,\n entityConstructor,\n request.sorting as EntityLoadRequest<TEntity>['sorting'],\n ),\n }),\n catch: error => error as EntifixError,\n });\n\n const where =\n clauses.where === undefined\n ? sql.literal('')\n : sql`WHERE ${clauses.where}`;\n const orderBy =\n clauses.orderBy === undefined\n ? sql.literal('')\n : sql`ORDER BY ${clauses.orderBy}`;\n\n const page = request.page ?? 1;\n const pageSize = request.pageSize ?? 10;\n const offset = (page - 1) * pageSize;\n\n const rows = yield* sql<SqlRow>`\n SELECT * FROM ${table} ${where} ${orderBy} LIMIT ${pageSize} OFFSET ${offset}\n `.pipe(\n Effect.mapError(error =>\n fail('Failed to load entities from SQL', error),\n ),\n );\n\n const counted = yield* sql<{\n readonly count: number;\n }>`SELECT COUNT(*)::int AS count FROM ${table} ${where}`.pipe(\n Effect.mapError(error =>\n fail('Failed to count entities in SQL', error),\n ),\n );\n\n const items = yield* deserializeEntityCollection(entityConstructor, [\n ...rows,\n ]);\n\n return {\n items: items as unknown as T[],\n total: counted[0]?.count ?? 0,\n request,\n } satisfies EntityPage<T>;\n });\n\n const get = <T extends Entity>(id: EntityId) =>\n Effect.gen(function* () {\n const rows = yield* sql<SqlRow>`\n SELECT * FROM ${table} WHERE ${sql('id')} = ${id} LIMIT 1\n `.pipe(\n Effect.mapError(error =>\n fail('Failed to read entity from SQL', error, { id }),\n ),\n );\n\n const entity = yield* deserializeSingleEntity(\n entityConstructor,\n rows[0] ?? null,\n );\n if (entity === undefined) {\n return yield* Effect.fail(fail('Entity not found', undefined, { id }));\n }\n return entity as unknown as T;\n });\n\n const save = <T extends Entity>(entity: T) =>\n Effect.gen(function* () {\n const serialized = serializeEntity(\n entityConstructor,\n entity as unknown as TEntity,\n );\n // A create arrives without an id, so the store mints one — the same\n // contract as the Mongo adapter, and what lets `save` double as the upsert\n // behind both POST and PUT.\n const id = entity.id ?? randomUUID();\n // The serializer drops `undefined`, so the resolved id is spread back in.\n const row: SqlRow = { ...serialized, id };\n\n const updatable = Object.keys(row).filter(column => column !== 'id');\n // An entity whose only member is its id has nothing to update; `DO UPDATE\n // SET` with an empty list is a syntax error, so it degrades to a no-op.\n const onConflict =\n updatable.length === 0\n ? sql.literal('DO NOTHING')\n : sql`DO UPDATE SET ${sql.csv(\n updatable.map(\n column => sql`${sql(column)} = EXCLUDED.${sql(column)}`,\n ),\n )}`;\n\n yield* sql`\n INSERT INTO ${table} ${sql.insert(row)}\n ON CONFLICT (${sql('id')}) ${onConflict}\n `.pipe(\n Effect.mapError(error =>\n fail('Failed to save entity to SQL', error, { id }),\n ),\n );\n\n entity.id = id;\n return entity;\n });\n\n const del = <T extends Entity>(entityOrId: EntityId | T) =>\n Effect.gen(function* () {\n const id = toEntityId(entityOrId);\n yield* sql`DELETE FROM ${table} WHERE ${sql('id')} = ${id}`.pipe(\n Effect.mapError(error =>\n fail('Failed to delete entity from SQL', error, { id }),\n ),\n );\n });\n\n // The interface types each method's requirement channel as\n // `ConfigurationRepositoryTag`; these effects require nothing (`never`), which\n // is assignable. Cast through the interface to keep the generic signatures.\n return {\n load,\n get,\n save,\n delete: del,\n } as unknown as EntityRepository;\n}\n\nexport type { EntifixError };\n"],"names":["randomUUID","deserializeEntityCollection","deserializeSingleEntity","EntifixConnError","extractMetaEntity","serializeEntity","Effect","translateFiltering","translateSorting","tableName","entityConstructor","metaEntity","key","name","toEntityId","entityOrId","id","makeSqlRepository","sql","table","fail","message","error","details","entity","load","request","gen","counted","clauses","try","where","filtering","orderBy","sorting","catch","undefined","literal","page","pageSize","offset","rows","pipe","mapError","items","total","count","get","save","serialized","row","updatable","Object","keys","filter","column","onConflict","length","csv","map","insert","del","delete"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/adapters/make-sql-repository.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type { EntityRepository } from '@entifix/business';\nimport {\n deserializeEntityCollection,\n deserializeSingleEntity,\n EntifixConnError,\n type EntifixError,\n type Entity,\n type EntityConstructor,\n type EntityId,\n type EntityLoadRequest,\n type EntityPage,\n extractMetaEntity,\n serializeEntity,\n} from '@entifix/core';\nimport { Effect } from 'effect';\nimport type { SqlClient } from 'effect/sql';\n\nimport {\n translateFiltering,\n translateSorting,\n} from './sql-filter-translator.js';\n\n/** A row as the driver returns it: column names, untransformed. */\ntype SqlRow = Record<string, unknown>;\n\nfunction tableName<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n): string {\n const metaEntity = extractMetaEntity(entityConstructor);\n return metaEntity.key ?? metaEntity.name;\n}\n\nfunction toEntityId(entityOrId: EntityId | Entity): EntityId {\n return entityOrId != null && typeof entityOrId === 'object'\n ? (entityOrId as Entity).id\n : (entityOrId as EntityId);\n}\n\n/**\n * Builds an {@link EntityRepository} for a single entity type, backed by an\n * `@effect/sql` client. The `sql` client and `entityConstructor` are captured in\n * closures, so every method's Effect has no outstanding requirements\n * (`R = never`), which is exactly what the interface asks.\n *\n * This is the relational mirror of `makeMongoRepository`, and it is short for the\n * same reason: the entifix serializer is store-agnostic. `serializeEntity` emits\n * a **flat record keyed by `alias ?? name`**, which for a scalar entity *is* a\n * table row, and the deserializer consumes the same shape coming back. So the\n * entity's own `alias` metadata is the column mapping — there is no separate\n * table/column mapping layer, and none is needed.\n *\n * Table name = the entity's `key` (falling back to its class name), the same rule\n * the Mongo adapter uses for a collection.\n *\n * **v1 handles flat scalar entities.** Embedded links and `linkCollection`\n * members have no single-column representation, so an entity carrying them needs\n * either a join-aware adapter or `alias`ed scalar foreign keys.\n *\n * Two dialect assumptions, both true of Postgres and stated so a future MySQL or\n * SQLite adapter knows what to override: `ILIKE` for case-insensitive matching,\n * and `COUNT(*)::int` for the total. The client must **not** be configured with\n * name transformation (`transformResultNames`), since `alias` is already the\n * mapping and a second one would fight it.\n */\nexport function makeSqlRepository<TEntity extends Entity>(\n sql: SqlClient.SqlClient,\n entityConstructor: EntityConstructor<TEntity>,\n): EntityRepository {\n const table = sql(tableName(entityConstructor));\n\n const fail = (\n message: string,\n error: unknown,\n details?: Record<string, unknown>,\n ) =>\n new EntifixConnError(message, error, {\n entity: entityConstructor.name,\n ...details,\n });\n\n const load = <T extends Entity>(request: EntityLoadRequest<T>) =>\n Effect.gen(function* () {\n // The translators are framework-free and reject an undeclared member by\n // throwing, so the throw is caught back into the failure channel here —\n // the same crossing `readLoadRequest` makes for the URL codec.\n const clauses = yield* Effect.try({\n try: () => ({\n where: translateFiltering(\n sql,\n entityConstructor,\n request.filtering as EntityLoadRequest<TEntity>['filtering'],\n ),\n orderBy: translateSorting(\n sql,\n entityConstructor,\n request.sorting as EntityLoadRequest<TEntity>['sorting'],\n ),\n }),\n catch: error => error as EntifixError,\n });\n\n const where =\n clauses.where === undefined\n ? sql.literal('')\n : sql`WHERE ${clauses.where}`;\n const orderBy =\n clauses.orderBy === undefined\n ? sql.literal('')\n : sql`ORDER BY ${clauses.orderBy}`;\n\n const page = request.page ?? 1;\n const pageSize = request.pageSize ?? 10;\n const offset = (page - 1) * pageSize;\n\n const rows = yield* sql<SqlRow>`\n SELECT * FROM ${table} ${where} ${orderBy} LIMIT ${pageSize} OFFSET ${offset}\n `.pipe(\n Effect.mapError(error =>\n fail('Failed to load entities from SQL', error),\n ),\n );\n\n const counted = yield* sql<{\n readonly count: number;\n }>`SELECT COUNT(*)::int AS count FROM ${table} ${where}`.pipe(\n Effect.mapError(error =>\n fail('Failed to count entities in SQL', error),\n ),\n );\n\n const items = yield* deserializeEntityCollection(entityConstructor, [\n ...rows,\n ]);\n\n return {\n items: items as unknown as T[],\n total: counted[0]?.count ?? 0,\n request,\n } satisfies EntityPage<T>;\n });\n\n const get = <T extends Entity>(id: EntityId) =>\n Effect.gen(function* () {\n const rows = yield* sql<SqlRow>`\n SELECT * FROM ${table} WHERE ${sql('id')} = ${id} LIMIT 1\n `.pipe(\n Effect.mapError(error =>\n fail('Failed to read entity from SQL', error, { id }),\n ),\n );\n\n const entity = yield* deserializeSingleEntity(\n entityConstructor,\n rows[0] ?? null,\n );\n if (entity === undefined) {\n return yield* Effect.fail(fail('Entity not found', undefined, { id }));\n }\n return entity as unknown as T;\n });\n\n const save = <T extends Entity>(entity: T) =>\n Effect.gen(function* () {\n const serialized = serializeEntity(\n entityConstructor,\n entity as unknown as TEntity,\n );\n // A create arrives without an id, so the store mints one — the same\n // contract as the Mongo adapter, and what lets `save` double as the upsert\n // behind both POST and PUT.\n const id = entity.id ?? randomUUID();\n // The serializer drops `undefined`, so the resolved id is spread back in.\n const row: SqlRow = { ...serialized, id };\n\n const updatable = Object.keys(row).filter(column => column !== 'id');\n // An entity whose only member is its id has nothing to update; `DO UPDATE\n // SET` with an empty list is a syntax error, so it degrades to a no-op.\n const onConflict =\n updatable.length === 0\n ? sql.literal('DO NOTHING')\n : sql`DO UPDATE SET ${sql.csv(\n updatable.map(\n column => sql`${sql(column)} = EXCLUDED.${sql(column)}`,\n ),\n )}`;\n\n yield* sql`\n INSERT INTO ${table} ${sql.insert(row)}\n ON CONFLICT (${sql('id')}) ${onConflict}\n `.pipe(\n Effect.mapError(error =>\n fail('Failed to save entity to SQL', error, { id }),\n ),\n );\n\n entity.id = id;\n return entity;\n });\n\n const del = <T extends Entity>(entityOrId: EntityId | T) =>\n Effect.gen(function* () {\n const id = toEntityId(entityOrId);\n yield* sql`DELETE FROM ${table} WHERE ${sql('id')} = ${id}`.pipe(\n Effect.mapError(error =>\n fail('Failed to delete entity from SQL', error, { id }),\n ),\n );\n });\n\n // Cast through the interface to keep its generic signatures: each method here\n // is written against this entity's type, the interface against any entity's.\n return {\n load,\n get,\n save,\n delete: del,\n } as unknown as EntityRepository;\n}\n\nexport type { EntifixError };\n"],"names":["randomUUID","deserializeEntityCollection","deserializeSingleEntity","EntifixConnError","extractMetaEntity","serializeEntity","Effect","translateFiltering","translateSorting","tableName","entityConstructor","metaEntity","key","name","toEntityId","entityOrId","id","makeSqlRepository","sql","table","fail","message","error","details","entity","load","request","gen","counted","clauses","try","where","filtering","orderBy","sorting","catch","undefined","literal","page","pageSize","offset","rows","pipe","mapError","items","total","count","get","save","serialized","row","updatable","Object","keys","filter","column","onConflict","length","csv","map","insert","del","delete"],"mappings":"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"}
@@ -18,7 +18,7 @@ export declare const SQL_PROBE_NAME = "postgres";
18
18
  *
19
19
  * `stores` names the Stores this connection backs, by their register name in
20
20
  * `tools/slices/`. See {@link MongoHealthProbeLayer} for why it is the
21
- * composition root's to supply and why it is a list (ADR 0031).
21
+ * composition root's to supply and why it is a list.
22
22
  */
23
23
  export declare const SqlHealthProbeLayer: (stores: readonly string[]) => Layer.Layer<never, never, SqlClient.SqlClient | HealthRegistryTag>;
24
24
  //# sourceMappingURL=sql-health-probe.d.ts.map
@@ -17,7 +17,7 @@ import { SqlClient } from 'effect/sql';
17
17
  *
18
18
  * `stores` names the Stores this connection backs, by their register name in
19
19
  * `tools/slices/`. See {@link MongoHealthProbeLayer} for why it is the
20
- * composition root's to supply and why it is a list (ADR 0031).
20
+ * composition root's to supply and why it is a list.
21
21
  */ export const SqlHealthProbeLayer = (stores)=>Layer.effectDiscard(Effect.gen(function*() {
22
22
  const sql = yield* SqlClient.SqlClient;
23
23
  const registry = yield* HealthRegistryTag;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sql-database/sql-health-probe.ts"],"sourcesContent":["import { HealthRegistryTag } from '@entifix/business';\nimport { Effect, Layer } from 'effect';\nimport { SqlClient } from 'effect/sql';\n\n/** Probe name reported by `/api/health/ready` when the database is unreachable. */\nexport const SQL_PROBE_NAME = 'postgres';\n\n/**\n * Registers a readiness probe for the connected SQL database.\n *\n * `SELECT 1` rather than \"is the pool object there\": a connection pool survives\n * a database that has gone away, so only a query that crosses the wire can tell\n * the two apart. For config-service in particular, being reachable while unable\n * to read its table is precisely the state the rest of the fleet needs to hear\n * about — every service resolves its settings from there.\n *\n * Merge it beside the client layer in a service's `AppLayer` and readiness starts\n * reporting the database, with nothing in the service describing the probe — so\n * it cannot drift from the dependency it describes.\n *\n * `stores` names the Stores this connection backs, by their register name in\n * `tools/slices/`. See {@link MongoHealthProbeLayer} for why it is the\n * composition root's to supply and why it is a list (ADR 0031).\n */\nexport const SqlHealthProbeLayer = (\n stores: readonly string[],\n): Layer.Layer<never, never, SqlClient.SqlClient | HealthRegistryTag> =>\n Layer.effectDiscard(\n Effect.gen(function* () {\n const sql = yield* SqlClient.SqlClient;\n const registry = yield* HealthRegistryTag;\n\n yield* registry.register({\n name: SQL_PROBE_NAME,\n kind: 'datastore',\n targets: stores,\n check: sql`SELECT 1`.pipe(\n Effect.as(true),\n // Unreachable is a `false`, never a failed endpoint.\n Effect.catch(() => Effect.succeed(false)),\n ),\n });\n }),\n );\n"],"names":["HealthRegistryTag","Effect","Layer","SqlClient","SQL_PROBE_NAME","SqlHealthProbeLayer","stores","effectDiscard","gen","sql","registry","register","name","kind","targets","check","pipe","as","catch","succeed"],"mappings":"AAAA,SAASA,iBAAiB,QAAQ,oBAAoB;AACtD,SAASC,MAAM,EAAEC,KAAK,QAAQ,SAAS;AACvC,SAASC,SAAS,QAAQ,aAAa;AAEvC,iFAAiF,GACjF,OAAO,MAAMC,iBAAiB,WAAW;AAEzC;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,MAAMC,sBAAsB,CACjCC,SAEAJ,MAAMK,aAAa,CACjBN,OAAOO,GAAG,CAAC;QACT,MAAMC,MAAM,OAAON,UAAUA,SAAS;QACtC,MAAMO,WAAW,OAAOV;QAExB,OAAOU,SAASC,QAAQ,CAAC;YACvBC,MAAMR;YACNS,MAAM;YACNC,SAASR;YACTS,OAAON,GAAG,CAAC,QAAQ,CAAC,CAACO,IAAI,CACvBf,OAAOgB,EAAE,CAAC,OACV,qDAAqD;YACrDhB,OAAOiB,KAAK,CAAC,IAAMjB,OAAOkB,OAAO,CAAC;QAEtC;IACF,IACA"}
1
+ {"version":3,"sources":["../../src/sql-database/sql-health-probe.ts"],"sourcesContent":["import { HealthRegistryTag } from '@entifix/business';\nimport { Effect, Layer } from 'effect';\nimport { SqlClient } from 'effect/sql';\n\n/** Probe name reported by `/api/health/ready` when the database is unreachable. */\nexport const SQL_PROBE_NAME = 'postgres';\n\n/**\n * Registers a readiness probe for the connected SQL database.\n *\n * `SELECT 1` rather than \"is the pool object there\": a connection pool survives\n * a database that has gone away, so only a query that crosses the wire can tell\n * the two apart. For config-service in particular, being reachable while unable\n * to read its table is precisely the state the rest of the fleet needs to hear\n * about — every service resolves its settings from there.\n *\n * Merge it beside the client layer in a service's `AppLayer` and readiness starts\n * reporting the database, with nothing in the service describing the probe — so\n * it cannot drift from the dependency it describes.\n *\n * `stores` names the Stores this connection backs, by their register name in\n * `tools/slices/`. See {@link MongoHealthProbeLayer} for why it is the\n * composition root's to supply and why it is a list.\n */\nexport const SqlHealthProbeLayer = (\n stores: readonly string[],\n): Layer.Layer<never, never, SqlClient.SqlClient | HealthRegistryTag> =>\n Layer.effectDiscard(\n Effect.gen(function* () {\n const sql = yield* SqlClient.SqlClient;\n const registry = yield* HealthRegistryTag;\n\n yield* registry.register({\n name: SQL_PROBE_NAME,\n kind: 'datastore',\n targets: stores,\n check: sql`SELECT 1`.pipe(\n Effect.as(true),\n // Unreachable is a `false`, never a failed endpoint.\n Effect.catch(() => Effect.succeed(false)),\n ),\n });\n }),\n );\n"],"names":["HealthRegistryTag","Effect","Layer","SqlClient","SQL_PROBE_NAME","SqlHealthProbeLayer","stores","effectDiscard","gen","sql","registry","register","name","kind","targets","check","pipe","as","catch","succeed"],"mappings":"AAAA,SAASA,iBAAiB,QAAQ,oBAAoB;AACtD,SAASC,MAAM,EAAEC,KAAK,QAAQ,SAAS;AACvC,SAASC,SAAS,QAAQ,aAAa;AAEvC,iFAAiF,GACjF,OAAO,MAAMC,iBAAiB,WAAW;AAEzC;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,MAAMC,sBAAsB,CACjCC,SAEAJ,MAAMK,aAAa,CACjBN,OAAOO,GAAG,CAAC;QACT,MAAMC,MAAM,OAAON,UAAUA,SAAS;QACtC,MAAMO,WAAW,OAAOV;QAExB,OAAOU,SAASC,QAAQ,CAAC;YACvBC,MAAMR;YACNS,MAAM;YACNC,SAASR;YACTS,OAAON,GAAG,CAAC,QAAQ,CAAC,CAACO,IAAI,CACvBf,OAAOgB,EAAE,CAAC,OACV,qDAAqD;YACrDhB,OAAOiB,KAAK,CAAC,IAAMjB,OAAOkB,OAAO,CAAC;QAEtC;IACF,IACA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entifix/sql",
3
- "version": "0.4.2",
3
+ "version": "0.6.0",
4
4
  "description": "SQL adapter over @effect/sql: an entity repository with RSQL filter and sort translation, and a health probe.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -51,12 +51,12 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@swc/helpers": "0.5.23",
54
- "@entifix/business": "0.4.2",
55
- "@entifix/core": "0.4.2"
54
+ "@entifix/core": "0.6.0",
55
+ "@entifix/business": "0.6.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "effect": "4.0.5",
59
- "@entifix/testing-unit": "0.4.2"
59
+ "@entifix/testing-unit": "0.6.0"
60
60
  },
61
61
  "peerDependencies": {
62
62
  "effect": "^4.0.5"