@shivaedev/sql 0.37.0 → 0.39.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 +24 -0
- package/README.md +117 -2
- package/dist/Model.d.ts +9 -0
- package/dist/Model.d.ts.map +1 -0
- package/dist/Model.js +6 -0
- package/dist/Model.js.map +1 -0
- package/dist/VersionedRepositoryShape.d.ts +6 -0
- package/dist/VersionedRepositoryShape.d.ts.map +1 -0
- package/dist/VersionedRepositoryShape.js +2 -0
- package/dist/VersionedRepositoryShape.js.map +1 -0
- package/dist/WorkerRelease.d.ts +1 -8
- package/dist/WorkerRelease.d.ts.map +1 -1
- package/dist/WorkerRelease.js +4 -29
- package/dist/WorkerRelease.js.map +1 -1
- package/dist/adapters/groupColumns.d.ts +14 -0
- package/dist/adapters/groupColumns.d.ts.map +1 -0
- package/dist/adapters/groupColumns.js +54 -0
- package/dist/adapters/groupColumns.js.map +1 -0
- package/dist/adapters/storageSchema.d.ts +4 -0
- package/dist/adapters/storageSchema.d.ts.map +1 -0
- package/dist/adapters/storageSchema.js +7 -0
- package/dist/adapters/storageSchema.js.map +1 -0
- package/dist/backfillPostgres.d.ts +18 -0
- package/dist/backfillPostgres.d.ts.map +1 -0
- package/dist/backfillPostgres.js +35 -0
- package/dist/backfillPostgres.js.map +1 -0
- package/dist/batchWrites.d.ts.map +1 -1
- package/dist/batchWrites.js +6 -3
- package/dist/batchWrites.js.map +1 -1
- package/dist/compositeRecordKey.d.ts +8 -0
- package/dist/compositeRecordKey.d.ts.map +1 -0
- package/dist/compositeRecordKey.js +15 -0
- package/dist/compositeRecordKey.js.map +1 -0
- package/dist/compositeRepository.d.ts.map +1 -1
- package/dist/compositeRepository.js +4 -1
- package/dist/compositeRepository.js.map +1 -1
- package/dist/conditionalUpsert.d.ts.map +1 -1
- package/dist/conditionalUpsert.js +5 -2
- package/dist/conditionalUpsert.js.map +1 -1
- package/dist/conflictWrites.d.ts.map +1 -1
- package/dist/conflictWrites.js +6 -3
- package/dist/conflictWrites.js.map +1 -1
- package/dist/coordinateWorkerRelease.js +1 -1
- package/dist/coordinateWorkerRelease.js.map +1 -1
- package/dist/initializeWorkerRelease.d.ts +7 -0
- package/dist/initializeWorkerRelease.d.ts.map +1 -0
- package/dist/initializeWorkerRelease.js +7 -0
- package/dist/initializeWorkerRelease.js.map +1 -0
- package/dist/insertFromSelect.d.ts.map +1 -1
- package/dist/insertFromSelect.js +4 -1
- package/dist/insertFromSelect.js.map +1 -1
- package/dist/latestPerGroup.d.ts.map +1 -1
- package/dist/latestPerGroup.js +7 -3
- package/dist/latestPerGroup.js.map +1 -1
- package/dist/makeOrderedChildren.d.ts +26 -0
- package/dist/makeOrderedChildren.d.ts.map +1 -0
- package/dist/makeOrderedChildren.js +38 -0
- package/dist/makeOrderedChildren.js.map +1 -0
- package/dist/makeWorkerRelease.d.ts +8 -0
- package/dist/makeWorkerRelease.d.ts.map +1 -0
- package/dist/makeWorkerRelease.js +12 -0
- package/dist/makeWorkerRelease.js.map +1 -0
- package/dist/orderedChildStatements.d.ts +30 -0
- package/dist/orderedChildStatements.d.ts.map +1 -0
- package/dist/orderedChildStatements.js +61 -0
- package/dist/orderedChildStatements.js.map +1 -0
- package/dist/registerWorkerRelease.d.ts +4 -0
- package/dist/registerWorkerRelease.d.ts.map +1 -0
- package/dist/registerWorkerRelease.js +14 -0
- package/dist/registerWorkerRelease.js.map +1 -0
- package/dist/repository.d.ts +9 -8
- package/dist/repository.d.ts.map +1 -1
- package/dist/repository.js.map +1 -1
- package/dist/repositoryQueries.d.ts +3 -2
- package/dist/repositoryQueries.d.ts.map +1 -1
- package/dist/repositoryQueries.js +9 -4
- package/dist/repositoryQueries.js.map +1 -1
- package/dist/repositoryUpdate.d.ts +3 -3
- package/dist/repositoryUpdate.d.ts.map +1 -1
- package/dist/repositoryWrites.d.ts.map +1 -1
- package/dist/repositoryWrites.js +21 -6
- package/dist/repositoryWrites.js.map +1 -1
- package/dist/setWorkerReleaseActive.d.ts +4 -0
- package/dist/setWorkerReleaseActive.d.ts.map +1 -0
- package/dist/setWorkerReleaseActive.js +13 -0
- package/dist/setWorkerReleaseActive.js.map +1 -0
- package/dist/sqlNameTransforms.d.ts +5 -0
- package/dist/sqlNameTransforms.d.ts.map +1 -0
- package/dist/sqlNameTransforms.js +8 -0
- package/dist/sqlNameTransforms.js.map +1 -0
- package/dist/upsertResult.d.ts +2 -1
- package/dist/upsertResult.d.ts.map +1 -1
- package/dist/upsertResult.js +3 -2
- package/dist/upsertResult.js.map +1 -1
- package/dist/versionedRepository.d.ts +15 -6
- package/dist/versionedRepository.d.ts.map +1 -1
- package/dist/versionedRepository.js +8 -5
- package/dist/versionedRepository.js.map +1 -1
- package/dist/versionedStatements.d.ts +1 -1
- package/dist/versionedStatements.d.ts.map +1 -1
- package/dist/versionedStatements.js +10 -4
- package/dist/versionedStatements.js.map +1 -1
- package/package.json +15 -15
- package/src/Model.ts +7 -0
- package/src/VersionedRepositoryShape.ts +11 -0
- package/src/WorkerRelease.ts +4 -33
- package/src/adapters/groupColumns.ts +63 -0
- package/src/adapters/storageSchema.ts +10 -0
- package/src/backfillPostgres.ts +47 -0
- package/src/batchWrites.ts +6 -3
- package/src/compositeRecordKey.ts +26 -0
- package/src/compositeRepository.ts +4 -1
- package/src/conditionalUpsert.ts +5 -2
- package/src/conflictWrites.ts +6 -3
- package/src/coordinateWorkerRelease.ts +1 -1
- package/src/initializeWorkerRelease.ts +7 -0
- package/src/insertFromSelect.ts +4 -1
- package/src/latestPerGroup.ts +7 -3
- package/src/makeOrderedChildren.ts +78 -0
- package/src/makeWorkerRelease.ts +12 -0
- package/src/orderedChildStatements.ts +76 -0
- package/src/registerWorkerRelease.ts +15 -0
- package/src/repository.ts +14 -13
- package/src/repositoryQueries.ts +18 -5
- package/src/repositoryUpdate.ts +3 -3
- package/src/repositoryWrites.ts +21 -6
- package/src/setWorkerReleaseActive.ts +17 -0
- package/src/sqlNameTransforms.ts +9 -0
- package/src/upsertResult.ts +4 -2
- package/src/versionedRepository.ts +31 -11
- package/src/versionedStatements.ts +14 -4
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Cause, Data, Effect, Equal, Schema } from "effect";
|
|
2
|
+
import type { Model } from "effect/schema";
|
|
3
|
+
import { SqlClient, SqlSchema } from "effect/sql";
|
|
4
|
+
import type { RepositoryChanges } from "#repositoryChanges.ts";
|
|
5
|
+
|
|
6
|
+
export class ChildOrderError extends Data.TaggedError("ChildOrderError")<{
|
|
7
|
+
readonly reason: "membership" | "position";
|
|
8
|
+
}> {}
|
|
9
|
+
|
|
10
|
+
export interface ChildOrderOptions {
|
|
11
|
+
readonly idColumn: string;
|
|
12
|
+
readonly parentColumn: string;
|
|
13
|
+
readonly parentIdColumn: string;
|
|
14
|
+
readonly parentTable: string;
|
|
15
|
+
readonly positionColumn: string;
|
|
16
|
+
readonly tableName: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const orderedChildStatements = Effect.fnUntraced(function* (model: Model.Any, options: ChildOrderOptions, changes: RepositoryChanges) {
|
|
20
|
+
const sql = yield* SqlClient.SqlClient;
|
|
21
|
+
const { idColumn, parentColumn, parentTable, parentIdColumn, positionColumn, tableName } = options;
|
|
22
|
+
const parent = model.fields[parentColumn];
|
|
23
|
+
const idField = model.fields[idColumn];
|
|
24
|
+
if (parent === undefined || idField === undefined) {
|
|
25
|
+
return yield* Effect.die(new Error("Unknown ordered child key column"));
|
|
26
|
+
}
|
|
27
|
+
const encodeParent = Schema.encodeEffect(parent);
|
|
28
|
+
const encodeId = Schema.encodeEffect(idField);
|
|
29
|
+
const lock = Effect.fnUntraced(function* (value: unknown) {
|
|
30
|
+
const encoded = yield* encodeParent(value);
|
|
31
|
+
const rows = yield* sql`select ${sql(parentIdColumn)} from ${sql(parentTable)} where ${sql(parentIdColumn)} = ${encoded} for update`;
|
|
32
|
+
if (rows.length === 0) {
|
|
33
|
+
return yield* new Cause.NoSuchElementError();
|
|
34
|
+
}
|
|
35
|
+
return encoded;
|
|
36
|
+
});
|
|
37
|
+
const children = SqlSchema.findAll({
|
|
38
|
+
execute: (encodedParent) =>
|
|
39
|
+
sql`select ${sql(idColumn)} as ${sql("childId")} from ${sql(tableName)} where ${sql(parentColumn)} = ${encodedParent} order by ${sql(positionColumn)}, ${sql(idColumn)}`,
|
|
40
|
+
Request: Schema.Unknown,
|
|
41
|
+
Result: Schema.Struct({ childId: idField }),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const write = Effect.fnUntraced(function* (encodedParent: unknown, ids: readonly unknown[]) {
|
|
45
|
+
if (ids.length === 0) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const encoded = yield* Effect.forEach(ids, (value) => encodeId(value));
|
|
49
|
+
const positions = sql.join(" ", false)(encoded.map((value, index) => sql`when ${value} then ${index}`));
|
|
50
|
+
const rows = yield* sql`update ${sql(tableName)} set ${sql(positionColumn)} = case ${sql(idColumn)} ${positions} end
|
|
51
|
+
where ${sql(parentColumn)} = ${encodedParent} returning ${changes.returning()}`;
|
|
52
|
+
yield* changes.record(rows);
|
|
53
|
+
});
|
|
54
|
+
const reorder = Effect.fnUntraced(function* (value: unknown, ids: readonly unknown[]) {
|
|
55
|
+
const encodedParent = yield* lock(value);
|
|
56
|
+
const current = yield* children(encodedParent);
|
|
57
|
+
if (ids.length !== current.length || current.some(({ childId }) => ids.filter((key) => Equal.equals(childId, key)).length !== 1)) {
|
|
58
|
+
return yield* new ChildOrderError({ reason: "membership" });
|
|
59
|
+
}
|
|
60
|
+
yield* write(encodedParent, ids);
|
|
61
|
+
});
|
|
62
|
+
const move = Effect.fnUntraced(function* (value: unknown, child: unknown, position: number) {
|
|
63
|
+
const encodedParent = yield* lock(value);
|
|
64
|
+
const current = yield* children(encodedParent);
|
|
65
|
+
if (!current.some(({ childId }) => Equal.equals(childId, child))) {
|
|
66
|
+
return yield* new ChildOrderError({ reason: "membership" });
|
|
67
|
+
}
|
|
68
|
+
if (!Number.isInteger(position) || position < 0 || position >= current.length) {
|
|
69
|
+
return yield* new ChildOrderError({ reason: "position" });
|
|
70
|
+
}
|
|
71
|
+
const ids = current.map(({ childId }) => childId).filter((key) => !Equal.equals(key, child));
|
|
72
|
+
ids.splice(position, 0, child);
|
|
73
|
+
yield* write(encodedParent, ids);
|
|
74
|
+
});
|
|
75
|
+
return { lock, move, reorder };
|
|
76
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { initializeWorkerRelease } from "./initializeWorkerRelease.ts";
|
|
3
|
+
|
|
4
|
+
export function registerWorkerRelease(state: Effect.Success<typeof initializeWorkerRelease>) {
|
|
5
|
+
return function register<E>(control: (active: boolean) => Effect.Effect<void, E>) {
|
|
6
|
+
const attach = Effect.gen(function* () {
|
|
7
|
+
state.controls.add(control);
|
|
8
|
+
yield* control(state.active);
|
|
9
|
+
});
|
|
10
|
+
const detach = Effect.sync(() => {
|
|
11
|
+
state.controls.delete(control);
|
|
12
|
+
});
|
|
13
|
+
return Effect.acquireRelease(state.locked(attach), () => state.locked(detach));
|
|
14
|
+
};
|
|
15
|
+
}
|
package/src/repository.ts
CHANGED
|
@@ -10,7 +10,8 @@ import { repositoryChanges } from "#repositoryChanges.ts";
|
|
|
10
10
|
import { type BucketRegistrationError, type RepositoryOptions, validateRepositoryCollection } from "#repositoryCollection.ts";
|
|
11
11
|
import type { FindManyFilter, RepositoryFilter, RepositoryListFilter } from "#repositoryFilter.ts";
|
|
12
12
|
import { makeQueries, type Queries } from "#repositoryQueries.ts";
|
|
13
|
-
import
|
|
13
|
+
import type { VersionedRepositoryShape } from "#VersionedRepositoryShape.ts";
|
|
14
|
+
import { type IdColumn, makeVersionedWrites, type VersionColumn } from "#versionedRepository.ts";
|
|
14
15
|
|
|
15
16
|
type Key<TModel extends Model.Any> = Extract<keyof TModel["fields"] & keyof Row<TModel>, string>;
|
|
16
17
|
type Row<TModel extends Model.Any> = Schema.Struct.Type<TModel["fields"]>;
|
|
@@ -27,6 +28,8 @@ export interface FindMany<TModel extends Model.Any, TKey extends Key<TModel>> {
|
|
|
27
28
|
readonly where?: FindManyFilter<TModel>;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
export type FindOne<TModel extends Model.Any, TKey extends Key<TModel>> = Omit<FindMany<TModel, TKey>, "limit">;
|
|
32
|
+
|
|
30
33
|
export interface LatestPerGroup<TModel extends Model.Any> {
|
|
31
34
|
readonly limit: number;
|
|
32
35
|
readonly orderBy: readonly [OrderBy<TModel>, ...OrderBy<TModel>[]];
|
|
@@ -41,39 +44,37 @@ export class OptimisticConcurrencyError extends Data.TaggedError("OptimisticConc
|
|
|
41
44
|
readonly reason: "missing" | "stale";
|
|
42
45
|
}> {}
|
|
43
46
|
|
|
44
|
-
type VersionedRepository<TModel extends Model.Any, TId extends PrimaryKey<TModel>> = Omit<
|
|
45
|
-
Repository<TModel, TId>,
|
|
46
|
-
keyof VersionedWrites<TModel, TId>
|
|
47
|
-
> &
|
|
48
|
-
VersionedWrites<TModel, TId> &
|
|
49
|
-
Queries<TModel>;
|
|
50
|
-
|
|
51
47
|
export function makeRepository<
|
|
52
48
|
TModel extends Model.Any,
|
|
53
49
|
const TId extends PrimaryKey<TModel>,
|
|
54
50
|
const TConflict extends ConflictColumns<TModel>,
|
|
55
51
|
const TWhere extends keyof RepositoryFilter<TModel> = never,
|
|
52
|
+
const TVersion extends VersionColumn<TModel, TId> = VersionColumn<TModel, TId>,
|
|
56
53
|
>(
|
|
57
54
|
model: TModel,
|
|
58
55
|
options: RepositoryOptions<TId> & {
|
|
59
|
-
readonly versionColumn:
|
|
56
|
+
readonly versionColumn: TVersion;
|
|
60
57
|
readonly conflictColumns: TConflict;
|
|
61
58
|
readonly conflictWhere?: Pick<RepositoryFilter<TModel>, TWhere>;
|
|
62
59
|
},
|
|
63
60
|
): Effect.Effect<
|
|
64
|
-
|
|
61
|
+
VersionedRepositoryShape<TModel, TId, TVersion> &
|
|
65
62
|
Pick<ConflictWrites<TModel, never, TModel["fields"][Extract<TWhere, keyof TModel["fields"]>]["EncodingServices"]>, "insertOrIgnore">,
|
|
66
63
|
BucketRegistrationError,
|
|
67
64
|
SqlClient.SqlClient
|
|
68
65
|
>;
|
|
69
|
-
export function makeRepository<
|
|
66
|
+
export function makeRepository<
|
|
67
|
+
TModel extends Model.Any,
|
|
68
|
+
const TId extends PrimaryKey<TModel>,
|
|
69
|
+
const TVersion extends VersionColumn<TModel, TId> = VersionColumn<TModel, TId>,
|
|
70
|
+
>(
|
|
70
71
|
model: TModel,
|
|
71
72
|
options: RepositoryOptions<TId> & {
|
|
72
|
-
readonly versionColumn:
|
|
73
|
+
readonly versionColumn: TVersion;
|
|
73
74
|
readonly conflictColumns?: never;
|
|
74
75
|
readonly conflictWhere?: never;
|
|
75
76
|
},
|
|
76
|
-
): Effect.Effect<
|
|
77
|
+
): Effect.Effect<VersionedRepositoryShape<TModel, TId, TVersion>, BucketRegistrationError, SqlClient.SqlClient>;
|
|
77
78
|
export function makeRepository<
|
|
78
79
|
TModel extends Model.Any,
|
|
79
80
|
const TId extends PrimaryKey<TModel>,
|
package/src/repositoryQueries.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import { Effect, Schema } from "effect";
|
|
1
|
+
import { Array as Arr, Effect, type Option, Schema } from "effect";
|
|
2
2
|
import type { Model } from "effect/schema";
|
|
3
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
|
+
import { groupColumns } from "#adapters/groupColumns.ts";
|
|
7
|
+
import { storageSchema } from "#adapters/storageSchema.ts";
|
|
6
8
|
import type { Aggregate } from "#aggregateQuery.ts";
|
|
7
9
|
import { latestPerGroup as makeLatestPerGroup } from "#latestPerGroup.ts";
|
|
8
10
|
import { makeAggregate } from "#makeAggregate.ts";
|
|
9
11
|
import { makeQueryClauses } from "#makeQueryClauses.ts";
|
|
10
|
-
import type { FindMany, LatestPerGroup } from "#repository.ts";
|
|
12
|
+
import type { FindMany, FindOne, LatestPerGroup } from "#repository.ts";
|
|
11
13
|
import { makeFilter } from "#repositoryFilter.ts";
|
|
14
|
+
import { sqlNameTransforms } from "#sqlNameTransforms.ts";
|
|
12
15
|
|
|
13
16
|
type Key<TModel extends Model.Any> = Extract<keyof TModel["fields"] & keyof Row<TModel>, string>;
|
|
14
17
|
type Row<TModel extends Model.Any> = Schema.Struct.Type<TModel["fields"]>;
|
|
@@ -32,6 +35,13 @@ export interface Queries<TModel extends Model.Any> {
|
|
|
32
35
|
SqlError | Schema.SchemaError,
|
|
33
36
|
TModel["fields"][Key<TModel>]["EncodingServices"] | TModel["fields"][TKey]["DecodingServices"]
|
|
34
37
|
>;
|
|
38
|
+
findOne: <TKey extends Key<TModel> = Key<TModel>>(
|
|
39
|
+
query?: FindOne<TModel, TKey>,
|
|
40
|
+
) => Effect.Effect<
|
|
41
|
+
Option.Option<Pick<Row<TModel>, TKey>>,
|
|
42
|
+
SqlError | Schema.SchemaError,
|
|
43
|
+
TModel["fields"][Key<TModel>]["EncodingServices"] | TModel["fields"][TKey]["DecodingServices"]
|
|
44
|
+
>;
|
|
35
45
|
latestPerGroup: (
|
|
36
46
|
query: LatestPerGroup<TModel>,
|
|
37
47
|
) => Effect.Effect<
|
|
@@ -53,6 +63,7 @@ export function makeQueries<TModel extends Model.Any>(
|
|
|
53
63
|
): Effect.Effect<Queries<TModel>, never, SqlClient.SqlClient> {
|
|
54
64
|
return Effect.fnUntraced(function* () {
|
|
55
65
|
const sql = yield* SqlClient.SqlClient;
|
|
66
|
+
const nameTransforms = yield* sqlNameTransforms;
|
|
56
67
|
const { fieldAt, where } = yield* makeFilter(model);
|
|
57
68
|
const readLatest = yield* makeLatestPerGroup(model, options.tableName);
|
|
58
69
|
const { decodeLimit, orderClause, limitClause } = yield* makeQueryClauses(fieldAt);
|
|
@@ -77,7 +88,7 @@ export function makeQueries<TModel extends Model.Any>(
|
|
|
77
88
|
const predicate = yield* where(query.where, true, true);
|
|
78
89
|
const order = yield* orderClause(query.orderBy);
|
|
79
90
|
const limit = yield* limitClause(query.limit);
|
|
80
|
-
return sql`select ${sql.csv(keys.map((
|
|
91
|
+
return sql`select ${sql.csv(groupColumns(model.fields, keys).map(({ column }) => sql`${sql(column)}`))} from ${sql(options.tableName)} where ${predicate} ${order} ${limit}`;
|
|
81
92
|
});
|
|
82
93
|
return { fields, statement };
|
|
83
94
|
}
|
|
@@ -93,7 +104,7 @@ export function makeQueries<TModel extends Model.Any>(
|
|
|
93
104
|
return Effect.gen(function* () {
|
|
94
105
|
const selected = select(query);
|
|
95
106
|
const rows = yield* sql`${yield* selected.statement}`;
|
|
96
|
-
return yield* Schema.decodeUnknownEffect(Schema.Array(Schema.Struct(selected.fields)))(rows);
|
|
107
|
+
return yield* Schema.decodeUnknownEffect(Schema.Array(storageSchema(Schema.Struct(selected.fields), selected.fields, nameTransforms)))(rows);
|
|
97
108
|
}).pipe(Effect.withSpan(`${options.spanPrefix}.findMany`));
|
|
98
109
|
}
|
|
99
110
|
function latestPerGroup(query: LatestPerGroup<TModel>): ReturnType<Queries<TModel>["latestPerGroup"]>;
|
|
@@ -104,6 +115,8 @@ export function makeQueries<TModel extends Model.Any>(
|
|
|
104
115
|
return yield* readLatest(query, order, rowLimit);
|
|
105
116
|
}).pipe(Effect.withSpan(`${options.spanPrefix}.latestPerGroup`));
|
|
106
117
|
}
|
|
107
|
-
|
|
118
|
+
const findOne: Queries<TModel>["findOne"] = (query = {}) =>
|
|
119
|
+
findMany({ ...query, limit: 1 }).pipe(Effect.map(Arr.head), Effect.withSpan(`${options.spanPrefix}.findOne`));
|
|
120
|
+
return { aggregate, count, findMany, findOne, latestPerGroup, select };
|
|
108
121
|
})();
|
|
109
122
|
}
|
package/src/repositoryUpdate.ts
CHANGED
|
@@ -8,11 +8,11 @@ export type UpdateInput<TModel extends Model.Any> = {
|
|
|
8
8
|
readonly [TKey in keyof TModel["update"]["Type"]]: WithoutOverride<TModel["update"]["Type"][TKey]>;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
type Column<TModel extends Model.Any, TId extends PropertyKey> = Exclude<Extract<keyof UpdateInput<TModel>, string>, TId>;
|
|
12
|
-
type Selected<TModel extends Model.Any, TId extends PropertyKey, TColumn extends PropertyKey> = Required<
|
|
11
|
+
export type Column<TModel extends Model.Any, TId extends PropertyKey> = Exclude<Extract<keyof UpdateInput<TModel>, string>, TId>;
|
|
12
|
+
export type Selected<TModel extends Model.Any, TId extends PropertyKey, TColumn extends PropertyKey> = Required<
|
|
13
13
|
Pick<UpdateInput<TModel>, Extract<TId | TColumn, keyof UpdateInput<TModel>>>
|
|
14
14
|
>;
|
|
15
|
-
interface Columns<TColumn> {
|
|
15
|
+
export interface Columns<TColumn> {
|
|
16
16
|
readonly columns: readonly [TColumn, ...TColumn[]];
|
|
17
17
|
}
|
|
18
18
|
type WriteResult<TModel extends Model.Any, A, E = never> = Effect.Effect<
|
package/src/repositoryWrites.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
import type { Model } from "effect/schema";
|
|
3
3
|
import { SqlClient, SqlSchema } from "effect/sql";
|
|
4
|
+
import { storageSchema } from "#adapters/storageSchema.ts";
|
|
4
5
|
import type { RepositoryChanges } from "#repositoryChanges.ts";
|
|
5
6
|
import { makeFilter } from "#repositoryFilter.ts";
|
|
6
7
|
import { selectedUpdate } from "#repositoryUpdate.ts";
|
|
8
|
+
import { sqlNameTransforms } from "#sqlNameTransforms.ts";
|
|
7
9
|
|
|
8
10
|
export const repositoryWrites = Effect.fnUntraced(function* (
|
|
9
11
|
model: Model.Any,
|
|
@@ -11,6 +13,7 @@ export const repositoryWrites = Effect.fnUntraced(function* (
|
|
|
11
13
|
changes: RepositoryChanges,
|
|
12
14
|
) {
|
|
13
15
|
const sql = yield* SqlClient.SqlClient;
|
|
16
|
+
const nameTransforms = yield* sqlNameTransforms;
|
|
14
17
|
const { idColumns, tableName, spanPrefix } = options;
|
|
15
18
|
const { where } = yield* makeFilter(model);
|
|
16
19
|
const record = Schema.decodeUnknownEffect(Schema.Record(Schema.String, Schema.Unknown));
|
|
@@ -33,17 +36,29 @@ export const repositoryWrites = Effect.fnUntraced(function* (
|
|
|
33
36
|
),
|
|
34
37
|
);
|
|
35
38
|
}
|
|
36
|
-
const insertRow = SqlSchema.findOne({
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
const insertRow = SqlSchema.findOne({
|
|
40
|
+
execute: insert,
|
|
41
|
+
Request: storageSchema(model.insert, model.fields, nameTransforms),
|
|
42
|
+
Result: storageSchema(model, model.fields, nameTransforms),
|
|
43
|
+
});
|
|
44
|
+
const insertVoid = SqlSchema.void({ execute: insert, Request: storageSchema(model.insert, model.fields, nameTransforms) });
|
|
45
|
+
const updateRow = SqlSchema.findOne({
|
|
46
|
+
execute: update,
|
|
47
|
+
Request: storageSchema(model.update, model.fields, nameTransforms),
|
|
48
|
+
Result: storageSchema(model, model.fields, nameTransforms),
|
|
49
|
+
});
|
|
50
|
+
const updateVoid = SqlSchema.void({ execute: update, Request: storageSchema(model.update, model.fields, nameTransforms) });
|
|
40
51
|
function selected(input: unknown, columns: readonly string[], returning: boolean) {
|
|
41
52
|
return Effect.gen(function* () {
|
|
42
53
|
const row = yield* record(input);
|
|
43
54
|
const Request = yield* selectedUpdate(model, columns, idColumns, row);
|
|
44
55
|
return yield* returning
|
|
45
|
-
? SqlSchema.findOne({
|
|
46
|
-
|
|
56
|
+
? SqlSchema.findOne({
|
|
57
|
+
execute: update,
|
|
58
|
+
Request: storageSchema(Request, model.fields, nameTransforms),
|
|
59
|
+
Result: storageSchema(model, model.fields, nameTransforms),
|
|
60
|
+
})(row)
|
|
61
|
+
: SqlSchema.void({ execute: update, Request: storageSchema(Request, model.fields, nameTransforms) })(row);
|
|
47
62
|
});
|
|
48
63
|
}
|
|
49
64
|
const idColumn = idColumns[0] ?? "";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { initializeWorkerRelease } from "./initializeWorkerRelease.ts";
|
|
3
|
+
|
|
4
|
+
export function setWorkerReleaseActive(state: Effect.Success<typeof initializeWorkerRelease>) {
|
|
5
|
+
return (next: boolean) =>
|
|
6
|
+
state
|
|
7
|
+
.locked(
|
|
8
|
+
Effect.gen(function* () {
|
|
9
|
+
if (next === state.active) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
state.active = next;
|
|
13
|
+
yield* Effect.forEach(state.controls, (control) => control(state.active), { concurrency: "unbounded", discard: true });
|
|
14
|
+
}),
|
|
15
|
+
)
|
|
16
|
+
.pipe(Effect.uninterruptible);
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { SqlClient } from "effect/sql";
|
|
3
|
+
import type { SqlNameTransforms } from "#clientOptions.ts";
|
|
4
|
+
|
|
5
|
+
export const sqlNameTransforms = Effect.gen(function* () {
|
|
6
|
+
const client = yield* SqlClient.SqlClient;
|
|
7
|
+
const namedClient: SqlClient.SqlClient & { readonly config?: SqlNameTransforms } = client;
|
|
8
|
+
return namedClient.config;
|
|
9
|
+
});
|
package/src/upsertResult.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
import type { Model } from "effect/schema";
|
|
3
3
|
import { SqlClient } from "effect/sql";
|
|
4
|
+
import { storageSchema } from "#adapters/storageSchema.ts";
|
|
5
|
+
import type { SqlNameTransforms } from "#clientOptions.ts";
|
|
4
6
|
|
|
5
7
|
export interface UpsertRow<TRow> {
|
|
6
8
|
readonly inserted: boolean;
|
|
@@ -12,8 +14,8 @@ export const upsertReturning = Effect.fnUntraced(function* () {
|
|
|
12
14
|
return sql`*, (xmax = 0) as ${sql("effectSqlInserted")}`;
|
|
13
15
|
});
|
|
14
16
|
|
|
15
|
-
export function makeUpsertDecoder(model: Model.Any) {
|
|
16
|
-
const decode = Schema.decodeUnknownEffect(model);
|
|
17
|
+
export function makeUpsertDecoder(model: Model.Any, names?: SqlNameTransforms) {
|
|
18
|
+
const decode = Schema.decodeUnknownEffect(storageSchema(model, model.fields, names));
|
|
17
19
|
return (row: Readonly<Record<string, unknown>> | undefined) =>
|
|
18
20
|
Effect.map(decode(row), (decoded) => ({ inserted: row?.effectSqlInserted === true, row: decoded }));
|
|
19
21
|
}
|
|
@@ -2,10 +2,12 @@ import { Effect, Option, Schema } from "effect";
|
|
|
2
2
|
import type { Model } from "effect/schema";
|
|
3
3
|
import { SqlClient, type SqlModel } from "effect/sql";
|
|
4
4
|
import type { SqlError } from "effect/sql/SqlError";
|
|
5
|
+
import { storageSchema } from "#adapters/storageSchema.ts";
|
|
5
6
|
import type { PrimaryKey, Repository } from "#compositeRepository.ts";
|
|
6
7
|
import type { OptimisticConcurrencyError } from "#repository.ts";
|
|
7
8
|
import type { RepositoryChanges } from "#repositoryChanges.ts";
|
|
8
|
-
import type { UpdateInput } from "#repositoryUpdate.ts";
|
|
9
|
+
import type { Column, Columns, Selected, UpdateInput } from "#repositoryUpdate.ts";
|
|
10
|
+
import { sqlNameTransforms } from "#sqlNameTransforms.ts";
|
|
9
11
|
import { type VersionedOptions, versionedStatements } from "#versionedStatements.ts";
|
|
10
12
|
|
|
11
13
|
export type IdColumn<TModel extends Model.Any> = keyof TModel["Type"] & keyof TModel["update"]["Type"] & keyof TModel["fields"];
|
|
@@ -30,17 +32,34 @@ type WriteError = SqlError | Schema.SchemaError | OptimisticConcurrencyError;
|
|
|
30
32
|
type KeyColumn<TModel extends Model.Any, TId extends PrimaryKey<TModel>> = TId extends readonly IdColumn<TModel>[]
|
|
31
33
|
? TId[number]
|
|
32
34
|
: Extract<TId, IdColumn<TModel>>;
|
|
33
|
-
|
|
35
|
+
type RequiredColumn<TModel extends Model.Any, TId extends PrimaryKey<TModel>, TVersion extends PropertyKey> = KeyColumn<TModel, TId> | TVersion;
|
|
36
|
+
interface VersionedUpdate<
|
|
37
|
+
TModel extends Model.Any,
|
|
38
|
+
TId extends PrimaryKey<TModel>,
|
|
39
|
+
TVersion extends VersionColumn<TModel, TId>,
|
|
40
|
+
A,
|
|
41
|
+
R = TModel["update"]["EncodingServices"] | TModel["DecodingServices"],
|
|
42
|
+
> {
|
|
43
|
+
<TColumn extends Column<TModel, RequiredColumn<TModel, TId, TVersion>>>(
|
|
44
|
+
input: Selected<TModel, RequiredColumn<TModel, TId, TVersion>, TColumn>,
|
|
45
|
+
options: Columns<TColumn>,
|
|
46
|
+
): Effect.Effect<A, WriteError, R>;
|
|
47
|
+
(input: UpdateInput<TModel>): Effect.Effect<A, WriteError, R>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface VersionedWrites<
|
|
51
|
+
TModel extends Model.Any,
|
|
52
|
+
TId extends PrimaryKey<TModel>,
|
|
53
|
+
TVersion extends VersionColumn<TModel, TId> = VersionColumn<TModel, TId>,
|
|
54
|
+
> {
|
|
34
55
|
readonly delete: (
|
|
35
56
|
id: TId extends readonly IdColumn<TModel>[] ? Pick<TModel["Type"], TId[number]> : TModel["fields"][Extract<TId, IdColumn<TModel>>]["Type"],
|
|
36
57
|
expectedVersion: number,
|
|
37
58
|
) => Effect.Effect<void, WriteError, TModel["fields"][KeyColumn<TModel, TId>]["EncodingServices"]>;
|
|
38
59
|
readonly insert: Repository<TModel, TId>["insert"];
|
|
39
60
|
readonly insertVoid: Repository<TModel, TId>["insertVoid"];
|
|
40
|
-
readonly update:
|
|
41
|
-
|
|
42
|
-
) => Effect.Effect<TModel["Type"], WriteError, TModel["update"]["EncodingServices"] | TModel["DecodingServices"]>;
|
|
43
|
-
readonly updateVoid: (input: UpdateInput<TModel>) => Effect.Effect<void, WriteError, TModel["update"]["EncodingServices"]>;
|
|
61
|
+
readonly update: VersionedUpdate<TModel, TId, TVersion, TModel["Type"]>;
|
|
62
|
+
readonly updateVoid: VersionedUpdate<TModel, TId, TVersion, void, TModel["update"]["EncodingServices"]>;
|
|
44
63
|
}
|
|
45
64
|
|
|
46
65
|
export const makeVersionedWrites = Effect.fnUntraced(function* (
|
|
@@ -48,9 +67,10 @@ export const makeVersionedWrites = Effect.fnUntraced(function* (
|
|
|
48
67
|
options: VersionedOptions & { readonly changes: RepositoryChanges; readonly spanPrefix: string },
|
|
49
68
|
) {
|
|
50
69
|
const sql = yield* SqlClient.SqlClient;
|
|
70
|
+
const nameTransforms = yield* sqlNameTransforms;
|
|
51
71
|
const { spanPrefix, changes } = options;
|
|
52
72
|
const statements = yield* versionedStatements(model, options);
|
|
53
|
-
const decodeRow = Schema.decodeUnknownEffect(model);
|
|
73
|
+
const decodeRow = Schema.decodeUnknownEffect(storageSchema(model, model.fields, nameTransforms));
|
|
54
74
|
function insert(input: unknown) {
|
|
55
75
|
return Effect.gen(function* () {
|
|
56
76
|
const statement = yield* statements.insert(input);
|
|
@@ -74,9 +94,9 @@ export const makeVersionedWrites = Effect.fnUntraced(function* (
|
|
|
74
94
|
return rows.length === 0 ? Option.none() : Option.some(yield* decodeRow(rows[0]));
|
|
75
95
|
}).pipe(changes.write, Effect.withSpan(`${spanPrefix}.insertOrIgnore`));
|
|
76
96
|
}
|
|
77
|
-
function update(input: Readonly<Record<string, unknown
|
|
97
|
+
function update(input: Readonly<Record<string, unknown>>, settings?: { readonly columns: readonly string[] }) {
|
|
78
98
|
return Effect.gen(function* () {
|
|
79
|
-
const { statement, classify } = yield* statements.update(input);
|
|
99
|
+
const { statement, classify } = yield* statements.update(input, settings?.columns);
|
|
80
100
|
const rows = yield* sql`${statement} returning ${changes.returning()}`;
|
|
81
101
|
if (rows.length === 0) {
|
|
82
102
|
return yield* classify();
|
|
@@ -85,9 +105,9 @@ export const makeVersionedWrites = Effect.fnUntraced(function* (
|
|
|
85
105
|
return yield* decodeRow(rows[0]);
|
|
86
106
|
}).pipe(changes.write, Effect.withSpan(`${spanPrefix}.update`));
|
|
87
107
|
}
|
|
88
|
-
function updateVoid(input: Readonly<Record<string, unknown
|
|
108
|
+
function updateVoid(input: Readonly<Record<string, unknown>>, settings?: { readonly columns: readonly string[] }) {
|
|
89
109
|
return Effect.gen(function* () {
|
|
90
|
-
const { statement, classify } = yield* statements.update(input);
|
|
110
|
+
const { statement, classify } = yield* statements.update(input, settings?.columns);
|
|
91
111
|
const rows = yield* sql`${statement} returning ${changes.returning()}`;
|
|
92
112
|
if (rows.length === 0) {
|
|
93
113
|
return yield* classify();
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
import type { Model } from "effect/schema";
|
|
3
3
|
import { SqlClient } from "effect/sql";
|
|
4
|
+
import { storageSchema } from "#adapters/storageSchema.ts";
|
|
4
5
|
import { makeConflictClause } from "#makeConflictClause.ts";
|
|
5
6
|
import type { OptimisticConcurrencyError } from "#repository.ts";
|
|
7
|
+
import { selectedUpdate } from "#repositoryUpdate.ts";
|
|
8
|
+
import { sqlNameTransforms } from "#sqlNameTransforms.ts";
|
|
6
9
|
|
|
7
10
|
const Version = Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0));
|
|
8
11
|
export interface VersionedOptions {
|
|
@@ -16,6 +19,7 @@ export interface VersionedOptions {
|
|
|
16
19
|
|
|
17
20
|
export const versionedStatements = Effect.fnUntraced(function* (model: Model.Any, options: VersionedOptions) {
|
|
18
21
|
const sql = yield* SqlClient.SqlClient;
|
|
22
|
+
const nameTransforms = yield* sqlNameTransforms;
|
|
19
23
|
const { tableName, idColumns, versionColumn, conflict } = options;
|
|
20
24
|
const entries: [string, Schema.Constraint][] = [];
|
|
21
25
|
for (const column of idColumns) {
|
|
@@ -31,8 +35,8 @@ export const versionedStatements = Effect.fnUntraced(function* (model: Model.Any
|
|
|
31
35
|
return yield* Effect.die(new Error(`Unknown version column: ${versionColumn}`));
|
|
32
36
|
}
|
|
33
37
|
const encodeVersion = Schema.encodeEffect(versionSchema);
|
|
34
|
-
const encodeInsert = Schema.encodeEffect(model.insert);
|
|
35
|
-
const encodeUpdate = Schema.encodeEffect(model.update);
|
|
38
|
+
const encodeInsert = Schema.encodeEffect(storageSchema(model.insert, model.fields, nameTransforms));
|
|
39
|
+
const encodeUpdate = Schema.encodeEffect(storageSchema(model.update, model.fields, nameTransforms));
|
|
36
40
|
const record = Schema.decodeUnknownEffect(Schema.Record(Schema.String, Schema.Unknown));
|
|
37
41
|
const validateVersion = Schema.decodeUnknownEffect(Version);
|
|
38
42
|
const clause =
|
|
@@ -59,9 +63,15 @@ export const versionedStatements = Effect.fnUntraced(function* (model: Model.Any
|
|
|
59
63
|
return sql`insert into ${sql(tableName)} ${sql.insert(encoded)} ${suffix}`;
|
|
60
64
|
});
|
|
61
65
|
}
|
|
62
|
-
function update(input: Readonly<Record<string, unknown
|
|
66
|
+
function update(input: Readonly<Record<string, unknown>>, columns?: readonly string[]) {
|
|
63
67
|
return Effect.gen(function* () {
|
|
64
|
-
const encoded = yield* record(
|
|
68
|
+
const encoded = yield* record(
|
|
69
|
+
yield* columns === undefined
|
|
70
|
+
? encodeUpdate(input)
|
|
71
|
+
: Effect.flatMap(selectedUpdate(model, columns, [...idColumns, versionColumn], input), (schema) =>
|
|
72
|
+
Schema.encodeEffect(storageSchema(schema, model.fields, nameTransforms))(input),
|
|
73
|
+
),
|
|
74
|
+
);
|
|
65
75
|
const expectedVersion = yield* validateVersion(encoded[versionColumn]);
|
|
66
76
|
const assignments = Object.entries(encoded).filter(([column]) => !idColumns.includes(column) && column !== versionColumn);
|
|
67
77
|
const set = sql.csv([
|