@entifix/sql 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/adapters/make-sql-repository.d.ts +1 -1
- package/dist/adapters/make-sql-repository.d.ts.map +1 -1
- package/dist/adapters/make-sql-repository.js.map +1 -1
- package/dist/adapters/sql-filter-translator.d.ts +2 -2
- package/dist/adapters/sql-filter-translator.d.ts.map +1 -1
- package/dist/adapters/sql-filter-translator.js.map +1 -1
- package/dist/sql-database/sql-health-probe.d.ts +1 -1
- package/dist/sql-database/sql-health-probe.d.ts.map +1 -1
- package/dist/sql-database/sql-health-probe.js +2 -2
- package/dist/sql-database/sql-health-probe.js.map +1 -1
- package/package.json +6 -7
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @entifix/sql
|
|
2
2
|
|
|
3
|
-
The SQL adapter, over
|
|
3
|
+
The SQL adapter, over `effect/sql`. `makeSqlRepository` implements the `EntityRepository` port from `@entifix/business` and translates an RSQL filter and sort into parameterised SQL — values are bound, never interpolated, and `LIKE` patterns are escaped.
|
|
4
4
|
|
|
5
|
-
Bring
|
|
5
|
+
Bring an Effect SQL driver for your database (`@effect/sql-pg`, say) and provide its `SqlClient`; this package does not choose one.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { SqlClient } from '@effect/sql';
|
|
2
1
|
import type { EntityRepository } from '@entifix/business';
|
|
3
2
|
import { type EntifixError, type Entity, type EntityConstructor } from '@entifix/core';
|
|
3
|
+
import type { SqlClient } from 'effect/sql';
|
|
4
4
|
/**
|
|
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
|
|
@@ -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,
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adapters/make-sql-repository.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type { SqlClient } from '@effect/sql';\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';\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`) 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,6 +1,6 @@
|
|
|
1
|
-
import type { SqlClient } from '@effect/sql';
|
|
2
|
-
import type { Statement } from '@effect/sql';
|
|
3
1
|
import { type Entity, type EntityConstructor, type EntityFiltering, type EntitySorting } from '@entifix/core';
|
|
2
|
+
import type { SqlClient } from 'effect/sql';
|
|
3
|
+
import type { Statement } from 'effect/sql';
|
|
4
4
|
/**
|
|
5
5
|
* Escapes a value so it matches literally inside an `ILIKE` pattern: `%` and `_`
|
|
6
6
|
* are the wildcards, and `\` is Postgres' default escape character, so it has to
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sql-filter-translator.d.ts","sourceRoot":"","sources":["../../src/adapters/sql-filter-translator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"sql-filter-translator.d.ts","sourceRoot":"","sources":["../../src/adapters/sql-filter-translator.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,MAAM,EACX,KAAK,iBAAiB,EAItB,KAAK,eAAe,EACpB,KAAK,aAAa,EAEnB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAY5C;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD;AAoGD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,SAAS,MAAM,EACvD,GAAG,EAAE,SAAS,CAAC,SAAS,EACxB,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,SAAS,CAAC,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,GACrC,SAAS,CAAC,QAAQ,GAAG,SAAS,CAsBhC;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,SAAS,MAAM,EACrD,GAAG,EAAE,SAAS,CAAC,SAAS,EACxB,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,OAAO,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,EAAE,GACjC,SAAS,CAAC,QAAQ,GAAG,SAAS,CAgChC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adapters/sql-filter-translator.ts"],"sourcesContent":["import type { SqlClient } from '@effect/sql';\nimport type { Statement } from '@effect/sql';\nimport {\n allowedDescriptors,\n EntifixBuildError,\n type Entity,\n type EntityConstructor,\n type EntityFieldDescriptor,\n type EntityFilter,\n type EntityFilterBinaryOperator,\n type EntityFiltering,\n type EntitySorting,\n type FilterGroup,\n} from '@entifix/core';\n\n/** SQL comparison for each binary operator. Fixed strings, never user input. */\nconst BINARY_OPERATORS: Record<EntityFilterBinaryOperator, string> = {\n eq: '=',\n ne: '<>',\n gt: '>',\n gte: '>=',\n lt: '<',\n lte: '<=',\n};\n\n/**\n * Escapes a value so it matches literally inside an `ILIKE` pattern: `%` and `_`\n * are the wildcards, and `\\` is Postgres' default escape character, so it has to\n * escape itself first — which a single character class does for free.\n *\n * This is the SQL counterpart to the Mongo translator's `escapeRegex`: `like` is\n * an unanchored \"contains\" match in both, so a user typing `100%` searches for\n * that string rather than for everything.\n */\nexport function escapeLikePattern(value: string): string {\n return value.replace(/[\\\\%_]/g, '\\\\$&');\n}\n\nfunction isFilterGroup<TEntity extends Entity>(\n node: EntityFilter<TEntity> | FilterGroup<TEntity>,\n): node is FilterGroup<TEntity> {\n // A group carries `values` + a logic operator and, unlike the array filter,\n // has no `property`.\n return (\n !('property' in node) && (node.operator === 'and' || node.operator === 'or')\n );\n}\n\n/**\n * Resolves a filter/sort member to its **column name**, or rejects it.\n *\n * This is the security boundary of this file. A property name reaches SQL as an\n * identifier, so it can never be taken on trust: it is looked up in the\n * entity's own allowlist (`filterable`/`sortable` accessor metadata, the same\n * map `coerceRsql` validates against) and only the descriptor's stored `key` is\n * ever emitted. A member the entity did not declare is an\n * {@link EntifixBuildError}, which the service maps to a `400`.\n *\n * A request arriving through `parseLoadRequestParams` has already been through\n * this gate, so this is deliberately a second check — an `EntityLoadRequest`\n * built in code reaches the adapter without passing the URL codec at all.\n */\nfunction resolveColumn(\n property: string | number | symbol,\n allowed: Map<string, EntityFieldDescriptor>,\n requirement: 'filterable' | 'sortable',\n): string {\n const name = String(property);\n const descriptor = allowed.get(name);\n if (!descriptor) {\n throw new EntifixBuildError(\n `\"${name}\" is not a ${requirement} member`,\n undefined,\n { property: name },\n );\n }\n return descriptor.key;\n}\n\n/**\n * Translates one filter node into a parameterized SQL fragment.\n *\n * Values are always interpolated as template parameters and column names always\n * through `sql(identifier)`, which the compiler quotes — this file never\n * concatenates a string into SQL.\n *\n * Returns `undefined` for a group with no members, which has no meaningful\n * translation (`AND ()` is a syntax error) and is dropped by the caller.\n */\nfunction translateNode<TEntity extends Entity>(\n sql: SqlClient.SqlClient,\n node: EntityFilter<TEntity> | FilterGroup<TEntity>,\n allowed: Map<string, EntityFieldDescriptor>,\n): Statement.Fragment | undefined {\n if (isFilterGroup(node)) {\n const children = node.values\n .map(child => translateNode(sql, child, allowed))\n .filter((child): child is Statement.Fragment => child !== undefined);\n if (children.length === 0) return undefined;\n return node.operator === 'or' ? sql.or(children) : sql.and(children);\n }\n\n const column = sql(resolveColumn(node.property, allowed, 'filterable'));\n\n switch (node.operator) {\n case 'eq':\n case 'ne':\n case 'gt':\n case 'gte':\n case 'lt':\n case 'lte':\n return sql`${column} ${sql.literal(BINARY_OPERATORS[node.operator])} ${node.value}`;\n case 'in':\n return sql`${column} IN ${sql.in(node.values)}`;\n case 'nin':\n return sql`${column} NOT IN ${sql.in(node.values)}`;\n case 'between':\n return sql`${column} BETWEEN ${node.start} AND ${node.end}`;\n case 'nbetween':\n return sql`${column} NOT BETWEEN ${node.start} AND ${node.end}`;\n case 'like':\n return sql`${column} ILIKE ${`%${escapeLikePattern(node.value)}%`}`;\n case 'nlike':\n return sql`${column} NOT ILIKE ${`%${escapeLikePattern(node.value)}%`}`;\n case 'isNull':\n return sql`${column} IS NULL`;\n case 'isNotNull':\n return sql`${column} IS NOT NULL`;\n default: {\n // Exhaustiveness guard — a new operator must be handled explicitly.\n const _never: never = node;\n return _never;\n }\n }\n}\n\n/**\n * Translates the request's `filtering` into a single `WHERE`-body fragment, or\n * `undefined` when there is nothing to filter on (the caller then omits the\n * clause entirely rather than emitting `WHERE TRUE`).\n *\n * Every top-level entry is combined with `AND`, matching the Mongo translator.\n *\n * Throws {@link EntifixBuildError} for a member the entity did not declare\n * `filterable` — synchronously, because this is a framework-free codec like the\n * rest of the entifix query stack; the repository wraps it back into an Effect.\n */\nexport function translateFiltering<TEntity extends Entity>(\n sql: SqlClient.SqlClient,\n entityConstructor: EntityConstructor<TEntity>,\n filtering?: EntityFiltering<TEntity>[],\n): Statement.Fragment | undefined {\n if (!filtering || filtering.length === 0) {\n return undefined;\n }\n\n const allowed = allowedDescriptors(\n entityConstructor,\n descriptor => descriptor.filterable,\n );\n\n const clauses: Statement.Fragment[] = [];\n for (const entry of filtering) {\n const nodes = Array.isArray(entry) ? entry : [entry];\n for (const node of nodes) {\n const clause = translateNode(sql, node, allowed);\n if (clause !== undefined) clauses.push(clause);\n }\n }\n\n if (clauses.length === 0) return undefined;\n if (clauses.length === 1) return clauses[0];\n return sql.and(clauses);\n}\n\n/**\n * Translates the request's `sorting` into an `ORDER BY`-body fragment, or\n * `undefined` when there is nothing to sort by.\n *\n * Priorities are applied in ascending numeric order within each record, and a\n * column named twice keeps its first position with the last direction — the same\n * outcome Mongo's sort document produces by assignment, so the two adapters\n * order identically.\n */\nexport function translateSorting<TEntity extends Entity>(\n sql: SqlClient.SqlClient,\n entityConstructor: EntityConstructor<TEntity>,\n sorting?: EntitySorting<TEntity>[],\n): Statement.Fragment | undefined {\n if (!sorting || sorting.length === 0) {\n return undefined;\n }\n\n const allowed = allowedDescriptors(\n entityConstructor,\n descriptor => descriptor.sortable,\n );\n const directions = new Map<string, 'ASC' | 'DESC'>();\n\n for (const record of sorting) {\n for (const priority of Object.keys(record)\n .map(Number)\n .sort((left, right) => left - right)) {\n const entry = record[priority];\n if (entry) {\n directions.set(\n resolveColumn(entry.property, allowed, 'sortable'),\n entry.type === 'desc' ? 'DESC' : 'ASC',\n );\n }\n }\n }\n\n if (directions.size === 0) return undefined;\n\n return sql.csv(\n [...directions].map(\n ([column, direction]) => sql`${sql(column)} ${sql.literal(direction)}`,\n ),\n );\n}\n"],"names":["allowedDescriptors","EntifixBuildError","BINARY_OPERATORS","eq","ne","gt","gte","lt","lte","escapeLikePattern","value","replace","isFilterGroup","node","operator","resolveColumn","property","allowed","requirement","name","String","descriptor","get","undefined","key","translateNode","sql","children","values","map","child","filter","length","or","and","column","literal","in","start","end","_never","translateFiltering","entityConstructor","filtering","filterable","clauses","entry","nodes","Array","isArray","clause","push","translateSorting","sorting","sortable","directions","Map","record","priority","Object","keys","Number","sort","left","right","set","type","size","csv","direction"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/sql-filter-translator.ts"],"sourcesContent":["import {\n allowedDescriptors,\n EntifixBuildError,\n type Entity,\n type EntityConstructor,\n type EntityFieldDescriptor,\n type EntityFilter,\n type EntityFilterBinaryOperator,\n type EntityFiltering,\n type EntitySorting,\n type FilterGroup,\n} from '@entifix/core';\nimport type { SqlClient } from 'effect/sql';\nimport type { Statement } from 'effect/sql';\n\n/** SQL comparison for each binary operator. Fixed strings, never user input. */\nconst BINARY_OPERATORS: Record<EntityFilterBinaryOperator, string> = {\n eq: '=',\n ne: '<>',\n gt: '>',\n gte: '>=',\n lt: '<',\n lte: '<=',\n};\n\n/**\n * Escapes a value so it matches literally inside an `ILIKE` pattern: `%` and `_`\n * are the wildcards, and `\\` is Postgres' default escape character, so it has to\n * escape itself first — which a single character class does for free.\n *\n * This is the SQL counterpart to the Mongo translator's `escapeRegex`: `like` is\n * an unanchored \"contains\" match in both, so a user typing `100%` searches for\n * that string rather than for everything.\n */\nexport function escapeLikePattern(value: string): string {\n return value.replace(/[\\\\%_]/g, '\\\\$&');\n}\n\nfunction isFilterGroup<TEntity extends Entity>(\n node: EntityFilter<TEntity> | FilterGroup<TEntity>,\n): node is FilterGroup<TEntity> {\n // A group carries `values` + a logic operator and, unlike the array filter,\n // has no `property`.\n return (\n !('property' in node) && (node.operator === 'and' || node.operator === 'or')\n );\n}\n\n/**\n * Resolves a filter/sort member to its **column name**, or rejects it.\n *\n * This is the security boundary of this file. A property name reaches SQL as an\n * identifier, so it can never be taken on trust: it is looked up in the\n * entity's own allowlist (`filterable`/`sortable` accessor metadata, the same\n * map `coerceRsql` validates against) and only the descriptor's stored `key` is\n * ever emitted. A member the entity did not declare is an\n * {@link EntifixBuildError}, which the service maps to a `400`.\n *\n * A request arriving through `parseLoadRequestParams` has already been through\n * this gate, so this is deliberately a second check — an `EntityLoadRequest`\n * built in code reaches the adapter without passing the URL codec at all.\n */\nfunction resolveColumn(\n property: string | number | symbol,\n allowed: Map<string, EntityFieldDescriptor>,\n requirement: 'filterable' | 'sortable',\n): string {\n const name = String(property);\n const descriptor = allowed.get(name);\n if (!descriptor) {\n throw new EntifixBuildError(\n `\"${name}\" is not a ${requirement} member`,\n undefined,\n { property: name },\n );\n }\n return descriptor.key;\n}\n\n/**\n * Translates one filter node into a parameterized SQL fragment.\n *\n * Values are always interpolated as template parameters and column names always\n * through `sql(identifier)`, which the compiler quotes — this file never\n * concatenates a string into SQL.\n *\n * Returns `undefined` for a group with no members, which has no meaningful\n * translation (`AND ()` is a syntax error) and is dropped by the caller.\n */\nfunction translateNode<TEntity extends Entity>(\n sql: SqlClient.SqlClient,\n node: EntityFilter<TEntity> | FilterGroup<TEntity>,\n allowed: Map<string, EntityFieldDescriptor>,\n): Statement.Fragment | undefined {\n if (isFilterGroup(node)) {\n const children = node.values\n .map(child => translateNode(sql, child, allowed))\n .filter((child): child is Statement.Fragment => child !== undefined);\n if (children.length === 0) return undefined;\n return node.operator === 'or' ? sql.or(children) : sql.and(children);\n }\n\n const column = sql(resolveColumn(node.property, allowed, 'filterable'));\n\n switch (node.operator) {\n case 'eq':\n case 'ne':\n case 'gt':\n case 'gte':\n case 'lt':\n case 'lte':\n return sql`${column} ${sql.literal(BINARY_OPERATORS[node.operator])} ${node.value}`;\n case 'in':\n return sql`${column} IN ${sql.in(node.values)}`;\n case 'nin':\n return sql`${column} NOT IN ${sql.in(node.values)}`;\n case 'between':\n return sql`${column} BETWEEN ${node.start} AND ${node.end}`;\n case 'nbetween':\n return sql`${column} NOT BETWEEN ${node.start} AND ${node.end}`;\n case 'like':\n return sql`${column} ILIKE ${`%${escapeLikePattern(node.value)}%`}`;\n case 'nlike':\n return sql`${column} NOT ILIKE ${`%${escapeLikePattern(node.value)}%`}`;\n case 'isNull':\n return sql`${column} IS NULL`;\n case 'isNotNull':\n return sql`${column} IS NOT NULL`;\n default: {\n // Exhaustiveness guard — a new operator must be handled explicitly.\n const _never: never = node;\n return _never;\n }\n }\n}\n\n/**\n * Translates the request's `filtering` into a single `WHERE`-body fragment, or\n * `undefined` when there is nothing to filter on (the caller then omits the\n * clause entirely rather than emitting `WHERE TRUE`).\n *\n * Every top-level entry is combined with `AND`, matching the Mongo translator.\n *\n * Throws {@link EntifixBuildError} for a member the entity did not declare\n * `filterable` — synchronously, because this is a framework-free codec like the\n * rest of the entifix query stack; the repository wraps it back into an Effect.\n */\nexport function translateFiltering<TEntity extends Entity>(\n sql: SqlClient.SqlClient,\n entityConstructor: EntityConstructor<TEntity>,\n filtering?: EntityFiltering<TEntity>[],\n): Statement.Fragment | undefined {\n if (!filtering || filtering.length === 0) {\n return undefined;\n }\n\n const allowed = allowedDescriptors(\n entityConstructor,\n descriptor => descriptor.filterable,\n );\n\n const clauses: Statement.Fragment[] = [];\n for (const entry of filtering) {\n const nodes = Array.isArray(entry) ? entry : [entry];\n for (const node of nodes) {\n const clause = translateNode(sql, node, allowed);\n if (clause !== undefined) clauses.push(clause);\n }\n }\n\n if (clauses.length === 0) return undefined;\n if (clauses.length === 1) return clauses[0];\n return sql.and(clauses);\n}\n\n/**\n * Translates the request's `sorting` into an `ORDER BY`-body fragment, or\n * `undefined` when there is nothing to sort by.\n *\n * Priorities are applied in ascending numeric order within each record, and a\n * column named twice keeps its first position with the last direction — the same\n * outcome Mongo's sort document produces by assignment, so the two adapters\n * order identically.\n */\nexport function translateSorting<TEntity extends Entity>(\n sql: SqlClient.SqlClient,\n entityConstructor: EntityConstructor<TEntity>,\n sorting?: EntitySorting<TEntity>[],\n): Statement.Fragment | undefined {\n if (!sorting || sorting.length === 0) {\n return undefined;\n }\n\n const allowed = allowedDescriptors(\n entityConstructor,\n descriptor => descriptor.sortable,\n );\n const directions = new Map<string, 'ASC' | 'DESC'>();\n\n for (const record of sorting) {\n for (const priority of Object.keys(record)\n .map(Number)\n .sort((left, right) => left - right)) {\n const entry = record[priority];\n if (entry) {\n directions.set(\n resolveColumn(entry.property, allowed, 'sortable'),\n entry.type === 'desc' ? 'DESC' : 'ASC',\n );\n }\n }\n }\n\n if (directions.size === 0) return undefined;\n\n return sql.csv(\n [...directions].map(\n ([column, direction]) => sql`${sql(column)} ${sql.literal(direction)}`,\n ),\n );\n}\n"],"names":["allowedDescriptors","EntifixBuildError","BINARY_OPERATORS","eq","ne","gt","gte","lt","lte","escapeLikePattern","value","replace","isFilterGroup","node","operator","resolveColumn","property","allowed","requirement","name","String","descriptor","get","undefined","key","translateNode","sql","children","values","map","child","filter","length","or","and","column","literal","in","start","end","_never","translateFiltering","entityConstructor","filtering","filterable","clauses","entry","nodes","Array","isArray","clause","push","translateSorting","sorting","sortable","directions","Map","record","priority","Object","keys","Number","sort","left","right","set","type","size","csv","direction"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { SqlClient } from '@effect/sql';
|
|
2
1
|
import { HealthRegistryTag } from '@entifix/business';
|
|
3
2
|
import { Layer } from 'effect';
|
|
3
|
+
import { SqlClient } from 'effect/sql';
|
|
4
4
|
/** Probe name reported by `/api/health/ready` when the database is unreachable. */
|
|
5
5
|
export declare const SQL_PROBE_NAME = "postgres";
|
|
6
6
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sql-health-probe.d.ts","sourceRoot":"","sources":["../../src/sql-database/sql-health-probe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"sql-health-probe.d.ts","sourceRoot":"","sources":["../../src/sql-database/sql-health-probe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,mFAAmF;AACnF,eAAO,MAAM,cAAc,aAAa,CAAC;AAEzC;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,mBAAmB,GAC9B,QAAQ,SAAS,MAAM,EAAE,KACxB,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,SAAS,GAAG,iBAAiB,CAiBjE,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { SqlClient } from '@effect/sql';
|
|
2
1
|
import { HealthRegistryTag } from '@entifix/business';
|
|
3
2
|
import { Effect, Layer } from 'effect';
|
|
3
|
+
import { SqlClient } from 'effect/sql';
|
|
4
4
|
/** Probe name reported by `/api/health/ready` when the database is unreachable. */ export const SQL_PROBE_NAME = 'postgres';
|
|
5
5
|
/**
|
|
6
6
|
* Registers a readiness probe for the connected SQL database.
|
|
@@ -26,7 +26,7 @@ import { Effect, Layer } from 'effect';
|
|
|
26
26
|
kind: 'datastore',
|
|
27
27
|
targets: stores,
|
|
28
28
|
check: sql`SELECT 1`.pipe(Effect.as(true), // Unreachable is a `false`, never a failed endpoint.
|
|
29
|
-
Effect.
|
|
29
|
+
Effect.catch(()=>Effect.succeed(false)))
|
|
30
30
|
});
|
|
31
31
|
}));
|
|
32
32
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/sql-database/sql-health-probe.ts"],"sourcesContent":["import {
|
|
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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entifix/sql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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,16 +51,15 @@
|
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@effect/sql": "0.52.1",
|
|
55
54
|
"@swc/helpers": "0.5.23",
|
|
56
|
-
"@entifix/business": "0.
|
|
57
|
-
"@entifix/core": "0.
|
|
55
|
+
"@entifix/business": "0.2.0",
|
|
56
|
+
"@entifix/core": "0.2.0"
|
|
58
57
|
},
|
|
59
58
|
"devDependencies": {
|
|
60
|
-
"effect": "
|
|
61
|
-
"@entifix/testing-unit": "0.
|
|
59
|
+
"effect": "4.0.5",
|
|
60
|
+
"@entifix/testing-unit": "0.2.0"
|
|
62
61
|
},
|
|
63
62
|
"peerDependencies": {
|
|
64
|
-
"effect": "
|
|
63
|
+
"effect": "^4.0.5"
|
|
65
64
|
}
|
|
66
65
|
}
|