@entifix/sql 0.4.2 → 0.5.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`)
|
|
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
|
|
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`)
|
|
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
|
-
//
|
|
122
|
-
//
|
|
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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entifix/sql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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.
|
|
55
|
-
"@entifix/core": "0.
|
|
54
|
+
"@entifix/business": "0.5.0",
|
|
55
|
+
"@entifix/core": "0.5.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"effect": "4.0.5",
|
|
59
|
-
"@entifix/testing-unit": "0.
|
|
59
|
+
"@entifix/testing-unit": "0.5.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
62
|
"effect": "^4.0.5"
|