@shivaedev/sql 0.39.0 → 0.40.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/CHANGELOG.md +6 -0
- package/README.md +3 -3
- package/dist/keysetPage.d.ts +5 -6
- package/dist/keysetPage.d.ts.map +1 -1
- package/dist/keysetPage.js +20 -11
- package/dist/keysetPage.js.map +1 -1
- package/package.json +6 -6
- package/src/keysetPage.ts +28 -20
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ const backend = Effect.gen(function* () {
|
|
|
69
69
|
}).pipe(Effect.scoped, Effect.provide(database));
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
The Layer owns the pool and waits for every socket to close during shutdown. A reserved connection sees committed writes from client transactions in the same database; it does not implicitly join a client's open transaction. `appDatabase` from `@shivaedev/server/appDatabase.ts` and database-backed `appHost` from `@shivaedev/server/
|
|
72
|
+
The Layer owns the pool and waits for every socket to close during shutdown. A reserved connection sees committed writes from client transactions in the same database; it does not implicitly join a client's open transaction. `appDatabase` from `@shivaedev/server/appDatabase.ts` and database-backed `appHost` from `@shivaedev/server/appDatabase.ts` compose this Layer and expose the pool to application services.
|
|
73
73
|
|
|
74
74
|
### SQL names: configure the client once
|
|
75
75
|
|
|
@@ -87,7 +87,7 @@ const database = PgClient.layer(
|
|
|
87
87
|
|
|
88
88
|
`clientOptions` enables Effect SQL's `transformQueryNames` with Effect's `String.camelToSnake` and `transformResultNames` with `String.snakeToCamel` by default. A model field `createdAt` writes the migration-created `created_at` column, and returned rows contain `createdAt`. Inserts, selects, filters, ordering, updates and raw SQL share the client's transforms; column aliases are unnecessary. Write literal migration SQL with snake_case columns. In tagged SQL, interpolated identifiers such as `sql("createdAt")` are transformed; literal SQL text stays as written.
|
|
89
89
|
|
|
90
|
-
To opt out for an existing database with quoted camelCase columns or models with snake_case fields, use `PgClient.layer(clientOptions(options, { snakeCase: false }))`. Existing native clients are unchanged until configured with this helper. Native `transformQueryNames` and `transformResultNames` supplied in `options` take precedence. For `appHost` from `@shivaedev/server/
|
|
90
|
+
To opt out for an existing database with quoted camelCase columns or models with snake_case fields, use `PgClient.layer(clientOptions(options, { snakeCase: false }))`. Existing native clients are unchanged until configured with this helper. Native `transformQueryNames` and `transformResultNames` supplied in `options` take precedence. For `appHost` from `@shivaedev/server/appDatabase.ts` or `appDatabase` from `@shivaedev/server/appDatabase.ts`, use `clientOptions` on their `config` object; the host forwards those native transforms to its client.
|
|
91
91
|
|
|
92
92
|
### Grouped values in flat columns
|
|
93
93
|
|
|
@@ -398,7 +398,7 @@ const rows = yield* paymentsBySequence([]);
|
|
|
398
398
|
|
|
399
399
|
### Keyset pages over native SQL
|
|
400
400
|
|
|
401
|
-
`keysetPage` builds seek and ordering fragments and asks your `read` callback for one extra row. It returns `{ rows, next }`, where `next` is an `Option` containing the last returned row's cursor only when another row exists. Your query keeps its predicates, joins and result schema:
|
|
401
|
+
`keysetPage` builds seek and ordering fragments and asks your `read` callback for one extra row. It returns `{ rows, next }`, where `next` is an `Option` containing the last returned row's cursor only when another row exists. Your query keeps its predicates, joins and result schema. The helper builds native statement fragments without reading `SqlClient`; its service requirements come from your `read` callback:
|
|
402
402
|
|
|
403
403
|
```ts
|
|
404
404
|
import { Effect, Schema } from "effect";
|
package/dist/keysetPage.d.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { Effect, Option, Schema } from "effect";
|
|
2
|
-
import
|
|
3
|
-
import type { Fragment } from "effect/sql/Statement";
|
|
2
|
+
import * as Statement from "effect/sql/Statement";
|
|
4
3
|
export interface KeysetOrder<TCursor> {
|
|
5
|
-
readonly column: string | Fragment;
|
|
4
|
+
readonly column: string | Statement.Fragment;
|
|
6
5
|
readonly direction: "asc" | "desc";
|
|
7
6
|
readonly value: (cursor: TCursor) => string | number | bigint | boolean;
|
|
8
7
|
}
|
|
9
8
|
export interface KeysetPageQuery {
|
|
10
9
|
readonly limit: number;
|
|
11
|
-
readonly orderBy: Fragment;
|
|
12
|
-
readonly seek: Fragment;
|
|
10
|
+
readonly orderBy: Statement.Fragment;
|
|
11
|
+
readonly seek: Statement.Fragment;
|
|
13
12
|
}
|
|
14
13
|
export interface KeysetPage<TRow, TCursor> {
|
|
15
14
|
readonly next: Option.Option<TCursor>;
|
|
@@ -22,5 +21,5 @@ export declare const keysetPage: <TRow, TCursor, E, R>(options: {
|
|
|
22
21
|
readonly tieBreaker: KeysetOrder<TCursor>;
|
|
23
22
|
readonly cursorFor: (row: TRow) => TCursor;
|
|
24
23
|
readonly read: (query: KeysetPageQuery) => Effect.Effect<readonly TRow[], E, R>;
|
|
25
|
-
}) => Effect.Effect<KeysetPage<TRow, TCursor>, E | Schema.SchemaError, R
|
|
24
|
+
}) => Effect.Effect<KeysetPage<TRow, TCursor>, E | Schema.SchemaError, R>;
|
|
26
25
|
//# sourceMappingURL=keysetPage.d.ts.map
|
package/dist/keysetPage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keysetPage.d.ts","sourceRoot":"","sources":["../src/keysetPage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,
|
|
1
|
+
{"version":3,"file":"keysetPage.d.ts","sourceRoot":"","sources":["../src/keysetPage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,KAAK,SAAS,MAAM,sBAAsB,CAAC;AAElD,MAAM,WAAW,WAAW,CAAC,OAAO;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,QAAQ,CAAC;IAC7C,QAAQ,CAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CACxE;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC,QAAQ,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC;CAClC;AAED,MAAM,WAAW,UAAU,CAAC,IAAI,EAAE,OAAO;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,SAAS,IAAI,EAAE,CAAC;CAC/B;AAID,eAAO,MAAM,UAAU,GAAgC,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;oBACzD,MAAM;qBACL,OAAO,GAAG,SAAS;sBAClB,SAAS,WAAW,CAAC,OAAO,CAAC,EAAE;yBAC5B,WAAW,CAAC,OAAO,CAAC;wBACrB,CAAC,GAAG,EAAE,IAAI,KAAK,OAAO;mBAC3B,CAAC,KAAK,EAAE,eAAe,KAAK,MAAM,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;yEAiC9E,CAAC"}
|
package/dist/keysetPage.js
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import { Effect, Option, Schema } from "effect";
|
|
2
|
-
import
|
|
2
|
+
import * as Statement from "effect/sql/Statement";
|
|
3
3
|
const PageSize = Schema.Number.check(Schema.isInt(), Schema.isGreaterThan(0), Schema.isLessThan(Number.MAX_SAFE_INTEGER));
|
|
4
4
|
export const keysetPage = Effect.fnUntraced(function* (options) {
|
|
5
5
|
const limit = yield* Schema.decodeUnknownEffect(PageSize)(options.limit);
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const order = [...options.orderBy, options.tieBreaker].map((part) => ({
|
|
7
|
+
...part,
|
|
8
|
+
column: typeof part.column === "string" ? Statement.fragment([Statement.identifier(part.column)]) : part.column,
|
|
9
|
+
}));
|
|
9
10
|
const after = options.after;
|
|
10
11
|
const values = after === undefined ? undefined : order.map(({ value }) => value(after));
|
|
11
|
-
const
|
|
12
|
-
?
|
|
13
|
-
:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
const conditions = values === undefined
|
|
13
|
+
? []
|
|
14
|
+
: order.map(({ column, direction }, index) => {
|
|
15
|
+
const prefix = order
|
|
16
|
+
.slice(0, index)
|
|
17
|
+
.map(({ column: previousColumn }, previous) => Statement.fragment([...previousColumn.segments, Statement.literal(" = "), Statement.parameter(values[previous])]));
|
|
18
|
+
const comparison = Statement.fragment([
|
|
19
|
+
...column.segments,
|
|
20
|
+
Statement.literal(direction === "asc" ? " > " : " < "),
|
|
21
|
+
Statement.parameter(values[index]),
|
|
22
|
+
]);
|
|
23
|
+
return Statement.and([...prefix, comparison]);
|
|
24
|
+
});
|
|
25
|
+
const seek = values === undefined ? Statement.fragment([Statement.literal("true")]) : Statement.or(conditions);
|
|
26
|
+
const orderBy = Statement.csv(order.map(({ column, direction }) => Statement.fragment([...column.segments, Statement.literal(direction === "asc" ? " asc" : " desc")])));
|
|
18
27
|
const found = yield* options.read({ limit: limit + 1, orderBy, seek });
|
|
19
28
|
const rows = found.slice(0, limit);
|
|
20
29
|
const last = rows.at(-1);
|
package/dist/keysetPage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keysetPage.js","sourceRoot":"","sources":["../src/keysetPage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,
|
|
1
|
+
{"version":3,"file":"keysetPage.js","sourceRoot":"","sources":["../src/keysetPage.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/sql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "Schema-derived repositories over native Effect SQL",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect"
|
|
@@ -51,11 +51,11 @@
|
|
|
51
51
|
"@effect/sql-pg": "4.0.7",
|
|
52
52
|
"@effect/sql-sqlite-node": "4.0.7",
|
|
53
53
|
"@shivaedev/contract": "0.36.1",
|
|
54
|
-
"@shivaedev/jobs": "0.
|
|
55
|
-
"@shivaedev/quality": "0.
|
|
56
|
-
"@shivaedev/test-kits": "0.
|
|
57
|
-
"@shivaedev/test-sql": "0.21.
|
|
58
|
-
"@shivaedev/test-story": "0.39.
|
|
54
|
+
"@shivaedev/jobs": "0.37.0",
|
|
55
|
+
"@shivaedev/quality": "0.62.0",
|
|
56
|
+
"@shivaedev/test-kits": "0.5.0",
|
|
57
|
+
"@shivaedev/test-sql": "0.21.2",
|
|
58
|
+
"@shivaedev/test-story": "0.39.2",
|
|
59
59
|
"@types/node": "24.19.2",
|
|
60
60
|
"effect": "4.0.7",
|
|
61
61
|
"typescript": "7.0.2",
|
package/src/keysetPage.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { Effect, Option, Schema } from "effect";
|
|
2
|
-
import
|
|
3
|
-
import type { Fragment } from "effect/sql/Statement";
|
|
2
|
+
import * as Statement from "effect/sql/Statement";
|
|
4
3
|
|
|
5
4
|
export interface KeysetOrder<TCursor> {
|
|
6
|
-
readonly column: string | Fragment;
|
|
5
|
+
readonly column: string | Statement.Fragment;
|
|
7
6
|
readonly direction: "asc" | "desc";
|
|
8
7
|
readonly value: (cursor: TCursor) => string | number | bigint | boolean;
|
|
9
8
|
}
|
|
10
9
|
|
|
11
10
|
export interface KeysetPageQuery {
|
|
12
11
|
readonly limit: number;
|
|
13
|
-
readonly orderBy: Fragment;
|
|
14
|
-
readonly seek: Fragment;
|
|
12
|
+
readonly orderBy: Statement.Fragment;
|
|
13
|
+
readonly seek: Statement.Fragment;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
export interface KeysetPage<TRow, TCursor> {
|
|
@@ -28,25 +27,34 @@ export const keysetPage = Effect.fnUntraced(function* <TRow, TCursor, E, R>(opti
|
|
|
28
27
|
readonly tieBreaker: KeysetOrder<TCursor>;
|
|
29
28
|
readonly cursorFor: (row: TRow) => TCursor;
|
|
30
29
|
readonly read: (query: KeysetPageQuery) => Effect.Effect<readonly TRow[], E, R>;
|
|
31
|
-
}): Effect.fn.Return<KeysetPage<TRow, TCursor>, E | Schema.SchemaError, R
|
|
30
|
+
}): Effect.fn.Return<KeysetPage<TRow, TCursor>, E | Schema.SchemaError, R> {
|
|
32
31
|
const limit = yield* Schema.decodeUnknownEffect(PageSize)(options.limit);
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
const order = [...options.orderBy, options.tieBreaker].map((part) => ({
|
|
33
|
+
...part,
|
|
34
|
+
column: typeof part.column === "string" ? Statement.fragment([Statement.identifier(part.column)]) : part.column,
|
|
35
|
+
}));
|
|
36
36
|
const after = options.after;
|
|
37
37
|
const values = after === undefined ? undefined : order.map(({ value }) => value(after));
|
|
38
|
-
const
|
|
38
|
+
const conditions =
|
|
39
39
|
values === undefined
|
|
40
|
-
?
|
|
41
|
-
:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
? []
|
|
41
|
+
: order.map(({ column, direction }, index) => {
|
|
42
|
+
const prefix = order
|
|
43
|
+
.slice(0, index)
|
|
44
|
+
.map(({ column: previousColumn }, previous) =>
|
|
45
|
+
Statement.fragment([...previousColumn.segments, Statement.literal(" = "), Statement.parameter(values[previous])]),
|
|
46
|
+
);
|
|
47
|
+
const comparison = Statement.fragment([
|
|
48
|
+
...column.segments,
|
|
49
|
+
Statement.literal(direction === "asc" ? " > " : " < "),
|
|
50
|
+
Statement.parameter(values[index]),
|
|
51
|
+
]);
|
|
52
|
+
return Statement.and([...prefix, comparison]);
|
|
53
|
+
});
|
|
54
|
+
const seek = values === undefined ? Statement.fragment([Statement.literal("true")]) : Statement.or(conditions);
|
|
55
|
+
const orderBy = Statement.csv(
|
|
56
|
+
order.map(({ column, direction }) => Statement.fragment([...column.segments, Statement.literal(direction === "asc" ? " asc" : " desc")])),
|
|
57
|
+
);
|
|
50
58
|
const found = yield* options.read({ limit: limit + 1, orderBy, seek });
|
|
51
59
|
const rows = found.slice(0, limit);
|
|
52
60
|
const last = rows.at(-1);
|