@shivaedev/sql 0.34.0 → 0.35.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 +8 -0
- package/README.md +2 -2
- package/dist/aggregateColumn.d.ts +3 -3
- package/dist/aggregateColumn.d.ts.map +1 -1
- package/dist/aggregateColumn.js +4 -2
- package/dist/aggregateColumn.js.map +1 -1
- package/dist/batchWrites.d.ts +4 -4
- package/dist/batchWrites.d.ts.map +1 -1
- package/dist/batchWrites.js +8 -5
- package/dist/batchWrites.js.map +1 -1
- package/dist/batchedDelete.js +1 -1
- package/dist/batchedDelete.js.map +1 -1
- package/dist/compositeRepository.d.ts +9 -5
- package/dist/compositeRepository.d.ts.map +1 -1
- package/dist/compositeRepository.js +12 -9
- package/dist/compositeRepository.js.map +1 -1
- package/dist/conditionalUpsert.d.ts +4 -4
- package/dist/conditionalUpsert.d.ts.map +1 -1
- package/dist/conditionalUpsert.js +7 -4
- package/dist/conditionalUpsert.js.map +1 -1
- package/dist/conflictWrites.d.ts +4 -4
- package/dist/conflictWrites.d.ts.map +1 -1
- package/dist/conflictWrites.js +11 -8
- package/dist/conflictWrites.js.map +1 -1
- package/dist/insertFromSelect.d.ts +3 -3
- package/dist/insertFromSelect.d.ts.map +1 -1
- package/dist/insertFromSelect.js +5 -3
- package/dist/insertFromSelect.js.map +1 -1
- package/dist/latestPerGroup.d.ts +2 -2
- package/dist/latestPerGroup.d.ts.map +1 -1
- package/dist/latestPerGroup.js +5 -4
- package/dist/latestPerGroup.js.map +1 -1
- package/dist/makeAggregate.d.ts +4 -3
- package/dist/makeAggregate.d.ts.map +1 -1
- package/dist/makeAggregate.js +35 -31
- package/dist/makeAggregate.js.map +1 -1
- package/dist/makeConflictClause.d.ts +3 -3
- package/dist/makeConflictClause.d.ts.map +1 -1
- package/dist/makeConflictClause.js +6 -4
- package/dist/makeConflictClause.js.map +1 -1
- package/dist/makeQueryClauses.d.ts +3 -3
- package/dist/makeQueryClauses.d.ts.map +1 -1
- package/dist/makeQueryClauses.js +5 -3
- package/dist/makeQueryClauses.js.map +1 -1
- package/dist/pinnedGroups.d.ts +3 -4
- package/dist/pinnedGroups.d.ts.map +1 -1
- package/dist/pinnedGroups.js +8 -10
- package/dist/pinnedGroups.js.map +1 -1
- package/dist/repository.d.ts +1 -1
- package/dist/repository.d.ts.map +1 -1
- package/dist/repository.js +7 -9
- package/dist/repository.js.map +1 -1
- package/dist/repositoryChanges.d.ts +2 -2
- package/dist/repositoryChanges.d.ts.map +1 -1
- package/dist/repositoryChanges.js +2 -1
- package/dist/repositoryChanges.js.map +1 -1
- package/dist/repositoryFilter.d.ts +4 -4
- package/dist/repositoryFilter.d.ts.map +1 -1
- package/dist/repositoryFilter.js +5 -3
- package/dist/repositoryFilter.js.map +1 -1
- package/dist/repositoryQueries.d.ts +3 -3
- package/dist/repositoryQueries.d.ts.map +1 -1
- package/dist/repositoryQueries.js +45 -41
- package/dist/repositoryQueries.js.map +1 -1
- package/dist/repositoryWrites.d.ts +9 -5
- package/dist/repositoryWrites.d.ts.map +1 -1
- package/dist/repositoryWrites.js +8 -7
- package/dist/repositoryWrites.js.map +1 -1
- package/dist/tupleIn.d.ts +1 -2
- package/dist/tupleIn.d.ts.map +1 -1
- package/dist/tupleIn.js +16 -17
- package/dist/tupleIn.js.map +1 -1
- package/dist/upsertResult.d.ts +2 -2
- package/dist/upsertResult.d.ts.map +1 -1
- package/dist/upsertResult.js +4 -2
- package/dist/upsertResult.js.map +1 -1
- package/dist/versionedRepository.d.ts +5 -5
- package/dist/versionedRepository.d.ts.map +1 -1
- package/dist/versionedRepository.js +5 -3
- package/dist/versionedRepository.js.map +1 -1
- package/dist/versionedStatements.d.ts +3 -3
- package/dist/versionedStatements.d.ts.map +1 -1
- package/dist/versionedStatements.js +12 -9
- package/dist/versionedStatements.js.map +1 -1
- package/package.json +7 -6
- package/src/aggregateColumn.ts +4 -3
- package/src/batchWrites.ts +10 -10
- package/src/batchedDelete.ts +1 -1
- package/src/compositeRepository.ts +15 -15
- package/src/conditionalUpsert.ts +7 -6
- package/src/conflictWrites.ts +14 -10
- package/src/insertFromSelect.ts +6 -6
- package/src/latestPerGroup.ts +5 -4
- package/src/makeAggregate.ts +48 -47
- package/src/makeConflictClause.ts +6 -6
- package/src/makeQueryClauses.ts +6 -6
- package/src/pinnedGroups.ts +10 -14
- package/src/repository.ts +8 -9
- package/src/repositoryChanges.ts +2 -6
- package/src/repositoryFilter.ts +5 -4
- package/src/repositoryQueries.ts +57 -55
- package/src/repositoryWrites.ts +9 -9
- package/src/tupleIn.ts +16 -18
- package/src/upsertResult.ts +4 -3
- package/src/versionedRepository.ts +6 -6
- package/src/versionedStatements.ts +15 -15
- package/dist/testing.d.ts +0 -27
- package/dist/testing.d.ts.map +0 -1
- package/dist/testing.js +0 -75
- package/dist/testing.js.map +0 -1
- package/src/testing.ts +0 -103
package/src/makeQueryClauses.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
|
-
import
|
|
2
|
+
import { SqlClient } from "effect/sql";
|
|
3
3
|
import type { Fragment } from "effect/sql/Statement";
|
|
4
4
|
|
|
5
5
|
const Limit = Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0));
|
|
@@ -9,11 +9,11 @@ interface Order {
|
|
|
9
9
|
readonly field: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export
|
|
13
|
-
sql: SqlClient.SqlClient,
|
|
12
|
+
export const makeQueryClauses = Effect.fnUntraced(function* (
|
|
14
13
|
fieldAt: (key: string) => Effect.Effect<unknown, Schema.SchemaError>,
|
|
15
|
-
column
|
|
14
|
+
column?: (key: string) => Fragment,
|
|
16
15
|
) {
|
|
16
|
+
const sql = yield* SqlClient.SqlClient;
|
|
17
17
|
function orderClause(orderBy: Order | readonly Order[] | undefined) {
|
|
18
18
|
if (orderBy === undefined) {
|
|
19
19
|
return Effect.succeed(sql``);
|
|
@@ -24,7 +24,7 @@ export function makeQueryClauses(
|
|
|
24
24
|
}
|
|
25
25
|
return Effect.map(
|
|
26
26
|
Effect.forEach(entries, ({ field, direction }) =>
|
|
27
|
-
Effect.as(fieldAt(field), sql`${column(field)} ${sql.literal(direction === "desc" ? "desc" : "asc")}`),
|
|
27
|
+
Effect.as(fieldAt(field), sql`${column?.(field) ?? sql`${sql(field)}`} ${sql.literal(direction === "desc" ? "desc" : "asc")}`),
|
|
28
28
|
),
|
|
29
29
|
(columns) => sql`order by ${sql.csv(columns)}`,
|
|
30
30
|
);
|
|
@@ -33,4 +33,4 @@ export function makeQueryClauses(
|
|
|
33
33
|
return limit === undefined ? Effect.succeed(sql``) : Effect.map(decodeLimit(limit), (rowLimit) => sql`limit ${rowLimit}`);
|
|
34
34
|
}
|
|
35
35
|
return { decodeLimit, limitClause, orderClause };
|
|
36
|
-
}
|
|
36
|
+
});
|
package/src/pinnedGroups.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
import type { Model } from "effect/schema";
|
|
3
|
-
import type { SqlClient } from "effect/sql";
|
|
4
3
|
import type { RepositoryListFilter } from "#repositoryFilter.ts";
|
|
5
4
|
import { makeFilter } from "#repositoryFilter.ts";
|
|
6
5
|
import { tupleFilter } from "#tupleIn.ts";
|
|
7
6
|
|
|
8
|
-
export
|
|
7
|
+
export const pinnedGroups = Effect.fnUntraced(function* (
|
|
9
8
|
model: Model.Any,
|
|
10
|
-
sql: SqlClient.SqlClient,
|
|
11
9
|
partitionBy: readonly string[],
|
|
12
10
|
input: RepositoryListFilter<Model.Any> = {},
|
|
13
11
|
) {
|
|
@@ -18,7 +16,7 @@ export function pinnedGroups(
|
|
|
18
16
|
(value) => typeof value === "object" && value !== null && "in" in value && Array.isArray(value.in) && value.in.length === 0,
|
|
19
17
|
)
|
|
20
18
|
) {
|
|
21
|
-
return
|
|
19
|
+
return { groups: [], remaining: input, tuple: undefined };
|
|
22
20
|
}
|
|
23
21
|
const exactTuple = tuple !== undefined && tuple.fields.length === partitionBy.length && partitionBy.every((key) => tuple.fields.includes(key));
|
|
24
22
|
let groups: readonly (readonly unknown[])[] = [[]];
|
|
@@ -30,19 +28,17 @@ export function pinnedGroups(
|
|
|
30
28
|
for (const key of partitionBy) {
|
|
31
29
|
const value = input[key];
|
|
32
30
|
if (value === undefined) {
|
|
33
|
-
return
|
|
31
|
+
return undefined;
|
|
34
32
|
}
|
|
35
33
|
const values = typeof value === "object" && value !== null && "in" in value && Array.isArray(value.in) ? value.in : [value];
|
|
36
34
|
groups = groups.flatMap((group) => values.map((entry) => [...group, entry]));
|
|
37
35
|
delete remaining[key];
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
|
-
const { fieldAt } = makeFilter(model
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
});
|
|
48
|
-
}
|
|
38
|
+
const { fieldAt } = yield* makeFilter(model);
|
|
39
|
+
const fields = yield* Effect.forEach(partitionBy, fieldAt);
|
|
40
|
+
const groupsEncoded: readonly (readonly unknown[])[] = yield* Effect.forEach(groups, (group) =>
|
|
41
|
+
Schema.encodeUnknownEffect(Schema.Tuple(fields))(group),
|
|
42
|
+
);
|
|
43
|
+
return { groups: groupsEncoded, remaining, tuple: exactTuple ? tuple : undefined };
|
|
44
|
+
});
|
package/src/repository.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Data, Effect, type Schema } from "effect";
|
|
2
2
|
import type { Model } from "effect/schema";
|
|
3
|
-
import { SqlClient } from "effect/sql";
|
|
3
|
+
import type { SqlClient } from "effect/sql";
|
|
4
4
|
import { makeBatchedDelete } from "#batchedDelete.ts";
|
|
5
5
|
import { type BatchWrites, makeInsertMany } from "#batchWrites.ts";
|
|
6
6
|
import { makeCompositeCrud, type PrimaryKey, type Repository } from "#compositeRepository.ts";
|
|
@@ -117,27 +117,26 @@ export function makeRepository<TModel extends Model.Any, TId extends IdColumn<TM
|
|
|
117
117
|
): Effect.Effect<unknown, BucketRegistrationError, SqlClient.SqlClient> {
|
|
118
118
|
return Effect.fnUntraced(function* () {
|
|
119
119
|
yield* validateRepositoryCollection(model.fields, options);
|
|
120
|
-
const sql = yield* SqlClient.SqlClient;
|
|
121
120
|
const composite = typeof options.idColumn === "object";
|
|
122
121
|
const idColumns = (composite ? options.idColumn : [options.idColumn]).map(String);
|
|
123
|
-
const changes = yield* repositoryChanges(
|
|
122
|
+
const changes = yield* repositoryChanges(options.tableName, idColumns, model.fields);
|
|
124
123
|
const crud = {
|
|
125
|
-
...makeCompositeCrud(model,
|
|
124
|
+
...(yield* makeCompositeCrud(model, { ...options, columns: idColumns }, changes)),
|
|
126
125
|
deleteBatched: yield* makeBatchedDelete(model, options, changes),
|
|
127
126
|
};
|
|
128
|
-
const queries = makeQueries(model,
|
|
127
|
+
const queries = yield* makeQueries(model, options);
|
|
129
128
|
return {
|
|
130
129
|
...crud,
|
|
131
130
|
...queries,
|
|
132
131
|
...(options.versionColumn === undefined
|
|
133
132
|
? {
|
|
134
|
-
insertFromSelect: makeInsertFromSelect(model,
|
|
135
|
-
insertMany: makeInsertMany(model,
|
|
133
|
+
insertFromSelect: yield* makeInsertFromSelect(model, { ...options, changes }, "insertFromSelect"),
|
|
134
|
+
insertMany: yield* makeInsertMany(model, { ...options, changes }, "insertMany"),
|
|
136
135
|
...(options.conflictColumns === undefined
|
|
137
136
|
? {}
|
|
138
|
-
: makeConflictWrites(model,
|
|
137
|
+
: yield* makeConflictWrites(model, { ...options, changes, conflictColumns: options.conflictColumns, idColumns })),
|
|
139
138
|
}
|
|
140
|
-
: makeVersionedWrites(model,
|
|
139
|
+
: yield* makeVersionedWrites(model, {
|
|
141
140
|
...options,
|
|
142
141
|
changes,
|
|
143
142
|
conflict: (id, expectedVersion, reason) => new OptimisticConcurrencyError({ expectedVersion, id, reason, tableName: options.tableName }),
|
package/src/repositoryChanges.ts
CHANGED
|
@@ -8,12 +8,8 @@ import { invalidateOnCommit, transact } from "#transact.ts";
|
|
|
8
8
|
|
|
9
9
|
export type RepositoryChanges = Effect.Success<ReturnType<typeof repositoryChanges>>;
|
|
10
10
|
|
|
11
|
-
export const repositoryChanges = Effect.fnUntraced(function* (
|
|
12
|
-
sql
|
|
13
|
-
table: string,
|
|
14
|
-
columns: readonly string[],
|
|
15
|
-
fields: Schema.Struct.Fields = {},
|
|
16
|
-
) {
|
|
11
|
+
export const repositoryChanges = Effect.fnUntraced(function* (table: string, columns: readonly string[], fields: Schema.Struct.Fields = {}) {
|
|
12
|
+
const sql = yield* SqlClient.SqlClient;
|
|
17
13
|
const buckets = declaredBuckets(fields);
|
|
18
14
|
const bucketKeys = Effect.fnUntraced(function* (row: Readonly<Record<string, unknown>>) {
|
|
19
15
|
const keys: Key[] = [];
|
package/src/repositoryFilter.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect, Schema, SchemaIssue } from "effect";
|
|
2
2
|
import type { Model } from "effect/schema";
|
|
3
|
-
import
|
|
3
|
+
import { SqlClient } from "effect/sql";
|
|
4
4
|
import { type TupleFilter, tuplePredicate } from "#tupleIn.ts";
|
|
5
5
|
|
|
6
6
|
export type RepositoryFilter<TModel extends Model.Any> = {
|
|
@@ -34,7 +34,8 @@ function isOperator(key: string): key is keyof typeof operators {
|
|
|
34
34
|
return Object.hasOwn(operators, key);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export function
|
|
37
|
+
export const makeFilter = Effect.fnUntraced(function* <TModel extends Model.Any>(model: TModel, references?: { readonly tableName: string }) {
|
|
38
|
+
const sql = yield* SqlClient.SqlClient;
|
|
38
39
|
function fieldAt(key: string) {
|
|
39
40
|
const field = Object.hasOwn(model.fields, key) ? model.fields[key] : undefined;
|
|
40
41
|
return field === undefined
|
|
@@ -105,7 +106,7 @@ export function makeFilter<TModel extends Model.Any>(model: TModel, sql: SqlClie
|
|
|
105
106
|
([key, value]) => predicate(key, value, allowIn, allowComparisons),
|
|
106
107
|
);
|
|
107
108
|
if (allowIn) {
|
|
108
|
-
const tuple = yield* tuplePredicate(input,
|
|
109
|
+
const tuple = yield* tuplePredicate(input, fieldAt).pipe(Effect.provideService(SqlClient.SqlClient, sql));
|
|
109
110
|
if (tuple !== undefined) {
|
|
110
111
|
predicates.push(tuple);
|
|
111
112
|
}
|
|
@@ -114,4 +115,4 @@ export function makeFilter<TModel extends Model.Any>(model: TModel, sql: SqlClie
|
|
|
114
115
|
});
|
|
115
116
|
}
|
|
116
117
|
return { fieldAt, where };
|
|
117
|
-
}
|
|
118
|
+
});
|
package/src/repositoryQueries.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
import type { Model } from "effect/schema";
|
|
3
|
-
import
|
|
3
|
+
import { SqlClient } from "effect/sql";
|
|
4
4
|
import type { SqlError } from "effect/sql/SqlError";
|
|
5
5
|
import type { Fragment } from "effect/sql/Statement";
|
|
6
6
|
import type { Aggregate } from "#aggregateQuery.ts";
|
|
@@ -46,62 +46,64 @@ export interface Queries<TModel extends Model.Any> {
|
|
|
46
46
|
|
|
47
47
|
export function makeQueries<TModel extends Model.Any>(
|
|
48
48
|
model: TModel,
|
|
49
|
-
sql: SqlClient.SqlClient,
|
|
50
49
|
options: {
|
|
51
50
|
readonly tableName: string;
|
|
52
51
|
readonly spanPrefix: string;
|
|
53
52
|
},
|
|
54
|
-
): Queries<TModel> {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
53
|
+
): Effect.Effect<Queries<TModel>, never, SqlClient.SqlClient> {
|
|
54
|
+
return Effect.fnUntraced(function* () {
|
|
55
|
+
const sql = yield* SqlClient.SqlClient;
|
|
56
|
+
const { fieldAt, where } = yield* makeFilter(model);
|
|
57
|
+
const readLatest = yield* makeLatestPerGroup(model, options.tableName);
|
|
58
|
+
const { decodeLimit, orderClause, limitClause } = yield* makeQueryClauses(fieldAt);
|
|
59
|
+
const aggregate = yield* makeAggregate(model, options);
|
|
60
|
+
function count(query?: Pick<FindMany<TModel, Key<TModel>>, "where">): ReturnType<Queries<TModel>["count"]>;
|
|
61
|
+
function count(query: Pick<FindMany<TModel, Key<TModel>>, "where"> = {}): Effect.Effect<number, SqlError | Schema.SchemaError, unknown> {
|
|
62
|
+
return Effect.gen(function* () {
|
|
63
|
+
const predicate = yield* where(query.where, true, true);
|
|
64
|
+
const rows = yield* sql`select cast(count(*) as text) as count from ${sql(options.tableName)} where ${predicate}`;
|
|
65
|
+
const [result] = yield* Schema.decodeUnknownEffect(CountResult)(rows);
|
|
66
|
+
return result.count;
|
|
67
|
+
}).pipe(Effect.withSpan(`${options.spanPrefix}.count`));
|
|
68
|
+
}
|
|
69
|
+
function select<TKey extends Key<TModel> = Key<TModel>>(
|
|
70
|
+
query?: FindMany<TModel, TKey>,
|
|
71
|
+
): RepositorySelect<Pick<TModel["fields"], TKey>, TModel["fields"][Key<TModel>]["EncodingServices"]>;
|
|
72
|
+
function select(query: FindMany<TModel, Key<TModel>> = {}): RepositorySelect<Schema.Struct.Fields, unknown> {
|
|
73
|
+
const keys = query.select ?? Object.keys(model.fields);
|
|
74
|
+
const fields = Object.fromEntries(keys.map((key) => [key, model.fields[key]]));
|
|
75
|
+
const statement = Effect.gen(function* () {
|
|
76
|
+
yield* Effect.forEach(keys, fieldAt);
|
|
77
|
+
const predicate = yield* where(query.where, true, true);
|
|
78
|
+
const order = yield* orderClause(query.orderBy);
|
|
79
|
+
const limit = yield* limitClause(query.limit);
|
|
80
|
+
return sql`select ${sql.csv(keys.map((key) => sql`${sql(key)}`))} from ${sql(options.tableName)} where ${predicate} ${order} ${limit}`;
|
|
81
|
+
});
|
|
82
|
+
return { fields, statement };
|
|
83
|
+
}
|
|
84
|
+
function findMany<TKey extends Key<TModel> = Key<TModel>>(
|
|
85
|
+
query?: FindMany<TModel, TKey>,
|
|
86
|
+
): Effect.Effect<
|
|
87
|
+
Pick<Row<TModel>, TKey>[],
|
|
88
|
+
SqlError | Schema.SchemaError,
|
|
89
|
+
TModel["fields"][Key<TModel>]["EncodingServices"] | TModel["fields"][TKey]["DecodingServices"]
|
|
90
|
+
>;
|
|
91
|
+
// Object.fromEntries erases the selected key-to-schema correspondence, so the typed overload restates it.
|
|
92
|
+
function findMany(query: FindMany<TModel, Key<TModel>> = {}): Effect.Effect<unknown, SqlError | Schema.SchemaError, unknown> {
|
|
93
|
+
return Effect.gen(function* () {
|
|
94
|
+
const selected = select(query);
|
|
95
|
+
const rows = yield* sql`${yield* selected.statement}`;
|
|
96
|
+
return yield* Schema.decodeUnknownEffect(Schema.Array(Schema.Struct(selected.fields)))(rows);
|
|
97
|
+
}).pipe(Effect.withSpan(`${options.spanPrefix}.findMany`));
|
|
98
|
+
}
|
|
99
|
+
function latestPerGroup(query: LatestPerGroup<TModel>): ReturnType<Queries<TModel>["latestPerGroup"]>;
|
|
100
|
+
function latestPerGroup(query: LatestPerGroup<TModel>): Effect.Effect<unknown, SqlError | Schema.SchemaError, unknown> {
|
|
101
|
+
return Effect.gen(function* () {
|
|
102
|
+
const order = yield* orderClause(query.orderBy);
|
|
103
|
+
const rowLimit = yield* decodeLimit(query.limit);
|
|
104
|
+
return yield* readLatest(query, order, rowLimit);
|
|
105
|
+
}).pipe(Effect.withSpan(`${options.spanPrefix}.latestPerGroup`));
|
|
106
|
+
}
|
|
107
|
+
return { aggregate, count, findMany, latestPerGroup, select };
|
|
108
|
+
})();
|
|
107
109
|
}
|
package/src/repositoryWrites.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
import type { Model } from "effect/schema";
|
|
3
|
-
import {
|
|
4
|
-
import type { RepositoryChanges } from "
|
|
5
|
-
import { makeFilter } from "
|
|
6
|
-
import { selectedUpdate } from "
|
|
3
|
+
import { SqlClient, SqlSchema } from "effect/sql";
|
|
4
|
+
import type { RepositoryChanges } from "#repositoryChanges.ts";
|
|
5
|
+
import { makeFilter } from "#repositoryFilter.ts";
|
|
6
|
+
import { selectedUpdate } from "#repositoryUpdate.ts";
|
|
7
7
|
|
|
8
|
-
export
|
|
8
|
+
export const repositoryWrites = Effect.fnUntraced(function* (
|
|
9
9
|
model: Model.Any,
|
|
10
|
-
sql: SqlClient.SqlClient,
|
|
11
10
|
options: { readonly idColumns: readonly string[]; readonly tableName: string; readonly spanPrefix: string },
|
|
12
11
|
changes: RepositoryChanges,
|
|
13
12
|
) {
|
|
13
|
+
const sql = yield* SqlClient.SqlClient;
|
|
14
14
|
const { idColumns, tableName, spanPrefix } = options;
|
|
15
|
-
const { where } = makeFilter(model
|
|
15
|
+
const { where } = yield* makeFilter(model);
|
|
16
16
|
const record = Schema.decodeUnknownEffect(Schema.Record(Schema.String, Schema.Unknown));
|
|
17
17
|
const filterRecord = Schema.decodeUnknownEffect(Schema.Record(Schema.PropertyKey, Schema.Unknown));
|
|
18
18
|
function predicate(input: Readonly<Record<string, unknown>>) {
|
|
@@ -49,7 +49,7 @@ export function repositoryWrites(
|
|
|
49
49
|
const idColumn = idColumns[0] ?? "";
|
|
50
50
|
const key = model.fields[idColumn];
|
|
51
51
|
if (key === undefined) {
|
|
52
|
-
|
|
52
|
+
return yield* Effect.die(new Error("Unknown repository ID column"));
|
|
53
53
|
}
|
|
54
54
|
const isKey = Schema.is(key);
|
|
55
55
|
const removeByKey = SqlSchema.void({
|
|
@@ -84,4 +84,4 @@ export function repositoryWrites(
|
|
|
84
84
|
.write(settings === undefined ? updateVoid(input) : selected(input, settings.columns, false))
|
|
85
85
|
.pipe(Effect.withSpan(`${spanPrefix}.updateVoid`)),
|
|
86
86
|
};
|
|
87
|
-
}
|
|
87
|
+
});
|
package/src/tupleIn.ts
CHANGED
|
@@ -51,27 +51,25 @@ export function tupleIn<TModel extends Model.Any, const TFields extends readonly
|
|
|
51
51
|
return { [tupleFilter]: { fields, model, types: fields.map((field: TFields[number]) => types[field]), values } };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
export
|
|
54
|
+
export const tuplePredicate = Effect.fnUntraced(function* (
|
|
55
55
|
input: Partial<TupleFilter<Model.Any>>,
|
|
56
|
-
sql: SqlClient.SqlClient,
|
|
57
56
|
fieldAt: (key: string) => Effect.Effect<Schema.Constraint, Schema.SchemaError>,
|
|
58
57
|
) {
|
|
59
58
|
const tuple = input[tupleFilter];
|
|
60
59
|
if (tuple === undefined) {
|
|
61
|
-
return
|
|
60
|
+
return undefined;
|
|
62
61
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
62
|
+
const sql = yield* SqlClient.SqlClient;
|
|
63
|
+
const fields = yield* Effect.forEach(tuple.fields, fieldAt);
|
|
64
|
+
const encode = Schema.encodeUnknownEffect(Schema.Tuple(fields));
|
|
65
|
+
const columns = sql.csv(tuple.fields.map((field) => sql`${sql(field)}`));
|
|
66
|
+
const rows: readonly (readonly unknown[])[] = yield* Effect.forEach(tuple.values, (value) => encode(value));
|
|
67
|
+
if (rows.length === 0) {
|
|
68
|
+
return sql`1 = 0`;
|
|
69
|
+
}
|
|
70
|
+
if (sql.onDialectOrElse({ orElse: () => false, pg: () => true })) {
|
|
71
|
+
const relation = yield* tupleUnnest(tuple.types, rows).pipe(Effect.provideService(SqlClient.SqlClient, sql));
|
|
72
|
+
return sql`(${columns}) in (select * from ${relation})`;
|
|
73
|
+
}
|
|
74
|
+
return sql.or(rows.map((row) => sql`(${columns}) = (${sql.csv(row.map((entry) => sql`${entry}`))})`));
|
|
75
|
+
});
|
package/src/upsertResult.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
import type { Model } from "effect/schema";
|
|
3
|
-
import
|
|
3
|
+
import { SqlClient } from "effect/sql";
|
|
4
4
|
|
|
5
5
|
export interface UpsertRow<TRow> {
|
|
6
6
|
readonly inserted: boolean;
|
|
7
7
|
readonly row: TRow;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export
|
|
10
|
+
export const upsertReturning = Effect.fnUntraced(function* () {
|
|
11
|
+
const sql = yield* SqlClient.SqlClient;
|
|
11
12
|
return sql`*, (xmax = 0) as ${sql("effectSqlInserted")}`;
|
|
12
|
-
}
|
|
13
|
+
});
|
|
13
14
|
|
|
14
15
|
export function makeUpsertDecoder(model: Model.Any) {
|
|
15
16
|
const decode = Schema.decodeUnknownEffect(model);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Effect, Option, Schema } from "effect";
|
|
2
2
|
import type { Model } from "effect/schema";
|
|
3
|
-
import
|
|
3
|
+
import { SqlClient, type SqlModel } from "effect/sql";
|
|
4
4
|
import type { SqlError } from "effect/sql/SqlError";
|
|
5
5
|
import type { PrimaryKey, Repository } from "#compositeRepository.ts";
|
|
6
6
|
import type { OptimisticConcurrencyError } from "#repository.ts";
|
|
7
|
+
import type { RepositoryChanges } from "#repositoryChanges.ts";
|
|
7
8
|
import type { UpdateInput } from "#repositoryUpdate.ts";
|
|
8
9
|
import { type VersionedOptions, versionedStatements } from "#versionedStatements.ts";
|
|
9
|
-
import type { RepositoryChanges } from "./repositoryChanges.ts";
|
|
10
10
|
|
|
11
11
|
export type IdColumn<TModel extends Model.Any> = keyof TModel["Type"] & keyof TModel["update"]["Type"] & keyof TModel["fields"];
|
|
12
12
|
export type VersionColumn<TModel extends Model.Any, TId extends PrimaryKey<TModel>> = {
|
|
@@ -43,13 +43,13 @@ export interface VersionedWrites<TModel extends Model.Any, TId extends PrimaryKe
|
|
|
43
43
|
readonly updateVoid: (input: UpdateInput<TModel>) => Effect.Effect<void, WriteError, TModel["update"]["EncodingServices"]>;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export
|
|
46
|
+
export const makeVersionedWrites = Effect.fnUntraced(function* (
|
|
47
47
|
model: Model.Any,
|
|
48
|
-
sql: SqlClient.SqlClient,
|
|
49
48
|
options: VersionedOptions & { readonly changes: RepositoryChanges; readonly spanPrefix: string },
|
|
50
49
|
) {
|
|
50
|
+
const sql = yield* SqlClient.SqlClient;
|
|
51
51
|
const { spanPrefix, changes } = options;
|
|
52
|
-
const statements = versionedStatements(model,
|
|
52
|
+
const statements = yield* versionedStatements(model, options);
|
|
53
53
|
const decodeRow = Schema.decodeUnknownEffect(model);
|
|
54
54
|
function insert(input: unknown) {
|
|
55
55
|
return Effect.gen(function* () {
|
|
@@ -106,4 +106,4 @@ export function makeVersionedWrites(
|
|
|
106
106
|
}).pipe(changes.write, Effect.withSpan(`${spanPrefix}.delete`));
|
|
107
107
|
}
|
|
108
108
|
return { delete: deleteRow, insert, insertVoid, update, updateVoid, ...(options.conflictColumns === undefined ? {} : { insertOrIgnore }) };
|
|
109
|
-
}
|
|
109
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
import type { Model } from "effect/schema";
|
|
3
|
-
import
|
|
3
|
+
import { SqlClient } from "effect/sql";
|
|
4
4
|
import { makeConflictClause } from "#makeConflictClause.ts";
|
|
5
5
|
import type { OptimisticConcurrencyError } from "#repository.ts";
|
|
6
6
|
|
|
@@ -14,21 +14,21 @@ export interface VersionedOptions {
|
|
|
14
14
|
readonly versionColumn: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export
|
|
17
|
+
export const versionedStatements = Effect.fnUntraced(function* (model: Model.Any, options: VersionedOptions) {
|
|
18
|
+
const sql = yield* SqlClient.SqlClient;
|
|
18
19
|
const { tableName, idColumns, versionColumn, conflict } = options;
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
);
|
|
28
|
-
const encodeKey = Schema.encodeEffect(Schema.Struct(fields));
|
|
20
|
+
const entries: [string, Schema.Constraint][] = [];
|
|
21
|
+
for (const column of idColumns) {
|
|
22
|
+
const field = model.fields[column];
|
|
23
|
+
if (field === undefined) {
|
|
24
|
+
return yield* Effect.die(new Error(`Unknown ID column: ${column}`));
|
|
25
|
+
}
|
|
26
|
+
entries.push([column, field]);
|
|
27
|
+
}
|
|
28
|
+
const encodeKey = Schema.encodeEffect(Schema.Struct(Object.fromEntries(entries)));
|
|
29
29
|
const versionSchema = model.fields[versionColumn];
|
|
30
30
|
if (versionSchema === undefined) {
|
|
31
|
-
|
|
31
|
+
return yield* Effect.die(new Error(`Unknown version column: ${versionColumn}`));
|
|
32
32
|
}
|
|
33
33
|
const encodeVersion = Schema.encodeEffect(versionSchema);
|
|
34
34
|
const encodeInsert = Schema.encodeEffect(model.insert);
|
|
@@ -36,7 +36,7 @@ export function versionedStatements(model: Model.Any, sql: SqlClient.SqlClient,
|
|
|
36
36
|
const record = Schema.decodeUnknownEffect(Schema.Record(Schema.String, Schema.Unknown));
|
|
37
37
|
const validateVersion = Schema.decodeUnknownEffect(Version);
|
|
38
38
|
const clause =
|
|
39
|
-
options.conflictColumns === undefined ? undefined : makeConflictClause(model,
|
|
39
|
+
options.conflictColumns === undefined ? undefined : yield* makeConflictClause(model, { ...options, conflictColumns: options.conflictColumns });
|
|
40
40
|
function key(input: unknown) {
|
|
41
41
|
return idColumns.length === 1 ? { [idColumns[0] ?? ""]: input } : input;
|
|
42
42
|
}
|
|
@@ -86,4 +86,4 @@ export function versionedStatements(model: Model.Any, sql: SqlClient.SqlClient,
|
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
88
|
return { insert, remove, update };
|
|
89
|
-
}
|
|
89
|
+
});
|
package/dist/testing.d.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { Effect, Schema } from "effect";
|
|
2
|
-
import { SqlClient } from "effect/sql";
|
|
3
|
-
import { type BucketKey, type ItemKey } from "@shivaedev/changes/keys.ts";
|
|
4
|
-
declare const UnannouncedWrites_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
5
|
-
readonly _tag: "UnannouncedWrites";
|
|
6
|
-
} & Readonly<A>;
|
|
7
|
-
export declare class UnannouncedWrites extends UnannouncedWrites_base<{
|
|
8
|
-
readonly tables: readonly string[];
|
|
9
|
-
}> {
|
|
10
|
-
}
|
|
11
|
-
export interface ChangeCoverage {
|
|
12
|
-
readonly covers: Readonly<Record<string, readonly (ItemKey | BucketKey)[]>>;
|
|
13
|
-
}
|
|
14
|
-
export declare const withChangeCoverage: <A, E, R>(body: Effect.Effect<A, E, R>, options?: ChangeCoverage | undefined) => Effect.Effect<A, Schema.SchemaError | import("effect/sql/SqlError").SqlError | UnannouncedWrites | Exclude<E, import("effect/sql/SqlError").SqlError>, import("effect/reactivity/Reactivity").Reactivity | SqlClient.SqlClient | Exclude<Exclude<R, never>, never>>;
|
|
15
|
-
declare const PublishedKeysMismatch_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
16
|
-
readonly _tag: "PublishedKeysMismatch";
|
|
17
|
-
} & Readonly<A>;
|
|
18
|
-
export declare class PublishedKeysMismatch extends PublishedKeysMismatch_base<{
|
|
19
|
-
readonly expected: readonly unknown[];
|
|
20
|
-
readonly actual: readonly unknown[];
|
|
21
|
-
}> {
|
|
22
|
-
get message(): string;
|
|
23
|
-
}
|
|
24
|
-
export declare function expectPublished(keys: readonly unknown[]): <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E | PublishedKeysMismatch, R>;
|
|
25
|
-
export declare function expectPublished<A>(keys: (result: A) => readonly unknown[]): <E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E | PublishedKeysMismatch, R>;
|
|
26
|
-
export {};
|
|
27
|
-
//# sourceMappingURL=testing.d.ts.map
|
package/dist/testing.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,OAAO,EAAoB,MAAM,4BAA4B,CAAC;;;;AAI5F,qBAAa,iBAAkB,SAAQ,uBAAsC;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;CAAG;AAEvH,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,OAAO,GAAG,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC;CAC5E;AAED,eAAO,MAAM,kBAAkB,GAAsD,CAAC,EAAE,CAAC,EAAE,CAAC,4UAuD1F,CAAC;;;;AAEH,qBAAa,qBAAsB,SAAQ,2BAA0C;IACpF,QAAQ,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC;CACpC,CAAC;IACD,IAAa,OAAO,WAEnB;CACD;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,qBAAqB,EAAE,CAAC,CAAC,CAAC;AACrJ,wBAAgB,eAAe,CAAC,CAAC,EAChC,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,SAAS,OAAO,EAAE,GACrC,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,qBAAqB,EAAE,CAAC,CAAC,CAAC"}
|
package/dist/testing.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { Data, Effect, Schema } from "effect";
|
|
2
|
-
import { SqlClient } from "effect/sql";
|
|
3
|
-
import { invalidationKeys } from "@shivaedev/changes/keys.ts";
|
|
4
|
-
import { ChangeDeclarations } from "#ChangeDeclarations.ts";
|
|
5
|
-
import { ChangeObserver, transact } from "#transact.ts";
|
|
6
|
-
export class UnannouncedWrites extends Data.TaggedError("UnannouncedWrites") {
|
|
7
|
-
}
|
|
8
|
-
export const withChangeCoverage = Effect.fn("Changes.withChangeCoverage")(function* (body, options) {
|
|
9
|
-
const observer = yield* ChangeObserver;
|
|
10
|
-
const sql = yield* SqlClient.SqlClient;
|
|
11
|
-
const named = new Set();
|
|
12
|
-
const declarations = new Map();
|
|
13
|
-
const outer = yield* ChangeDeclarations;
|
|
14
|
-
const written = sql `select relid, relname, (n_tup_ins + n_tup_upd + n_tup_del)::int as writes from pg_stat_xact_user_tables`.pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(Schema.Struct({ relid: Schema.Int, relname: Schema.String, writes: Schema.Int })))));
|
|
15
|
-
const checked = Effect.gen(function* () {
|
|
16
|
-
const before = new Map((yield* written).map((row) => [row.relid, row.writes]));
|
|
17
|
-
const result = yield* body;
|
|
18
|
-
const missing = (yield* written).filter((row) => {
|
|
19
|
-
const count = row.writes - (before.get(row.relid) ?? 0);
|
|
20
|
-
if (count <= 0) {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
const automatic = declarations.get(row.relname);
|
|
24
|
-
const explicit = options?.covers[row.relname] ?? [];
|
|
25
|
-
const expected = [...(automatic?.keys ?? []), ...invalidationKeys(explicit)];
|
|
26
|
-
return (count > (automatic?.rows ?? 0) && explicit.length === 0) || expected.some((key) => !named.has(key));
|
|
27
|
-
});
|
|
28
|
-
if (missing.length > 0) {
|
|
29
|
-
return yield* Effect.fail(new UnannouncedWrites({ tables: missing.map((row) => row.relname).sort() }));
|
|
30
|
-
}
|
|
31
|
-
return result;
|
|
32
|
-
});
|
|
33
|
-
return yield* checked.pipe(transact({ onSqlError: (error) => error }), Effect.provideService(ChangeDeclarations, {
|
|
34
|
-
declare: (table, keys, rows) => Effect.andThen(Effect.sync(() => {
|
|
35
|
-
const entry = declarations.get(table) ?? { keys: new Set(), rows: 0 };
|
|
36
|
-
for (const key of keys) {
|
|
37
|
-
entry.keys.add(key);
|
|
38
|
-
}
|
|
39
|
-
entry.rows += rows;
|
|
40
|
-
declarations.set(table, entry);
|
|
41
|
-
}), outer.declare(table, keys, rows)),
|
|
42
|
-
}), Effect.provideService(ChangeObserver, (event) => {
|
|
43
|
-
if (event._tag === "Recorded") {
|
|
44
|
-
for (const key of event.changes) {
|
|
45
|
-
named.add(key);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return observer(event);
|
|
49
|
-
}));
|
|
50
|
-
});
|
|
51
|
-
export class PublishedKeysMismatch extends Data.TaggedError("PublishedKeysMismatch") {
|
|
52
|
-
get message() {
|
|
53
|
-
return `Expected published keys ${JSON.stringify(this.expected)}; actual published keys ${JSON.stringify(this.actual)}`;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
export function expectPublished(keys) {
|
|
57
|
-
return Effect.fn("Changes.expectPublished")(function* (body) {
|
|
58
|
-
const observer = yield* ChangeObserver;
|
|
59
|
-
const actual = new Set();
|
|
60
|
-
const result = yield* body.pipe(Effect.provideService(ChangeObserver, (event) => {
|
|
61
|
-
if (event._tag === "Published") {
|
|
62
|
-
for (const key of event.changes) {
|
|
63
|
-
actual.add(key);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return observer(event);
|
|
67
|
-
}));
|
|
68
|
-
const expected = new Set(typeof keys === "function" ? keys(result) : keys);
|
|
69
|
-
if (actual.size !== expected.size || [...expected].some((key) => !actual.has(key))) {
|
|
70
|
-
return yield* Effect.fail(new PublishedKeysMismatch({ actual: [...actual], expected: [...expected] }));
|
|
71
|
-
}
|
|
72
|
-
return result;
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
//# sourceMappingURL=testing.js.map
|