@shivaedev/sql 0.33.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 +34 -0
- package/README.md +103 -10
- package/dist/MigrationStartedAt.d.ts.map +1 -0
- package/dist/{internal/MigrationStartedAt.js → MigrationStartedAt.js} +1 -1
- package/dist/MigrationStartedAt.js.map +1 -0
- 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.d.ts +9 -2
- package/dist/batchedDelete.d.ts.map +1 -1
- package/dist/batchedDelete.js +35 -36
- package/dist/batchedDelete.js.map +1 -1
- package/dist/changes.d.ts +8 -0
- package/dist/changes.d.ts.map +1 -0
- package/dist/changes.js +7 -0
- package/dist/changes.js.map +1 -0
- 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/deleteGroupsBatched.d.ts +13 -0
- package/dist/deleteGroupsBatched.d.ts.map +1 -0
- package/dist/deleteGroupsBatched.js +23 -0
- package/dist/deleteGroupsBatched.js.map +1 -0
- 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/invalidationKeys.d.ts +8 -0
- package/dist/invalidationKeys.d.ts.map +1 -0
- package/dist/invalidationKeys.js +15 -0
- package/dist/invalidationKeys.js.map +1 -0
- package/dist/keysetPage.d.ts +26 -0
- package/dist/keysetPage.d.ts.map +1 -0
- package/dist/keysetPage.js +23 -0
- package/dist/keysetPage.js.map +1 -0
- 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 +30 -6
- 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/migrationLock.d.ts.map +1 -0
- package/dist/migrationLock.js.map +1 -0
- package/dist/migrations.d.ts +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +18 -9
- package/dist/migrations.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/postgresChanges.d.ts.map +1 -1
- package/dist/postgresChanges.js +8 -4
- package/dist/postgresChanges.js.map +1 -1
- package/dist/postgresLayer.d.ts +2 -1
- package/dist/postgresLayer.d.ts.map +1 -1
- package/dist/postgresLayer.js +1 -1
- package/dist/postgresLayer.js.map +1 -1
- package/dist/postgresRelease.js +1 -1
- package/dist/postgresRelease.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 +6 -4
- 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/transact.d.ts +13 -5
- package/dist/transact.d.ts.map +1 -1
- package/dist/transact.js +45 -19
- package/dist/transact.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 +4 -4
- package/dist/versionedStatements.d.ts.map +1 -1
- package/dist/versionedStatements.js +12 -9
- package/dist/versionedStatements.js.map +1 -1
- package/package.json +8 -6
- package/src/{internal/MigrationStartedAt.ts → MigrationStartedAt.ts} +1 -1
- package/src/aggregateColumn.ts +4 -3
- package/src/batchWrites.ts +10 -9
- package/src/batchedDelete.ts +54 -41
- package/src/changes.ts +12 -0
- package/src/compositeRepository.ts +15 -15
- package/src/conditionalUpsert.ts +7 -6
- package/src/conflictWrites.ts +14 -10
- package/src/deleteGroupsBatched.ts +41 -0
- package/src/insertFromSelect.ts +6 -6
- package/src/invalidationKeys.ts +24 -0
- package/src/keysetPage.ts +54 -0
- package/src/latestPerGroup.ts +5 -4
- package/src/makeAggregate.ts +48 -47
- package/src/makeConflictClause.ts +37 -8
- package/src/makeQueryClauses.ts +6 -6
- package/src/migrations.ts +21 -10
- package/src/pinnedGroups.ts +10 -14
- package/src/postgresChanges.ts +13 -4
- package/src/postgresLayer.ts +1 -1
- package/src/postgresRelease.ts +1 -1
- package/src/repository.ts +8 -9
- package/src/repositoryChanges.ts +6 -9
- package/src/repositoryFilter.ts +5 -4
- package/src/repositoryQueries.ts +57 -55
- package/src/repositoryWrites.ts +9 -9
- package/src/transact.ts +61 -24
- 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/internal/CommandRecovery.d.ts +0 -3
- package/dist/internal/CommandRecovery.d.ts.map +0 -1
- package/dist/internal/CommandRecovery.js +0 -5
- package/dist/internal/CommandRecovery.js.map +0 -1
- package/dist/internal/MigrationStartedAt.d.ts.map +0 -1
- package/dist/internal/MigrationStartedAt.js.map +0 -1
- package/dist/internal/migrationLock.d.ts.map +0 -1
- package/dist/internal/migrationLock.js.map +0 -1
- 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/internal/CommandRecovery.ts +0 -5
- package/src/testing.ts +0 -103
- /package/dist/{internal/MigrationStartedAt.d.ts → MigrationStartedAt.d.ts} +0 -0
- /package/dist/{internal/migrationLock.d.ts → migrationLock.d.ts} +0 -0
- /package/dist/{internal/migrationLock.js → migrationLock.js} +0 -0
- /package/src/{internal/migrationLock.ts → migrationLock.ts} +0 -0
package/src/makeAggregate.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 { SqlError } from "effect/sql/SqlError";
|
|
5
5
|
import type { Fragment } from "effect/sql/Statement";
|
|
6
6
|
import { aggregateColumn } from "#aggregateColumn.ts";
|
|
@@ -10,55 +10,56 @@ import { type FindManyFilter, makeFilter } from "#repositoryFilter.ts";
|
|
|
10
10
|
|
|
11
11
|
export function makeAggregate<TModel extends Model.Any>(
|
|
12
12
|
model: TModel,
|
|
13
|
-
sql: SqlClient.SqlClient,
|
|
14
13
|
options: { readonly tableName: string; readonly spanPrefix: string },
|
|
15
|
-
): Aggregate<TModel>;
|
|
14
|
+
): Effect.Effect<Aggregate<TModel>, never, SqlClient.SqlClient>;
|
|
16
15
|
export function makeAggregate(
|
|
17
16
|
model: Model.Any,
|
|
18
|
-
sql: SqlClient.SqlClient,
|
|
19
17
|
options: { readonly tableName: string; readonly spanPrefix: string },
|
|
20
|
-
): unknown {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (groups.includes(name)) {
|
|
41
|
-
return yield* Effect.fail(new Schema.SchemaError(new SchemaIssue.Pointer([name], new SchemaIssue.UnexpectedKey(model.ast, name))));
|
|
18
|
+
): Effect.Effect<unknown, never, SqlClient.SqlClient> {
|
|
19
|
+
return Effect.fnUntraced(function* () {
|
|
20
|
+
const sql = yield* SqlClient.SqlClient;
|
|
21
|
+
const { fieldAt, where } = yield* makeFilter(model);
|
|
22
|
+
const selectAggregate = yield* aggregateColumn(fieldAt);
|
|
23
|
+
return (query: {
|
|
24
|
+
readonly groupBy: readonly string[];
|
|
25
|
+
readonly aggregates: Readonly<Record<string, object>>;
|
|
26
|
+
readonly where?: FindManyFilter<Model.Any>;
|
|
27
|
+
readonly orderBy?: readonly { readonly field: string; readonly direction: "asc" | "desc" }[];
|
|
28
|
+
readonly limit?: number;
|
|
29
|
+
}): Effect.Effect<unknown, SqlError | Schema.SchemaError, unknown> =>
|
|
30
|
+
Effect.gen(function* () {
|
|
31
|
+
const groups = yield* Schema.decodeUnknownEffect(Schema.NonEmptyArray(Schema.String))(query.groupBy);
|
|
32
|
+
const fields = new Map<string, Schema.Constraint>();
|
|
33
|
+
const columns: Fragment[] = [];
|
|
34
|
+
const ordering = new Map<string, Fragment>();
|
|
35
|
+
for (const key of groups) {
|
|
36
|
+
fields.set(key, yield* fieldAt(key));
|
|
37
|
+
columns.push(sql`${sql(key)}`);
|
|
42
38
|
}
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
39
|
+
for (const [name, expression] of Object.entries(query.aggregates)) {
|
|
40
|
+
if (groups.includes(name)) {
|
|
41
|
+
return yield* Effect.fail(new Schema.SchemaError(new SchemaIssue.Pointer([name], new SchemaIssue.UnexpectedKey(model.ast, name))));
|
|
42
|
+
}
|
|
43
|
+
const selected = yield* selectAggregate(name, expression);
|
|
44
|
+
ordering.set(name, selected.order);
|
|
45
|
+
columns.push(selected.column);
|
|
46
|
+
fields.set(name, selected.schema);
|
|
47
|
+
}
|
|
48
|
+
const Result = Schema.Struct(Object.fromEntries(fields));
|
|
49
|
+
const clauses = yield* makeQueryClauses(
|
|
50
|
+
(key) =>
|
|
51
|
+
fields.has(key)
|
|
52
|
+
? Effect.succeed(fields.get(key))
|
|
53
|
+
: Effect.fail(new Schema.SchemaError(new SchemaIssue.Pointer([key], new SchemaIssue.UnexpectedKey(Result.ast, key)))),
|
|
54
|
+
(key) => ordering.get(key) ?? sql`${sql(key)}`,
|
|
55
|
+
).pipe(Effect.provideService(SqlClient.SqlClient, sql));
|
|
56
|
+
const predicate = yield* where(query.where, true, true);
|
|
57
|
+
const order = yield* clauses.orderClause(query.orderBy);
|
|
58
|
+
const limit = yield* clauses.limitClause(query.limit);
|
|
59
|
+
const partitions = sql.csv(groups.map((key) => sql`${sql(key)}`));
|
|
60
|
+
const rows =
|
|
61
|
+
yield* sql`select ${sql.csv(columns)} from ${sql(options.tableName)} where ${predicate} group by ${partitions} ${order} ${limit}`;
|
|
62
|
+
return yield* Schema.decodeUnknownEffect(Schema.Array(Result))(rows);
|
|
63
|
+
}).pipe(Effect.withSpan(`${options.spanPrefix}.aggregate`));
|
|
64
|
+
})();
|
|
64
65
|
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { Effect, Schema, SchemaAST, SchemaIssue } from "effect";
|
|
1
|
+
import { Array as Arr, Effect, Schema, SchemaAST, SchemaIssue } from "effect";
|
|
2
2
|
import type { Model } from "effect/schema";
|
|
3
|
-
import
|
|
3
|
+
import { SqlClient } from "effect/sql";
|
|
4
4
|
import { makeFilter } from "#repositoryFilter.ts";
|
|
5
5
|
import type { UpsertAssignment } from "#upsertColumns.ts";
|
|
6
6
|
|
|
7
|
-
export
|
|
7
|
+
export const makeConflictClause = Effect.fnUntraced(function* (
|
|
8
8
|
model: Model.Any,
|
|
9
|
-
sql: SqlClient.SqlClient,
|
|
10
9
|
options: {
|
|
11
10
|
readonly tableName: string;
|
|
12
11
|
readonly conflictColumns: readonly string[];
|
|
@@ -14,9 +13,10 @@ export function makeConflictClause(
|
|
|
14
13
|
readonly idColumns: readonly string[];
|
|
15
14
|
},
|
|
16
15
|
) {
|
|
16
|
+
const sql = yield* SqlClient.SqlClient;
|
|
17
17
|
const { conflictColumns, idColumns } = options;
|
|
18
|
-
const updateFilter = makeFilter(model,
|
|
19
|
-
const filter = makeFilter(model
|
|
18
|
+
const updateFilter = yield* makeFilter(model, options);
|
|
19
|
+
const filter = yield* makeFilter(model);
|
|
20
20
|
const updateFields = SchemaAST.isObjects(model.update.ast) ? model.update.ast.propertySignatures.map((field) => field.name) : [];
|
|
21
21
|
const immutable = Object.keys(model.fields).filter((column) => !updateFields.includes(column));
|
|
22
22
|
function invalid(column: string) {
|
|
@@ -44,6 +44,35 @@ export function makeConflictClause(
|
|
|
44
44
|
}
|
|
45
45
|
return sql`${sql(column)} = ${value}`;
|
|
46
46
|
}
|
|
47
|
+
function encodePredicate([key, value]: readonly [string, unknown]) {
|
|
48
|
+
return Effect.gen(function* () {
|
|
49
|
+
const field = yield* filter.fieldAt(key);
|
|
50
|
+
const encoded = yield* Schema.encodeEffect(field)(value);
|
|
51
|
+
return { encoded, key, nullish: value === null || encoded === null };
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function postgresPredicate(input: Readonly<Record<string, unknown>>) {
|
|
55
|
+
return Effect.gen(function* () {
|
|
56
|
+
const entries = yield* Effect.forEach(
|
|
57
|
+
Object.entries(input).filter(([, value]) => value !== undefined),
|
|
58
|
+
encodePredicate,
|
|
59
|
+
);
|
|
60
|
+
if (entries.length === 0) {
|
|
61
|
+
return sql.and([]);
|
|
62
|
+
}
|
|
63
|
+
// PostgreSQL must see constants to infer a partial index under generic plans.
|
|
64
|
+
const [quoted] = yield* sql`select ${sql.csv(entries.map(({ encoded }) => sql`quote_nullable(${encoded}::text)`))}`.values;
|
|
65
|
+
const literals = yield* Schema.decodeUnknownEffect(Schema.Array(Schema.String))(quoted);
|
|
66
|
+
return sql.and(
|
|
67
|
+
Arr.zip(entries, literals).map(([{ key, nullish }, literal]) =>
|
|
68
|
+
nullish ? sql`${sql(key)} is null` : sql`${sql(key)} = ${sql.literal(literal)}`,
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function conflictPredicate(input: Readonly<Record<string, unknown>>) {
|
|
74
|
+
return sql.onDialectOrElse({ orElse: () => filter.where(input), pg: () => postgresPredicate(input) });
|
|
75
|
+
}
|
|
47
76
|
function clause(
|
|
48
77
|
encoded: Readonly<Record<string, unknown>>,
|
|
49
78
|
updateColumns?: readonly UpsertAssignment[],
|
|
@@ -59,11 +88,11 @@ export function makeConflictClause(
|
|
|
59
88
|
);
|
|
60
89
|
}
|
|
61
90
|
const conflict = sql.csv(conflictColumns.map((column) => sql`${sql(column)}`));
|
|
62
|
-
const where = options.conflictWhere === undefined ? sql`` : sql`where ${yield*
|
|
91
|
+
const where = options.conflictWhere === undefined ? sql`` : sql`where ${yield* conflictPredicate(options.conflictWhere)}`;
|
|
63
92
|
const action = updateColumns === undefined ? sql`do nothing` : sql`do update set ${sql.csv(updateColumns.map(assignment))}`;
|
|
64
93
|
const condition = updateWhere === undefined ? sql`` : sql`where ${yield* updateFilter.where(updateWhere, true, true)}`;
|
|
65
94
|
return sql`on conflict (${conflict}) ${where} ${action} ${condition}`;
|
|
66
95
|
});
|
|
67
96
|
}
|
|
68
97
|
return clause;
|
|
69
|
-
}
|
|
98
|
+
});
|
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/migrations.ts
CHANGED
|
@@ -2,12 +2,12 @@ import { Duration, Effect, Option, Schema } from "effect";
|
|
|
2
2
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
3
3
|
import { Migrator, SqlClient } from "effect/sql";
|
|
4
4
|
import type { SqlError } from "effect/sql/SqlError";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { postgresRelease } from "
|
|
9
|
-
import { runSchemaComponent, type SchemaComponent } from "
|
|
10
|
-
import { transact } from "
|
|
5
|
+
import { advisoryXactLock } from "#advisoryXactLock.ts";
|
|
6
|
+
import { MigrationStartedAt } from "#MigrationStartedAt.ts";
|
|
7
|
+
import { migrationLock } from "#migrationLock.ts";
|
|
8
|
+
import { postgresRelease } from "#postgresRelease.ts";
|
|
9
|
+
import { runSchemaComponent, type SchemaComponent } from "#SchemaComponent.ts";
|
|
10
|
+
import { transact } from "#transact.ts";
|
|
11
11
|
|
|
12
12
|
export interface PostgresMigrationOptions<R, E = never> {
|
|
13
13
|
readonly components?: readonly SchemaComponent<R, E>[];
|
|
@@ -52,12 +52,12 @@ export const migratePostgres = Effect.fn("EffectSql.migratePostgres")(function*
|
|
|
52
52
|
created_at timestamp with time zone not null default now(),
|
|
53
53
|
name text not null
|
|
54
54
|
)`;
|
|
55
|
-
const applied = yield* sql`select migration_id as id from ${sql(table)}`.withoutTransform.pipe(
|
|
56
|
-
Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(Schema.Struct({ id: Schema.Int })))),
|
|
55
|
+
const applied = yield* sql`select migration_id as id, name from ${sql(table)}`.withoutTransform.pipe(
|
|
56
|
+
Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(Schema.Struct({ id: Schema.Int, name: Schema.String })))),
|
|
57
57
|
);
|
|
58
|
-
const
|
|
58
|
+
const appliedNames = new Map(applied.map(({ id, name }) => [id, name]));
|
|
59
59
|
const latest = applied.reduce((max, { id }) => Math.max(max, id), 0);
|
|
60
|
-
const missing = current.filter(([id]) => id <= latest && !
|
|
60
|
+
const missing = current.filter(([id]) => id <= latest && !appliedNames.has(id));
|
|
61
61
|
if (missing.length > 0) {
|
|
62
62
|
return yield* Effect.fail(
|
|
63
63
|
new Migrator.MigrationError({
|
|
@@ -66,6 +66,17 @@ export const migratePostgres = Effect.fn("EffectSql.migratePostgres")(function*
|
|
|
66
66
|
}),
|
|
67
67
|
);
|
|
68
68
|
}
|
|
69
|
+
for (const [id, name] of current) {
|
|
70
|
+
const recorded = appliedNames.get(id);
|
|
71
|
+
if (recorded !== undefined && recorded !== name) {
|
|
72
|
+
return yield* Effect.fail(
|
|
73
|
+
new Migrator.MigrationError({
|
|
74
|
+
kind: "BadState",
|
|
75
|
+
message: `Migration ${id} is recorded as ${JSON.stringify(recorded)} but loaded as ${JSON.stringify(name)}`,
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
69
80
|
return yield* migrate({ loader: Effect.succeed(current), table });
|
|
70
81
|
}).pipe(transact({ onSqlError: (error) => error }), Effect.provideService(Reactivity.Reactivity, reactivity));
|
|
71
82
|
const schemaComponents: readonly SchemaComponent<R, E | SqlError | Schema.SchemaError>[] = [
|
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/postgresChanges.ts
CHANGED
|
@@ -2,10 +2,10 @@ import { Context, type Duration, Effect, Layer, type Queue, type Scope } from "e
|
|
|
2
2
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
3
3
|
import { SqlClient } from "effect/sql";
|
|
4
4
|
import type { SqlError } from "effect/sql/SqlError";
|
|
5
|
-
import {
|
|
5
|
+
import { CommandPublication, CommandPublicationError, type CommandPublisher } from "@shivaedev/changes/commandPublication.ts";
|
|
6
6
|
import { listenForChanges } from "./listenForChanges.ts";
|
|
7
7
|
import { encodeKeys, resyncPayload } from "./notifyPayload.ts";
|
|
8
|
-
import { BeforeCommit, CommittedSink } from "./transact.ts";
|
|
8
|
+
import { BeforeCommit, CommittedSink, invalidateOnCommit, transact } from "./transact.ts";
|
|
9
9
|
|
|
10
10
|
export interface PostgresNotification {
|
|
11
11
|
readonly payload: string;
|
|
@@ -48,10 +48,19 @@ export function postgresChanges(options: PostgresChangesOptions) {
|
|
|
48
48
|
}
|
|
49
49
|
const recover = options.resync.pipe(
|
|
50
50
|
Effect.andThen(options.client`select pg_notify(${options.channel}, ${resyncPayload})`),
|
|
51
|
-
Effect.catchCause((cause) => Effect.
|
|
51
|
+
Effect.catchCause((cause) => Effect.fail(new CommandPublicationError({ cause }))),
|
|
52
52
|
Effect.asVoid,
|
|
53
53
|
);
|
|
54
|
-
|
|
54
|
+
const publication: CommandPublisher = {
|
|
55
|
+
publish: (keys) =>
|
|
56
|
+
invalidateOnCommit(keys).pipe(
|
|
57
|
+
transact({ onSqlError: (error) => error }),
|
|
58
|
+
Effect.provideService(SqlClient.SqlClient, options.client),
|
|
59
|
+
Effect.catchCause((cause) => Effect.fail(new CommandPublicationError({ cause }))),
|
|
60
|
+
),
|
|
61
|
+
recover,
|
|
62
|
+
};
|
|
63
|
+
return Context.make(BeforeCommit, notify).pipe(Context.add(CommittedSink, committed), Context.add(CommandPublication, publication));
|
|
55
64
|
}),
|
|
56
65
|
);
|
|
57
66
|
}
|
package/src/postgresLayer.ts
CHANGED
|
@@ -26,5 +26,5 @@ export function postgresLayer(url: Redacted.Redacted<string>, options?: Postgres
|
|
|
26
26
|
const client = yield* makePostgresClient(config).pipe(Effect.provideService(PgPool.PgPool, pool));
|
|
27
27
|
return Context.make(PgPool.PgPool, pool).pipe(Context.add(PgClient.PgClient, client), Context.add(SqlClient.SqlClient, client));
|
|
28
28
|
}),
|
|
29
|
-
).pipe(Layer.
|
|
29
|
+
).pipe(Layer.provideMerge(Layer.fresh(Reactivity.layer)));
|
|
30
30
|
}
|
package/src/postgresRelease.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
import { SqlClient } from "effect/sql";
|
|
3
|
-
import { MigrationStartedAt } from "#
|
|
3
|
+
import { MigrationStartedAt } from "#MigrationStartedAt.ts";
|
|
4
4
|
import type { SchemaComponent } from "./SchemaComponent.ts";
|
|
5
5
|
|
|
6
6
|
export const readPostgresRelease = Effect.fn("EffectSql.readPostgresRelease")(function* (table = "effect_sql_migrations") {
|
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
|
@@ -2,18 +2,14 @@ import { Effect, Option, type Schema } from "effect";
|
|
|
2
2
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
3
3
|
import { SqlClient } from "effect/sql";
|
|
4
4
|
import { declaredBuckets } from "@shivaedev/changes/buckets.ts";
|
|
5
|
-
import { compositeId, invalidationKeys, type Key, recordKey } from "@shivaedev/changes/keys.ts";
|
|
5
|
+
import { collectionInvalidationKeys, compositeId, invalidationKeys, type Key, recordKey } from "@shivaedev/changes/keys.ts";
|
|
6
6
|
import { ChangeDeclarations } from "#ChangeDeclarations.ts";
|
|
7
7
|
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[] = [];
|
|
@@ -71,8 +67,9 @@ export const repositoryChanges = Effect.fnUntraced(function* (
|
|
|
71
67
|
return;
|
|
72
68
|
}
|
|
73
69
|
const declarations = yield* ChangeDeclarations;
|
|
74
|
-
|
|
75
|
-
yield*
|
|
70
|
+
const keys = collectionInvalidationKeys(table);
|
|
71
|
+
yield* declarations.declare(table, keys, rows);
|
|
72
|
+
yield* provide(invalidateOnCommit(keys));
|
|
76
73
|
});
|
|
77
74
|
return { record, recordTable, returning, write };
|
|
78
75
|
});
|
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
|
}
|