@shivaedev/sql 0.28.0 → 0.30.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +96 -4
  3. package/dist/batchedDelete.d.ts +33 -0
  4. package/dist/batchedDelete.d.ts.map +1 -0
  5. package/dist/batchedDelete.js +64 -0
  6. package/dist/batchedDelete.js.map +1 -0
  7. package/dist/compositeRepository.d.ts +14 -13
  8. package/dist/compositeRepository.d.ts.map +1 -1
  9. package/dist/compositeRepository.js.map +1 -1
  10. package/dist/jsonb.d.ts +3 -0
  11. package/dist/jsonb.d.ts.map +1 -0
  12. package/dist/jsonb.js +9 -0
  13. package/dist/jsonb.js.map +1 -0
  14. package/dist/repository.d.ts +5 -9
  15. package/dist/repository.d.ts.map +1 -1
  16. package/dist/repository.js +21 -17
  17. package/dist/repository.js.map +1 -1
  18. package/dist/repositoryChanges.d.ts +1 -0
  19. package/dist/repositoryChanges.d.ts.map +1 -1
  20. package/dist/repositoryChanges.js +9 -1
  21. package/dist/repositoryChanges.js.map +1 -1
  22. package/dist/repositoryCollection.d.ts +28 -0
  23. package/dist/repositoryCollection.d.ts.map +1 -0
  24. package/dist/repositoryCollection.js +21 -0
  25. package/dist/repositoryCollection.js.map +1 -0
  26. package/dist/repositoryUpdate.d.ts +31 -0
  27. package/dist/repositoryUpdate.d.ts.map +1 -0
  28. package/dist/repositoryUpdate.js +16 -0
  29. package/dist/repositoryUpdate.js.map +1 -0
  30. package/dist/repositoryWrites.d.ts +5 -2
  31. package/dist/repositoryWrites.d.ts.map +1 -1
  32. package/dist/repositoryWrites.js +19 -4
  33. package/dist/repositoryWrites.js.map +1 -1
  34. package/dist/testing.d.ts +2 -1
  35. package/dist/testing.d.ts.map +1 -1
  36. package/dist/testing.js +1 -1
  37. package/dist/testing.js.map +1 -1
  38. package/dist/tryPurgeLock.d.ts +4 -0
  39. package/dist/tryPurgeLock.d.ts.map +1 -0
  40. package/dist/tryPurgeLock.js +15 -0
  41. package/dist/tryPurgeLock.js.map +1 -0
  42. package/dist/versionedRepository.d.ts +3 -2
  43. package/dist/versionedRepository.d.ts.map +1 -1
  44. package/dist/versionedRepository.js.map +1 -1
  45. package/package.json +4 -4
  46. package/src/batchedDelete.ts +93 -0
  47. package/src/compositeRepository.ts +25 -34
  48. package/src/jsonb.ts +12 -0
  49. package/src/repository.ts +30 -28
  50. package/src/repositoryChanges.ts +9 -1
  51. package/src/repositoryCollection.ts +46 -0
  52. package/src/repositoryUpdate.ts +65 -0
  53. package/src/repositoryWrites.ts +19 -4
  54. package/src/testing.ts +7 -3
  55. package/src/tryPurgeLock.ts +18 -0
  56. package/src/versionedRepository.ts +3 -2
package/src/repository.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import { Data, Effect, type Schema } from "effect";
2
2
  import type { Model } from "effect/schema";
3
3
  import { SqlClient } from "effect/sql";
4
+ import { makeBatchedDelete } from "#batchedDelete.ts";
4
5
  import { type BatchWrites, makeInsertMany } from "#batchWrites.ts";
5
6
  import { makeCompositeCrud, type PrimaryKey, type Repository } from "#compositeRepository.ts";
6
7
  import { type ConflictColumns, type ConflictWrites, makeConflictWrites } from "#conflictWrites.ts";
7
8
  import { makeInsertFromSelect, type SelectWrites } from "#insertFromSelect.ts";
8
9
  import { repositoryChanges } from "#repositoryChanges.ts";
10
+ import { type BucketRegistrationError, type RepositoryOptions, validateRepositoryCollection } from "#repositoryCollection.ts";
9
11
  import type { FindManyFilter, RepositoryFilter, RepositoryListFilter } from "#repositoryFilter.ts";
10
12
  import { makeQueries, type Queries } from "#repositoryQueries.ts";
11
13
  import { type IdColumn, makeVersionedWrites, type VersionColumn, type VersionedWrites } from "#versionedRepository.ts";
@@ -39,12 +41,6 @@ export class OptimisticConcurrencyError extends Data.TaggedError("OptimisticConc
39
41
  readonly reason: "missing" | "stale";
40
42
  }> {}
41
43
 
42
- interface RepositoryOptions<TId> {
43
- readonly idColumn: TId;
44
- readonly spanPrefix: string;
45
- readonly tableName: string;
46
- }
47
-
48
44
  type VersionedRepository<TModel extends Model.Any, TId extends PrimaryKey<TModel>> = Omit<
49
45
  Repository<TModel, TId>,
50
46
  keyof VersionedWrites<TModel, TId>
@@ -67,7 +63,7 @@ export function makeRepository<
67
63
  ): Effect.Effect<
68
64
  VersionedRepository<TModel, TId> &
69
65
  Pick<ConflictWrites<TModel, never, TModel["fields"][Extract<TWhere, keyof TModel["fields"]>]["EncodingServices"]>, "insertOrIgnore">,
70
- never,
66
+ BucketRegistrationError,
71
67
  SqlClient.SqlClient
72
68
  >;
73
69
  export function makeRepository<TModel extends Model.Any, const TId extends PrimaryKey<TModel>>(
@@ -77,7 +73,7 @@ export function makeRepository<TModel extends Model.Any, const TId extends Prima
77
73
  readonly conflictColumns?: never;
78
74
  readonly conflictWhere?: never;
79
75
  },
80
- ): Effect.Effect<VersionedRepository<TModel, TId>, never, SqlClient.SqlClient>;
76
+ ): Effect.Effect<VersionedRepository<TModel, TId>, BucketRegistrationError, SqlClient.SqlClient>;
81
77
  export function makeRepository<
82
78
  TModel extends Model.Any,
83
79
  const TId extends PrimaryKey<TModel>,
@@ -100,13 +96,17 @@ export function makeRepository<
100
96
  TConflict[number] | (TId extends readonly PropertyKey[] ? TId[number] : TId),
101
97
  TModel["fields"][Extract<TWhere, keyof TModel["fields"]>]["EncodingServices"]
102
98
  >,
103
- never,
99
+ BucketRegistrationError,
104
100
  SqlClient.SqlClient
105
101
  >;
106
102
  export function makeRepository<TModel extends Model.Any, const TId extends PrimaryKey<TModel>>(
107
103
  model: TModel,
108
104
  options: RepositoryOptions<TId> & { readonly versionColumn?: never; readonly conflictColumns?: never; readonly conflictWhere?: never },
109
- ): Effect.Effect<Repository<TModel, TId> & BatchWrites<TModel> & SelectWrites<TModel> & Queries<TModel>, never, SqlClient.SqlClient>;
105
+ ): Effect.Effect<
106
+ Repository<TModel, TId> & BatchWrites<TModel> & SelectWrites<TModel> & Queries<TModel>,
107
+ BucketRegistrationError,
108
+ SqlClient.SqlClient
109
+ >;
110
110
  export function makeRepository<TModel extends Model.Any, TId extends IdColumn<TModel>>(
111
111
  model: TModel,
112
112
  options: RepositoryOptions<TId | readonly [TId, TId, ...TId[]]> & {
@@ -114,34 +114,36 @@ export function makeRepository<TModel extends Model.Any, TId extends IdColumn<TM
114
114
  readonly conflictColumns?: readonly string[];
115
115
  readonly conflictWhere?: Readonly<Record<string, unknown>>;
116
116
  },
117
- ): Effect.Effect<unknown, never, SqlClient.SqlClient> {
117
+ ): Effect.Effect<unknown, BucketRegistrationError, SqlClient.SqlClient> {
118
118
  return Effect.fnUntraced(function* () {
119
+ yield* validateRepositoryCollection(model.fields, options);
119
120
  const sql = yield* SqlClient.SqlClient;
120
121
  const composite = typeof options.idColumn === "object";
121
122
  const idColumns = (composite ? options.idColumn : [options.idColumn]).map(String);
122
123
  const changes = yield* repositoryChanges(sql, options.tableName, idColumns, model.fields);
123
- const crud = makeCompositeCrud(model, sql, { ...options, columns: idColumns }, changes);
124
+ const crud = {
125
+ ...makeCompositeCrud(model, sql, { ...options, columns: idColumns }, changes),
126
+ deleteBatched: yield* makeBatchedDelete(model, options, changes),
127
+ };
124
128
  const queries = makeQueries(model, sql, options);
125
- return options.versionColumn === undefined
126
- ? {
127
- ...crud,
128
- insertFromSelect: makeInsertFromSelect(model, sql, { ...options, changes }, "insertFromSelect"),
129
- insertMany: makeInsertMany(model, sql, { ...options, changes }, "insertMany"),
130
- ...(options.conflictColumns === undefined
131
- ? {}
132
- : makeConflictWrites(model, sql, { ...options, changes, conflictColumns: options.conflictColumns, idColumns })),
133
- ...queries,
134
- }
135
- : {
136
- ...crud,
137
- ...makeVersionedWrites(model, sql, {
129
+ return {
130
+ ...crud,
131
+ ...queries,
132
+ ...(options.versionColumn === undefined
133
+ ? {
134
+ insertFromSelect: makeInsertFromSelect(model, sql, { ...options, changes }, "insertFromSelect"),
135
+ insertMany: makeInsertMany(model, sql, { ...options, changes }, "insertMany"),
136
+ ...(options.conflictColumns === undefined
137
+ ? {}
138
+ : makeConflictWrites(model, sql, { ...options, changes, conflictColumns: options.conflictColumns, idColumns })),
139
+ }
140
+ : makeVersionedWrites(model, sql, {
138
141
  ...options,
139
142
  changes,
140
143
  conflict: (id, expectedVersion, reason) => new OptimisticConcurrencyError({ expectedVersion, id, reason, tableName: options.tableName }),
141
144
  idColumns,
142
145
  versionColumn: options.versionColumn,
143
- }),
144
- ...queries,
145
- };
146
+ })),
147
+ };
146
148
  })();
147
149
  }
@@ -66,5 +66,13 @@ export const repositoryChanges = Effect.fnUntraced(function* (
66
66
  function write<A, E, R>(effect: Effect.Effect<A, E, R>) {
67
67
  return provide(transact(effect, { onSqlError: (error) => error }));
68
68
  }
69
- return { record, returning, write };
69
+ const recordTable = Effect.fnUntraced(function* (rows: number) {
70
+ if (rows === 0) {
71
+ return;
72
+ }
73
+ const declarations = yield* ChangeDeclarations;
74
+ yield* declarations.declare(table, [table], rows);
75
+ yield* provide(invalidateOnCommit([table]));
76
+ });
77
+ return { record, recordTable, returning, write };
70
78
  });
@@ -0,0 +1,46 @@
1
+ import { Data, Effect, Order, type Schema } from "effect";
2
+ import { declaredBuckets } from "@shivaedev/changes/buckets.ts";
3
+
4
+ export interface RepositoryCollection {
5
+ readonly bucket: Readonly<Record<string, unknown>>;
6
+ readonly name: string;
7
+ }
8
+
9
+ export interface RepositoryOptions<TId> {
10
+ readonly collection?: RepositoryCollection;
11
+ readonly idColumn: TId;
12
+ readonly spanPrefix: string;
13
+ readonly tableName: string;
14
+ }
15
+
16
+ export class BucketRegistrationError extends Data.TaggedError("BucketRegistrationError")<{
17
+ readonly tableName: string;
18
+ readonly collectionName: string | undefined;
19
+ readonly modelFields: readonly string[];
20
+ readonly collectionFields: readonly string[];
21
+ }> {
22
+ override get message() {
23
+ return `Bucket registration for ${this.tableName}: model fields ${JSON.stringify(this.modelFields)} disagree with collection ${this.collectionName ?? "(missing)"} fields ${JSON.stringify(this.collectionFields)}`;
24
+ }
25
+ }
26
+
27
+ export const validateRepositoryCollection = Effect.fnUntraced(function* (
28
+ fields: Schema.Struct.Fields,
29
+ options: { readonly tableName: string; readonly collection?: RepositoryCollection },
30
+ ) {
31
+ const modelFields = declaredBuckets(fields)
32
+ .map(({ field }) => field)
33
+ .sort(Order.String);
34
+ const collectionFields = Object.keys(options.collection?.bucket ?? {}).sort(Order.String);
35
+ if (
36
+ (options.collection === undefined && modelFields.length === 0)
37
+ || (options.collection?.name === options.tableName
38
+ && modelFields.length === collectionFields.length
39
+ && modelFields.every((field, index) => field === collectionFields[index]))
40
+ ) {
41
+ return;
42
+ }
43
+ return yield* Effect.fail(
44
+ new BucketRegistrationError({ collectionFields, collectionName: options.collection?.name, modelFields, tableName: options.tableName }),
45
+ );
46
+ });
@@ -0,0 +1,65 @@
1
+ import type { Brand, Cause } from "effect";
2
+ import { Effect, Schema, SchemaAST, SchemaIssue } from "effect";
3
+ import type { Model } from "effect/schema";
4
+ import type { SqlError } from "effect/sql/SqlError";
5
+
6
+ type WithoutOverride<A> = A extends infer TValue & Brand.Brand<"Override"> ? TValue : A;
7
+ export type UpdateInput<TModel extends Model.Any> = {
8
+ readonly [TKey in keyof TModel["update"]["Type"]]: WithoutOverride<TModel["update"]["Type"][TKey]>;
9
+ };
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<
13
+ Pick<UpdateInput<TModel>, Extract<TId | TColumn, keyof UpdateInput<TModel>>>
14
+ >;
15
+ interface Columns<TColumn> {
16
+ readonly columns: readonly [TColumn, ...TColumn[]];
17
+ }
18
+ type WriteResult<TModel extends Model.Any, A, E = never> = Effect.Effect<
19
+ A,
20
+ SqlError | Schema.SchemaError | E,
21
+ TModel["update"]["EncodingServices"] | TModel["DecodingServices"]
22
+ >;
23
+
24
+ export interface Update<TModel extends Model.Any, TId extends PropertyKey> {
25
+ <TColumn extends Column<TModel, TId>>(
26
+ input: Selected<TModel, TId, TColumn>,
27
+ options: Columns<TColumn> & { readonly onMissing: "fail" },
28
+ ): WriteResult<TModel, TModel["Type"], Cause.NoSuchElementError>;
29
+ <TColumn extends Column<TModel, TId>>(input: Selected<TModel, TId, TColumn>, options: Columns<TColumn>): WriteResult<TModel, TModel["Type"]>;
30
+ (input: UpdateInput<TModel>, options: { readonly onMissing: "fail" }): WriteResult<TModel, TModel["Type"], Cause.NoSuchElementError>;
31
+ (input: UpdateInput<TModel>): WriteResult<TModel, TModel["Type"]>;
32
+ }
33
+
34
+ export interface UpdateVoid<TModel extends Model.Any, TId extends PropertyKey> {
35
+ <TColumn extends Column<TModel, TId>>(
36
+ input: Selected<TModel, TId, TColumn>,
37
+ options: Columns<TColumn>,
38
+ ): Effect.Effect<void, SqlError | Schema.SchemaError, TModel["update"]["EncodingServices"]>;
39
+ (input: UpdateInput<TModel>): Effect.Effect<void, SqlError | Schema.SchemaError, TModel["update"]["EncodingServices"]>;
40
+ }
41
+
42
+ export const selectedUpdate = Effect.fnUntraced(function* (
43
+ model: Model.Any,
44
+ columns: readonly string[],
45
+ idColumns: readonly string[],
46
+ input: Readonly<Record<string, unknown>>,
47
+ ) {
48
+ const ast = model.update.ast;
49
+ const fields = SchemaAST.isObjects(ast) ? ast.propertySignatures : [];
50
+ const writable = new Set(fields.map((field) => String(field.name)));
51
+ const invalid = columns.find((column) => idColumns.includes(column) || !writable.has(column));
52
+ if (columns.length === 0 || invalid !== undefined) {
53
+ return yield* Effect.fail(new Schema.SchemaError(new SchemaIssue.Pointer([invalid ?? ""], new SchemaIssue.UnexpectedKey(ast, invalid ?? ""))));
54
+ }
55
+ const missing = [...idColumns, ...columns].find((column) => !Object.hasOwn(input, column));
56
+ if (missing !== undefined) {
57
+ return yield* Effect.fail(new Schema.SchemaError(new SchemaIssue.Pointer([missing], new SchemaIssue.MissingKey(undefined))));
58
+ }
59
+ return Schema.make(
60
+ new SchemaAST.Objects(
61
+ fields.filter((field) => [...idColumns, ...columns].includes(String(field.name))),
62
+ [],
63
+ ),
64
+ );
65
+ });
@@ -3,6 +3,7 @@ import type { Model } from "effect/schema";
3
3
  import { type SqlClient, SqlSchema } from "effect/sql";
4
4
  import type { RepositoryChanges } from "./repositoryChanges.ts";
5
5
  import { makeFilter } from "./repositoryFilter.ts";
6
+ import { selectedUpdate } from "./repositoryUpdate.ts";
6
7
 
7
8
  export function repositoryWrites(
8
9
  model: Model.Any,
@@ -36,6 +37,15 @@ export function repositoryWrites(
36
37
  const insertVoid = SqlSchema.void({ execute: insert, Request: model.insert });
37
38
  const updateRow = SqlSchema.findOne({ execute: update, Request: model.update, Result: model });
38
39
  const updateVoid = SqlSchema.void({ execute: update, Request: model.update });
40
+ function selected(input: unknown, columns: readonly string[], returning: boolean) {
41
+ return Effect.gen(function* () {
42
+ const row = yield* record(input);
43
+ const Request = yield* selectedUpdate(model, columns, idColumns, row);
44
+ return yield* returning
45
+ ? SqlSchema.findOne({ execute: update, Request, Result: model })(row)
46
+ : SqlSchema.void({ execute: update, Request })(row);
47
+ });
48
+ }
39
49
  const idColumn = idColumns[0] ?? "";
40
50
  const key = model.fields[idColumn];
41
51
  if (key === undefined) {
@@ -63,10 +73,15 @@ export function repositoryWrites(
63
73
  insert: (input: unknown) =>
64
74
  changes.write(insertRow(input).pipe(Effect.catchTag("NoSuchElementError", Effect.die))).pipe(Effect.withSpan(`${spanPrefix}.insert`)),
65
75
  insertVoid: (input: unknown) => changes.write(insertVoid(input)).pipe(Effect.withSpan(`${spanPrefix}.insertVoid`)),
66
- update: (input: unknown, settings?: { readonly onMissing: "fail" }) =>
76
+ update: (input: unknown, settings?: { readonly onMissing?: "fail"; readonly columns?: readonly string[] }) => {
77
+ const result = settings?.columns === undefined ? updateRow(input) : selected(input, settings.columns, true);
78
+ return changes
79
+ .write(settings?.onMissing === "fail" ? result : result.pipe(Effect.catchTag("NoSuchElementError", Effect.die)))
80
+ .pipe(Effect.withSpan(`${spanPrefix}.update`));
81
+ },
82
+ updateVoid: (input: unknown, settings?: { readonly columns: readonly string[] }) =>
67
83
  changes
68
- .write(settings?.onMissing === "fail" ? updateRow(input) : updateRow(input).pipe(Effect.catchTag("NoSuchElementError", Effect.die)))
69
- .pipe(Effect.withSpan(`${spanPrefix}.update`)),
70
- updateVoid: (input: unknown) => changes.write(updateVoid(input)).pipe(Effect.withSpan(`${spanPrefix}.updateVoid`)),
84
+ .write(settings === undefined ? updateVoid(input) : selected(input, settings.columns, false))
85
+ .pipe(Effect.withSpan(`${spanPrefix}.updateVoid`)),
71
86
  };
72
87
  }
package/src/testing.ts CHANGED
@@ -76,8 +76,12 @@ export class PublishedKeysMismatch extends Data.TaggedError("PublishedKeysMismat
76
76
  }
77
77
  }
78
78
 
79
- export function expectPublished(keys: readonly unknown[]) {
80
- return Effect.fn("Changes.expectPublished")(function* <A, E, R>(body: Effect.Effect<A, E, R>) {
79
+ export function expectPublished(keys: readonly unknown[]): <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E | PublishedKeysMismatch, R>;
80
+ export function expectPublished<A>(
81
+ keys: (result: A) => readonly unknown[],
82
+ ): <E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E | PublishedKeysMismatch, R>;
83
+ export function expectPublished<A>(keys: readonly unknown[] | ((result: A) => readonly unknown[])) {
84
+ return Effect.fn("Changes.expectPublished")(function* <E, R>(body: Effect.Effect<A, E, R>) {
81
85
  const observer = yield* ChangeObserver;
82
86
  const actual = new Set<unknown>();
83
87
  const result = yield* body.pipe(
@@ -90,7 +94,7 @@ export function expectPublished(keys: readonly unknown[]) {
90
94
  return observer(event);
91
95
  }),
92
96
  );
93
- const expected = new Set(keys);
97
+ const expected = new Set(typeof keys === "function" ? keys(result) : keys);
94
98
  if (actual.size !== expected.size || [...expected].some((key) => !actual.has(key))) {
95
99
  return yield* Effect.fail(new PublishedKeysMismatch({ actual: [...actual], expected: [...expected] }));
96
100
  }
@@ -0,0 +1,18 @@
1
+ import { Effect } from "effect";
2
+ import { SqlClient } from "effect/sql";
3
+ import type { Statement } from "effect/sql/Statement";
4
+
5
+ export const tryPurgeLock = Effect.fnUntraced(function* (tableName: string) {
6
+ const sql = yield* SqlClient.SqlClient;
7
+ const [identifier] = sql`${sql(tableName)}`.compile();
8
+ const key = sql`hashtextextended('@shivaedev/sql/deleteBatched:' || ${identifier}::regclass::oid::text, 0)`;
9
+ const connection = yield* sql.reserve;
10
+ function execute(statement: Statement<Readonly<Record<string, unknown>>>) {
11
+ const [text, parameters] = statement.compile();
12
+ return connection.execute(text, parameters, undefined);
13
+ }
14
+ const [lock] = yield* Effect.acquireRelease(execute(sql`select pg_try_advisory_lock(${key}) as acquired`), (rows) =>
15
+ rows[0]?.acquired === true ? Effect.orDie(execute(sql`select pg_advisory_unlock(${key})`)) : Effect.void,
16
+ );
17
+ return lock?.acquired === true;
18
+ });
@@ -4,6 +4,7 @@ import type { SqlClient, 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 { UpdateInput } from "#repositoryUpdate.ts";
7
8
  import { type VersionedOptions, versionedStatements } from "#versionedStatements.ts";
8
9
  import type { RepositoryChanges } from "./repositoryChanges.ts";
9
10
 
@@ -37,9 +38,9 @@ export interface VersionedWrites<TModel extends Model.Any, TId extends PrimaryKe
37
38
  readonly insert: Repository<TModel, TId>["insert"];
38
39
  readonly insertVoid: Repository<TModel, TId>["insertVoid"];
39
40
  readonly update: (
40
- input: TModel["update"]["Type"],
41
+ input: UpdateInput<TModel>,
41
42
  ) => Effect.Effect<TModel["Type"], WriteError, TModel["update"]["EncodingServices"] | TModel["DecodingServices"]>;
42
- readonly updateVoid: (input: TModel["update"]["Type"]) => Effect.Effect<void, WriteError, TModel["update"]["EncodingServices"]>;
43
+ readonly updateVoid: (input: UpdateInput<TModel>) => Effect.Effect<void, WriteError, TModel["update"]["EncodingServices"]>;
43
44
  }
44
45
 
45
46
  export function makeVersionedWrites(