@gaganref/confect-server 10.3.1-fork.12 → 10.3.1-fork.14
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/dist/DatabaseWriter.d.ts.map +1 -1
- package/dist/DatabaseWriter.js +23 -3
- package/dist/DatabaseWriter.js.map +1 -1
- package/dist/internal/DocumentPatch.d.ts +16 -0
- package/dist/internal/DocumentPatch.d.ts.map +1 -0
- package/dist/internal/DocumentPatch.js +36 -0
- package/dist/internal/DocumentPatch.js.map +1 -0
- package/package.json +2 -2
- package/src/DatabaseWriter.ts +94 -21
- package/src/internal/DocumentPatch.ts +87 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DatabaseWriter.d.ts","sourceRoot":"","sources":["../src/DatabaseWriter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,qBAAqB,EAEtB,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"DatabaseWriter.d.ts","sourceRoot":"","sources":["../src/DatabaseWriter.ts"],"names":[],"mappings":"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"}
|
package/dist/DatabaseWriter.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { __exportAll } from "./_virtual/_rolldown/runtime.js";
|
|
2
2
|
import { tables } from "./DatabaseSchema.js";
|
|
3
|
-
import { encode } from "./Document.js";
|
|
4
|
-
import { getById } from "./QueryInitializer.js";
|
|
3
|
+
import { DocumentEncodeError, decode, encode } from "./Document.js";
|
|
4
|
+
import { GetByIdFailure, getById } from "./QueryInitializer.js";
|
|
5
|
+
import { encoder } from "./internal/DocumentPatch.js";
|
|
5
6
|
import * as Context from "effect/Context";
|
|
6
7
|
import * as Layer from "effect/Layer";
|
|
7
8
|
import * as Effect from "effect/Effect";
|
|
8
9
|
import { pipe } from "effect/Function";
|
|
10
|
+
import * as Predicate from "effect/Predicate";
|
|
11
|
+
import * as Result from "effect/Result";
|
|
9
12
|
import * as Record from "effect/Record";
|
|
10
13
|
//#region src/DatabaseWriter.ts
|
|
11
14
|
var DatabaseWriter_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -20,11 +23,28 @@ const make = (databaseSchema, convexDatabaseWriter) => {
|
|
|
20
23
|
const encodedDocument = yield* encode(document, tableDef);
|
|
21
24
|
return yield* Effect.promise(() => convexDatabaseWriter.insert(tableName, encodedDocument));
|
|
22
25
|
});
|
|
23
|
-
const
|
|
26
|
+
const replacePatched = (id, patchedValues) => Effect.gen(function* () {
|
|
24
27
|
const originalDecodedDoc = yield* getById(convexDatabaseWriter, tableDef)(id);
|
|
25
28
|
const updatedEncodedDoc = yield* pipe(patchedValues, Record.reduce(originalDecodedDoc, (acc, value, key) => value === void 0 ? Record.remove(acc, key) : Record.set(acc, key, value)), encode(tableDef));
|
|
26
29
|
yield* Effect.promise(() => convexDatabaseWriter.replace(id, updatedEncodedDoc));
|
|
27
30
|
});
|
|
31
|
+
const patch = Effect.fn("DatabaseWriter.patch")(function* (id, patchedValues) {
|
|
32
|
+
const encodePatch = encoder(tableDef);
|
|
33
|
+
if (encodePatch === void 0) return yield* replacePatched(id, patchedValues);
|
|
34
|
+
const storedDoc = yield* pipe(Effect.promise(() => convexDatabaseWriter.get(id)), Effect.andThen((value) => Effect.fromResult(Result.fromNullishOr(value, () => new GetByIdFailure({
|
|
35
|
+
tableName,
|
|
36
|
+
id
|
|
37
|
+
})))));
|
|
38
|
+
const encodedPatch = yield* pipe(encodePatch(patchedValues), Effect.mapError(({ message }) => message), Effect.tap((delta) => pipe({
|
|
39
|
+
...storedDoc,
|
|
40
|
+
...delta
|
|
41
|
+
}, Record.filter(Predicate.isNotUndefined), decode(tableDef), Effect.mapError(({ parseError }) => parseError))), Effect.catch((parseError) => pipe(decode(storedDoc, tableDef), Effect.andThen(Effect.fail(new DocumentEncodeError({
|
|
42
|
+
tableName,
|
|
43
|
+
id,
|
|
44
|
+
parseError
|
|
45
|
+
}))))));
|
|
46
|
+
yield* Effect.promise(() => convexDatabaseWriter.patch(id, encodedPatch));
|
|
47
|
+
});
|
|
28
48
|
const replace = Effect.fn("DatabaseWriter.replace")(function* (id, value) {
|
|
29
49
|
const updatedEncodedDoc = yield* encode(value, tableDef);
|
|
30
50
|
yield* Effect.promise(() => convexDatabaseWriter.replace(id, updatedEncodedDoc));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DatabaseWriter.js","names":["DatabaseSchema.tables","Document.encode","QueryInitializer.getById"],"sources":["../src/DatabaseWriter.ts"],"sourcesContent":["import type {\n BetterOmit,\n DocumentByName,\n Expand,\n GenericDatabaseWriter,\n WithoutSystemFields,\n} from \"convex/server\";\nimport type { GenericId } from \"convex/values\";\nimport { pipe } from \"effect/Function\";\nimport * as Context from \"effect/Context\";\nimport * as Effect from \"effect/Effect\";\nimport * as Layer from \"effect/Layer\";\nimport * as Record from \"effect/Record\";\nimport * as DatabaseSchema from \"./DatabaseSchema.js\";\nimport type * as DataModel from \"./DataModel.js\";\nimport type { DocumentByName as DocumentByName_ } from \"./DataModel.js\";\nimport * as Document from \"./Document.js\";\nimport * as QueryInitializer from \"./QueryInitializer.js\";\nimport type * as Table from \"./Table.js\";\n\n/**\n * The argument accepted by `patch`: like `Partial<Doc>` without system fields,\n * but the fields that are already optional also accept `undefined`, since\n * setting a field to `undefined` unsets it.\n */\nexport type PatchValue<Doc> = {\n [\n K in keyof Doc as K extends \"_id\" | \"_creationTime\" ? never : K\n ]?: undefined extends Doc[K] ? Doc[K] | undefined : Doc[K];\n};\n\nexport interface DatabaseWriterTableAccessor<\n DataModel_ extends DataModel.AnyWithProps,\n TableName extends DataModel.TableNames<DataModel_>,\n Doc = DocumentByName_<DataModel_, TableName>,\n> {\n readonly insert: (\n document: Document.WithoutSystemFields<Doc>,\n ) => Effect.Effect<\n DataModel.Id<DataModel_, TableName>,\n Document.DocumentEncodeError\n >;\n readonly patch: (\n id: DataModel.Id<DataModel_, TableName>,\n patchedValues: PatchValue<Doc>,\n ) => Effect.Effect<\n void,\n | QueryInitializer.GetByIdFailure\n | Document.DocumentDecodeError\n | Document.DocumentEncodeError\n >;\n readonly replace: (\n id: DataModel.Id<DataModel_, TableName>,\n value: Document.WithoutSystemFields<Doc>,\n ) => Effect.Effect<void, Document.DocumentEncodeError>;\n readonly delete: (\n id: DataModel.Id<DataModel_, TableName>,\n ) => Effect.Effect<void>;\n}\n\n/**\n * The service shape backing the `DatabaseWriter` tag. Named (rather than an\n * inferred anonymous object) so declaration emit prints\n * `DatabaseWriterService<…>` by reference instead of expanding the data model.\n * `Docs` is the optional named document registry (see\n * `DatabaseReaderService`).\n */\nexport interface DatabaseWriterService<\n DatabaseSchema_ extends DatabaseSchema.AnyWithProps,\n Docs = {},\n> {\n readonly table: <\n const TableName extends DataModel.TableNames<\n DataModel.FromSchema<DatabaseSchema_>\n >,\n >(\n tableName: TableName,\n ) => DatabaseWriterTableAccessor<\n DataModel.FromSchema<DatabaseSchema_>,\n TableName,\n TableName extends keyof Docs\n ? Docs[TableName]\n : DocumentByName_<DataModel.FromSchema<DatabaseSchema_>, TableName>\n >;\n}\n\n/**\n * The tag's _Identifier_ is `Docs`-independent (see `DatabaseReaderTag`); only\n * the _Service_ carries `Docs` so writer inputs print the named doc\n * interfaces.\n */\nexport type DatabaseWriterTag<\n DatabaseSchema_ extends DatabaseSchema.AnyWithProps,\n Docs = {},\n> = Context.Service<\n DatabaseWriterService<DatabaseSchema_>,\n DatabaseWriterService<DatabaseSchema_, Docs>\n>;\n\nexport const make = <DatabaseSchema_ extends DatabaseSchema.AnyWithProps>(\n databaseSchema: DatabaseSchema_,\n convexDatabaseWriter: GenericDatabaseWriter<\n DataModel.ToConvex<DataModel.FromSchema<DatabaseSchema_>>\n >,\n): DatabaseWriterService<DatabaseSchema_> => {\n type DataModel_ = DataModel.FromSchema<DatabaseSchema_>;\n\n const table = <const TableName extends DataModel.TableNames<DataModel_>>(\n tableName: TableName,\n ) => {\n const tableDef = DatabaseSchema.tables(databaseSchema)[tableName];\n\n const insert = Effect.fn(\"DatabaseWriter.insert\")(function* (\n document: Document.WithoutSystemFields<\n DocumentByName_<DataModel_, TableName>\n >,\n ) {\n const encodedDocument = yield* Document.encode(document, tableDef);\n\n const id = yield* Effect.promise(() =>\n convexDatabaseWriter.insert(\n tableName,\n encodedDocument as WithoutSystemFields<\n DocumentByName<DataModel.ToConvex<DataModel_>, TableName>\n >,\n ),\n );\n\n // The database supplies the ID in this table's execution scope. Its\n // scope brand is erased at the Convex syscall boundary.\n return id as unknown as DataModel.Id<DataModel_, TableName>;\n });\n\n const patch = Effect.fn(\"DatabaseWriter.patch\")(function* (\n id: DataModel.Id<DataModel_, TableName>,\n patchedValues: PatchValue<\n Document.WithoutSystemFields<DocumentByName_<DataModel_, TableName>>\n >,\n ) {\n const originalDecodedDoc = yield* QueryInitializer.getById<\n Table.AnyWithProps,\n TableName\n >(\n convexDatabaseWriter as any,\n tableDef,\n )(id as unknown as GenericId<TableName>);\n\n const updatedEncodedDoc = yield* pipe(\n patchedValues,\n Record.reduce(originalDecodedDoc, (acc, value, key) =>\n value === undefined\n ? Record.remove(acc, key)\n : Record.set(acc, key, value),\n ),\n Document.encode(tableDef),\n );\n\n yield* Effect.promise(() =>\n convexDatabaseWriter.replace(\n id as unknown as GenericId<TableName>,\n updatedEncodedDoc as Expand<\n BetterOmit<\n DocumentByName<DataModel.ToConvex<DataModel_>, TableName>,\n \"_creationTime\" | \"_id\"\n >\n >,\n ),\n );\n });\n\n const replace = Effect.fn(\"DatabaseWriter.replace\")(function* (\n id: DataModel.Id<DataModel_, TableName>,\n value: Document.WithoutSystemFields<\n DocumentByName_<DataModel_, TableName>\n >,\n ) {\n const updatedEncodedDoc = yield* Document.encode(value, tableDef);\n\n yield* Effect.promise(() =>\n convexDatabaseWriter.replace(\n id as unknown as GenericId<TableName>,\n updatedEncodedDoc as Expand<\n BetterOmit<\n DocumentByName<DataModel.ToConvex<DataModel_>, TableName>,\n \"_creationTime\" | \"_id\"\n >\n >,\n ),\n );\n });\n\n const delete_ = (id: DataModel.Id<DataModel_, TableName>) =>\n Effect.promise(() =>\n convexDatabaseWriter.delete(id as unknown as GenericId<TableName>),\n );\n\n return {\n insert,\n patch,\n replace,\n delete: delete_,\n };\n };\n\n return {\n table,\n } as DatabaseWriterService<DatabaseSchema_>;\n};\n\nexport const DatabaseWriter = <\n DatabaseSchema_ extends DatabaseSchema.AnyWithProps,\n Docs = {},\n>(): DatabaseWriterTag<DatabaseSchema_, Docs> =>\n Context.Service<\n DatabaseWriterService<DatabaseSchema_>,\n DatabaseWriterService<DatabaseSchema_, Docs>\n >(\"@confect/server/DatabaseWriter\");\n\nexport type DatabaseWriter<\n DatabaseSchema_ extends DatabaseSchema.AnyWithProps,\n> = DatabaseWriterService<DatabaseSchema_>;\n\nexport const layer = <DatabaseSchema_ extends DatabaseSchema.AnyWithProps>(\n databaseSchema: DatabaseSchema_,\n convexDatabaseWriter: GenericDatabaseWriter<\n DataModel.ToConvex<DataModel.FromSchema<DatabaseSchema_>>\n >,\n) =>\n Layer.succeed(\n DatabaseWriter<DatabaseSchema_>(),\n make(databaseSchema, convexDatabaseWriter),\n );\n"],"mappings":";;;;;;;;;;;;;;;AAmGA,MAAa,QACX,gBACA,yBAG2C;CAG3C,MAAM,SACJ,cACG;EACH,MAAM,WAAWA,OAAsB,cAAc,CAAC,CAAC;EAEvD,MAAM,SAAS,OAAO,GAAG,uBAAuB,CAAC,CAAC,WAChD,UAGA;GACA,MAAM,kBAAkB,OAAOC,OAAgB,UAAU,QAAQ;GAajE,OAAO,OAXW,OAAO,cACvB,qBAAqB,OACnB,WACA,eAGF,CACF;EAKF,CAAC;EAED,MAAM,QAAQ,OAAO,GAAG,sBAAsB,CAAC,CAAC,WAC9C,IACA,eAGA;GACA,MAAM,qBAAqB,OAAOC,QAIhC,sBACA,QACF,CAAC,CAAC,EAAqC;GAEvC,MAAM,oBAAoB,OAAO,KAC/B,eACA,OAAO,OAAO,qBAAqB,KAAK,OAAO,QAC7C,UAAU,KAAA,IACN,OAAO,OAAO,KAAK,GAAG,IACtB,OAAO,IAAI,KAAK,KAAK,KAAK,CAChC,GACAD,OAAgB,QAAQ,CAC1B;GAEA,OAAO,OAAO,cACZ,qBAAqB,QACnB,IACA,iBAMF,CACF;EACF,CAAC;EAED,MAAM,UAAU,OAAO,GAAG,wBAAwB,CAAC,CAAC,WAClD,IACA,OAGA;GACA,MAAM,oBAAoB,OAAOA,OAAgB,OAAO,QAAQ;GAEhE,OAAO,OAAO,cACZ,qBAAqB,QACnB,IACA,iBAMF,CACF;EACF,CAAC;EAED,MAAM,WAAW,OACf,OAAO,cACL,qBAAqB,OAAO,EAAqC,CACnE;EAEF,OAAO;GACL;GACA;GACA;GACA,QAAQ;EACV;CACF;CAEA,OAAO,EACL,MACF;AACF;AAEA,MAAa,uBAIX,QAAQ,QAGN,gCAAgC;AAMpC,MAAa,SACX,gBACA,yBAIA,MAAM,QACJ,eAAgC,GAChC,KAAK,gBAAgB,oBAAoB,CAC3C"}
|
|
1
|
+
{"version":3,"file":"DatabaseWriter.js","names":["DatabaseSchema.tables","Document.encode","QueryInitializer.getById","DocumentPatch.encoder","QueryInitializer.GetByIdFailure","Document.decode","Document.DocumentEncodeError"],"sources":["../src/DatabaseWriter.ts"],"sourcesContent":["import type {\n BetterOmit,\n DocumentByName,\n Expand,\n GenericDatabaseWriter,\n WithoutSystemFields,\n} from \"convex/server\";\nimport type { GenericId } from \"convex/values\";\nimport { pipe } from \"effect/Function\";\nimport * as Context from \"effect/Context\";\nimport * as Effect from \"effect/Effect\";\nimport * as Layer from \"effect/Layer\";\nimport * as Predicate from \"effect/Predicate\";\nimport * as Record from \"effect/Record\";\nimport * as Result from \"effect/Result\";\nimport * as DatabaseSchema from \"./DatabaseSchema.js\";\nimport type * as DataModel from \"./DataModel.js\";\nimport type { DocumentByName as DocumentByName_ } from \"./DataModel.js\";\nimport * as Document from \"./Document.js\";\nimport * as DocumentPatch from \"./internal/DocumentPatch.js\";\nimport * as QueryInitializer from \"./QueryInitializer.js\";\nimport type * as Table from \"./Table.js\";\n\n/**\n * The argument accepted by `patch`: like `Partial<Doc>` without system fields,\n * but the fields that are already optional also accept `undefined`, since\n * setting a field to `undefined` unsets it.\n */\nexport type PatchValue<Doc> = {\n [\n K in keyof Doc as K extends \"_id\" | \"_creationTime\" ? never : K\n ]?: undefined extends Doc[K] ? Doc[K] | undefined : Doc[K];\n};\n\nexport interface DatabaseWriterTableAccessor<\n DataModel_ extends DataModel.AnyWithProps,\n TableName extends DataModel.TableNames<DataModel_>,\n Doc = DocumentByName_<DataModel_, TableName>,\n> {\n readonly insert: (\n document: Document.WithoutSystemFields<Doc>,\n ) => Effect.Effect<\n DataModel.Id<DataModel_, TableName>,\n Document.DocumentEncodeError\n >;\n readonly patch: (\n id: DataModel.Id<DataModel_, TableName>,\n patchedValues: PatchValue<Doc>,\n ) => Effect.Effect<\n void,\n | QueryInitializer.GetByIdFailure\n | Document.DocumentDecodeError\n | Document.DocumentEncodeError\n >;\n readonly replace: (\n id: DataModel.Id<DataModel_, TableName>,\n value: Document.WithoutSystemFields<Doc>,\n ) => Effect.Effect<void, Document.DocumentEncodeError>;\n readonly delete: (\n id: DataModel.Id<DataModel_, TableName>,\n ) => Effect.Effect<void>;\n}\n\n/**\n * The service shape backing the `DatabaseWriter` tag. Named (rather than an\n * inferred anonymous object) so declaration emit prints\n * `DatabaseWriterService<…>` by reference instead of expanding the data model.\n * `Docs` is the optional named document registry (see\n * `DatabaseReaderService`).\n */\nexport interface DatabaseWriterService<\n DatabaseSchema_ extends DatabaseSchema.AnyWithProps,\n Docs = {},\n> {\n readonly table: <\n const TableName extends DataModel.TableNames<\n DataModel.FromSchema<DatabaseSchema_>\n >,\n >(\n tableName: TableName,\n ) => DatabaseWriterTableAccessor<\n DataModel.FromSchema<DatabaseSchema_>,\n TableName,\n TableName extends keyof Docs\n ? Docs[TableName]\n : DocumentByName_<DataModel.FromSchema<DatabaseSchema_>, TableName>\n >;\n}\n\n/**\n * The tag's _Identifier_ is `Docs`-independent (see `DatabaseReaderTag`); only\n * the _Service_ carries `Docs` so writer inputs print the named doc\n * interfaces.\n */\nexport type DatabaseWriterTag<\n DatabaseSchema_ extends DatabaseSchema.AnyWithProps,\n Docs = {},\n> = Context.Service<\n DatabaseWriterService<DatabaseSchema_>,\n DatabaseWriterService<DatabaseSchema_, Docs>\n>;\n\nexport const make = <DatabaseSchema_ extends DatabaseSchema.AnyWithProps>(\n databaseSchema: DatabaseSchema_,\n convexDatabaseWriter: GenericDatabaseWriter<\n DataModel.ToConvex<DataModel.FromSchema<DatabaseSchema_>>\n >,\n): DatabaseWriterService<DatabaseSchema_> => {\n type DataModel_ = DataModel.FromSchema<DatabaseSchema_>;\n\n const table = <const TableName extends DataModel.TableNames<DataModel_>>(\n tableName: TableName,\n ) => {\n const tableDef = DatabaseSchema.tables(databaseSchema)[tableName];\n\n const insert = Effect.fn(\"DatabaseWriter.insert\")(function* (\n document: Document.WithoutSystemFields<\n DocumentByName_<DataModel_, TableName>\n >,\n ) {\n const encodedDocument = yield* Document.encode(document, tableDef);\n\n const id = yield* Effect.promise(() =>\n convexDatabaseWriter.insert(\n tableName,\n encodedDocument as WithoutSystemFields<\n DocumentByName<DataModel.ToConvex<DataModel_>, TableName>\n >,\n ),\n );\n\n // The database supplies the ID in this table's execution scope. Its\n // scope brand is erased at the Convex syscall boundary.\n return id as unknown as DataModel.Id<DataModel_, TableName>;\n });\n\n // A codec whose encoding spans fields cannot encode a patch on its own, so\n // the patch is merged into the decoded document and the result replaces it.\n const replacePatched = (\n id: DataModel.Id<DataModel_, TableName>,\n patchedValues: PatchValue<\n Document.WithoutSystemFields<DocumentByName_<DataModel_, TableName>>\n >,\n ) =>\n Effect.gen(function* () {\n const originalDecodedDoc = yield* QueryInitializer.getById<\n Table.AnyWithProps,\n TableName\n >(\n convexDatabaseWriter as any,\n tableDef,\n )(id as unknown as GenericId<TableName>);\n\n const updatedEncodedDoc = yield* pipe(\n patchedValues,\n Record.reduce(originalDecodedDoc, (acc, value, key) =>\n value === undefined\n ? Record.remove(acc, key)\n : Record.set(acc, key, value),\n ),\n Document.encode(tableDef),\n );\n\n yield* Effect.promise(() =>\n convexDatabaseWriter.replace(\n id as unknown as GenericId<TableName>,\n updatedEncodedDoc as Expand<\n BetterOmit<\n DocumentByName<DataModel.ToConvex<DataModel_>, TableName>,\n \"_creationTime\" | \"_id\"\n >\n >,\n ),\n );\n });\n\n const patch = Effect.fn(\"DatabaseWriter.patch\")(function* (\n id: DataModel.Id<DataModel_, TableName>,\n patchedValues: PatchValue<\n Document.WithoutSystemFields<DocumentByName_<DataModel_, TableName>>\n >,\n ) {\n const encodePatch = DocumentPatch.encoder(tableDef);\n if (encodePatch === undefined) {\n return yield* replacePatched(id, patchedValues);\n }\n\n const storedDoc = yield* pipe(\n Effect.promise(() =>\n convexDatabaseWriter.get(id as unknown as GenericId<TableName>),\n ),\n Effect.andThen((value) =>\n Effect.fromResult(\n Result.fromNullishOr(\n value,\n () =>\n new QueryInitializer.GetByIdFailure({\n tableName,\n id,\n }),\n ),\n ),\n ),\n );\n // The patch must encode and the patched document must decode. When\n // either fails, the stored document decides whose fault that is.\n const encodedPatch = yield* pipe(\n encodePatch(patchedValues),\n Effect.mapError(({ message }) => message),\n Effect.tap((delta) =>\n pipe(\n { ...storedDoc, ...delta },\n Record.filter(Predicate.isNotUndefined),\n Document.decode(tableDef),\n Effect.mapError(({ parseError }) => parseError),\n ),\n ),\n Effect.catch((parseError) =>\n pipe(\n Document.decode(storedDoc, tableDef),\n Effect.andThen(\n Effect.fail(\n new Document.DocumentEncodeError({\n tableName,\n id,\n parseError,\n }),\n ),\n ),\n ),\n ),\n );\n\n yield* Effect.promise(() =>\n convexDatabaseWriter.patch(\n id as unknown as GenericId<TableName>,\n encodedPatch as Partial<\n DocumentByName<DataModel.ToConvex<DataModel_>, TableName>\n >,\n ),\n );\n });\n\n const replace = Effect.fn(\"DatabaseWriter.replace\")(function* (\n id: DataModel.Id<DataModel_, TableName>,\n value: Document.WithoutSystemFields<\n DocumentByName_<DataModel_, TableName>\n >,\n ) {\n const updatedEncodedDoc = yield* Document.encode(value, tableDef);\n\n yield* Effect.promise(() =>\n convexDatabaseWriter.replace(\n id as unknown as GenericId<TableName>,\n updatedEncodedDoc as Expand<\n BetterOmit<\n DocumentByName<DataModel.ToConvex<DataModel_>, TableName>,\n \"_creationTime\" | \"_id\"\n >\n >,\n ),\n );\n });\n\n const delete_ = (id: DataModel.Id<DataModel_, TableName>) =>\n Effect.promise(() =>\n convexDatabaseWriter.delete(id as unknown as GenericId<TableName>),\n );\n\n return {\n insert,\n patch,\n replace,\n delete: delete_,\n };\n };\n\n return {\n table,\n } as DatabaseWriterService<DatabaseSchema_>;\n};\n\nexport const DatabaseWriter = <\n DatabaseSchema_ extends DatabaseSchema.AnyWithProps,\n Docs = {},\n>(): DatabaseWriterTag<DatabaseSchema_, Docs> =>\n Context.Service<\n DatabaseWriterService<DatabaseSchema_>,\n DatabaseWriterService<DatabaseSchema_, Docs>\n >(\"@confect/server/DatabaseWriter\");\n\nexport type DatabaseWriter<\n DatabaseSchema_ extends DatabaseSchema.AnyWithProps,\n> = DatabaseWriterService<DatabaseSchema_>;\n\nexport const layer = <DatabaseSchema_ extends DatabaseSchema.AnyWithProps>(\n databaseSchema: DatabaseSchema_,\n convexDatabaseWriter: GenericDatabaseWriter<\n DataModel.ToConvex<DataModel.FromSchema<DatabaseSchema_>>\n >,\n) =>\n Layer.succeed(\n DatabaseWriter<DatabaseSchema_>(),\n make(databaseSchema, convexDatabaseWriter),\n );\n"],"mappings":";;;;;;;;;;;;;;;;;;AAsGA,MAAa,QACX,gBACA,yBAG2C;CAG3C,MAAM,SACJ,cACG;EACH,MAAM,WAAWA,OAAsB,cAAc,CAAC,CAAC;EAEvD,MAAM,SAAS,OAAO,GAAG,uBAAuB,CAAC,CAAC,WAChD,UAGA;GACA,MAAM,kBAAkB,OAAOC,OAAgB,UAAU,QAAQ;GAajE,OAAO,OAXW,OAAO,cACvB,qBAAqB,OACnB,WACA,eAGF,CACF;EAKF,CAAC;EAID,MAAM,kBACJ,IACA,kBAIA,OAAO,IAAI,aAAa;GACtB,MAAM,qBAAqB,OAAOC,QAIhC,sBACA,QACF,CAAC,CAAC,EAAqC;GAEvC,MAAM,oBAAoB,OAAO,KAC/B,eACA,OAAO,OAAO,qBAAqB,KAAK,OAAO,QAC7C,UAAU,KAAA,IACN,OAAO,OAAO,KAAK,GAAG,IACtB,OAAO,IAAI,KAAK,KAAK,KAAK,CAChC,GACAD,OAAgB,QAAQ,CAC1B;GAEA,OAAO,OAAO,cACZ,qBAAqB,QACnB,IACA,iBAMF,CACF;EACF,CAAC;EAEH,MAAM,QAAQ,OAAO,GAAG,sBAAsB,CAAC,CAAC,WAC9C,IACA,eAGA;GACA,MAAM,cAAcE,QAAsB,QAAQ;GAClD,IAAI,gBAAgB,KAAA,GAClB,OAAO,OAAO,eAAe,IAAI,aAAa;GAGhD,MAAM,YAAY,OAAO,KACvB,OAAO,cACL,qBAAqB,IAAI,EAAqC,CAChE,GACA,OAAO,SAAS,UACd,OAAO,WACL,OAAO,cACL,aAEE,IAAIC,eAAgC;IAClC;IACA;GACF,CAAC,CACL,CACF,CACF,CACF;GAGA,MAAM,eAAe,OAAO,KAC1B,YAAY,aAAa,GACzB,OAAO,UAAU,EAAE,cAAc,OAAO,GACxC,OAAO,KAAK,UACV,KACE;IAAE,GAAG;IAAW,GAAG;GAAM,GACzB,OAAO,OAAO,UAAU,cAAc,GACtCC,OAAgB,QAAQ,GACxB,OAAO,UAAU,EAAE,iBAAiB,UAAU,CAChD,CACF,GACA,OAAO,OAAO,eACZ,KACEA,OAAgB,WAAW,QAAQ,GACnC,OAAO,QACL,OAAO,KACL,IAAIC,oBAA6B;IAC/B;IACA;IACA;GACF,CAAC,CACH,CACF,CACF,CACF,CACF;GAEA,OAAO,OAAO,cACZ,qBAAqB,MACnB,IACA,YAGF,CACF;EACF,CAAC;EAED,MAAM,UAAU,OAAO,GAAG,wBAAwB,CAAC,CAAC,WAClD,IACA,OAGA;GACA,MAAM,oBAAoB,OAAOL,OAAgB,OAAO,QAAQ;GAEhE,OAAO,OAAO,cACZ,qBAAqB,QACnB,IACA,iBAMF,CACF;EACF,CAAC;EAED,MAAM,WAAW,OACf,OAAO,cACL,qBAAqB,OAAO,EAAqC,CACnE;EAEF,OAAO;GACL;GACA;GACA;GACA,QAAQ;EACV;CACF;CAEA,OAAO,EACL,MACF;AACF;AAEA,MAAa,uBAIX,QAAQ,QAGN,gCAAgC;AAMpC,MAAa,SACX,gBACA,yBAIA,MAAM,QACJ,eAAgC,GAChC,KAAK,gBAAgB,oBAAoB,CAC3C"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReadonlyValue } from "@confect/core/SchemaToValidator";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Record from "effect/Record";
|
|
4
|
+
import * as Schema from "effect/Schema";
|
|
5
|
+
import type * as Table from "../Table.js";
|
|
6
|
+
export type EncodedPatch = Record.ReadonlyRecord<string, ReadonlyValue | undefined>;
|
|
7
|
+
export type PatchEncoder = (patch: Record.ReadonlyRecord<string, unknown>) => Effect.Effect<EncodedPatch, Schema.SchemaError>;
|
|
8
|
+
/**
|
|
9
|
+
* Returns the encoder for patches to the table's documents, or `undefined` when
|
|
10
|
+
* the table's codec does not encode each field independently. The encoder maps
|
|
11
|
+
* every patched field of the table to its encoded value, or to `undefined` when
|
|
12
|
+
* the patch unsets the field or the field's codec encodes the value as an
|
|
13
|
+
* absent key. Keys outside the table's fields are left out.
|
|
14
|
+
*/
|
|
15
|
+
export declare const encoder: (table: Table.AnyWithProps) => PatchEncoder | undefined;
|
|
16
|
+
//# sourceMappingURL=DocumentPatch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DocumentPatch.d.ts","sourceRoot":"","sources":["../../src/internal/DocumentPatch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAErE,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,KAAK,KAAK,MAAM,aAAa,CAAC;AAE1C,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,cAAc,CAC9C,MAAM,EACN,aAAa,GAAG,SAAS,CAC1B,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,CACzB,KAAK,EAAE,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,KAC1C,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;AAIrD;;;;;;GAMG;AACH,eAAO,MAAM,OAAO,UACX,KAAK,CAAC,YAAY,KACxB,YAAY,GAAG,SAOjB,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import { pipe } from "effect/Function";
|
|
4
|
+
import * as SchemaAST from "effect/SchemaAST";
|
|
5
|
+
import * as Record from "effect/Record";
|
|
6
|
+
//#region src/internal/DocumentPatch.ts
|
|
7
|
+
const patchEncoders = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
/**
|
|
9
|
+
* Returns the encoder for patches to the table's documents, or `undefined` when
|
|
10
|
+
* the table's codec does not encode each field independently. The encoder maps
|
|
11
|
+
* every patched field of the table to its encoded value, or to `undefined` when
|
|
12
|
+
* the patch unsets the field or the field's codec encodes the value as an
|
|
13
|
+
* absent key. Keys outside the table's fields are left out.
|
|
14
|
+
*/
|
|
15
|
+
const encoder = (table) => {
|
|
16
|
+
let cached = patchEncoders.get(table);
|
|
17
|
+
if (cached === void 0) {
|
|
18
|
+
cached = makePatchEncoder(table);
|
|
19
|
+
patchEncoders.set(table, cached);
|
|
20
|
+
}
|
|
21
|
+
return cached ?? void 0;
|
|
22
|
+
};
|
|
23
|
+
const makePatchEncoder = (table) => {
|
|
24
|
+
const ast = table.Fields.ast;
|
|
25
|
+
if (!SchemaAST.isObjects(ast) || ast.encoding !== void 0 || ast.checks !== void 0 || ast.indexSignatures.length > 0) return null;
|
|
26
|
+
const fieldEncoders = Object.fromEntries(ast.propertySignatures.map(({ name, type }) => {
|
|
27
|
+
const key = String(name);
|
|
28
|
+
const encode = Schema.encodeUnknownEffect(Schema.Struct({ [key]: Schema.optionalKey(Schema.make(type)) }));
|
|
29
|
+
return [key, (value) => Effect.map(encode({ [key]: value }), (encoded) => encoded[key])];
|
|
30
|
+
}));
|
|
31
|
+
return (patch) => pipe(fieldEncoders, Record.filter((_, name) => Object.hasOwn(patch, name)), Record.map((encode, name) => patch[name] === void 0 ? Effect.undefined : encode(patch[name])), Effect.all);
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { encoder };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=DocumentPatch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DocumentPatch.js","names":[],"sources":["../../src/internal/DocumentPatch.ts"],"sourcesContent":["import type { ReadonlyValue } from \"@confect/core/SchemaToValidator\";\nimport { pipe } from \"effect/Function\";\nimport * as Effect from \"effect/Effect\";\nimport * as Record from \"effect/Record\";\nimport * as Schema from \"effect/Schema\";\nimport * as SchemaAST from \"effect/SchemaAST\";\nimport type * as Table from \"../Table.js\";\n\nexport type EncodedPatch = Record.ReadonlyRecord<\n string,\n ReadonlyValue | undefined\n>;\n\nexport type PatchEncoder = (\n patch: Record.ReadonlyRecord<string, unknown>,\n) => Effect.Effect<EncodedPatch, Schema.SchemaError>;\n\nconst patchEncoders = new WeakMap<Table.AnyWithProps, PatchEncoder | null>();\n\n/**\n * Returns the encoder for patches to the table's documents, or `undefined` when\n * the table's codec does not encode each field independently. The encoder maps\n * every patched field of the table to its encoded value, or to `undefined` when\n * the patch unsets the field or the field's codec encodes the value as an\n * absent key. Keys outside the table's fields are left out.\n */\nexport const encoder = (\n table: Table.AnyWithProps,\n): PatchEncoder | undefined => {\n let cached = patchEncoders.get(table);\n if (cached === undefined) {\n cached = makePatchEncoder(table);\n patchEncoders.set(table, cached);\n }\n return cached ?? undefined;\n};\n\n// A struct with no encoding of its own encodes each field through that field's\n// codec alone, so a patch encodes field by field. A union or a transformation\n// over the whole struct can make one field's encoding depend on another field.\n// A struct-level check must see the decoded fields before they are encoded,\n// and keys under an index signature have no field codec of their own.\nconst makePatchEncoder = (table: Table.AnyWithProps): PatchEncoder | null => {\n const ast = table.Fields.ast;\n if (\n !SchemaAST.isObjects(ast) ||\n ast.encoding !== undefined ||\n ast.checks !== undefined ||\n ast.indexSignatures.length > 0\n ) {\n return null;\n }\n // A field's codec runs only when the patch gives the field a value, since a\n // codec can handle a missing key. Each comes from the table, whose codecs\n // need no services.\n const fieldEncoders: Record.ReadonlyRecord<\n string,\n (\n value: unknown,\n ) => Effect.Effect<ReadonlyValue | undefined, Schema.SchemaError>\n > = Object.fromEntries(\n ast.propertySignatures.map(({ name, type }) => {\n const key = String(name);\n const encode = Schema.encodeUnknownEffect(\n Schema.Struct({\n [key]: Schema.optionalKey(\n Schema.make<Schema.Codec<unknown, ReadonlyValue>>(type),\n ),\n }),\n );\n return [\n key,\n (value: unknown) =>\n Effect.map(encode({ [key]: value }), (encoded) => encoded[key]),\n ];\n }),\n );\n return (patch) =>\n pipe(\n fieldEncoders,\n Record.filter((_, name) => Object.hasOwn(patch, name)),\n Record.map((encode, name) =>\n patch[name] === undefined ? Effect.undefined : encode(patch[name]),\n ),\n Effect.all,\n );\n};\n"],"mappings":";;;;;;AAiBA,MAAM,gCAAgB,IAAI,QAAiD;;;;;;;;AAS3E,MAAa,WACX,UAC6B;CAC7B,IAAI,SAAS,cAAc,IAAI,KAAK;CACpC,IAAI,WAAW,KAAA,GAAW;EACxB,SAAS,iBAAiB,KAAK;EAC/B,cAAc,IAAI,OAAO,MAAM;CACjC;CACA,OAAO,UAAU,KAAA;AACnB;AAOA,MAAM,oBAAoB,UAAmD;CAC3E,MAAM,MAAM,MAAM,OAAO;CACzB,IACE,CAAC,UAAU,UAAU,GAAG,KACxB,IAAI,aAAa,KAAA,KACjB,IAAI,WAAW,KAAA,KACf,IAAI,gBAAgB,SAAS,GAE7B,OAAO;CAKT,MAAM,gBAKF,OAAO,YACT,IAAI,mBAAmB,KAAK,EAAE,MAAM,WAAW;EAC7C,MAAM,MAAM,OAAO,IAAI;EACvB,MAAM,SAAS,OAAO,oBACpB,OAAO,OAAO,GACX,MAAM,OAAO,YACZ,OAAO,KAA2C,IAAI,CACxD,EACF,CAAC,CACH;EACA,OAAO,CACL,MACC,UACC,OAAO,IAAI,OAAO,GAAG,MAAM,MAAM,CAAC,IAAI,YAAY,QAAQ,IAAI,CAClE;CACF,CAAC,CACH;CACA,QAAQ,UACN,KACE,eACA,OAAO,QAAQ,GAAG,SAAS,OAAO,OAAO,OAAO,IAAI,CAAC,GACrD,OAAO,KAAK,QAAQ,SAClB,MAAM,UAAU,KAAA,IAAY,OAAO,YAAY,OAAO,MAAM,KAAK,CACnE,GACA,OAAO,GACT;AACJ"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaganref/confect-server",
|
|
3
|
-
"version": "10.3.1-fork.
|
|
3
|
+
"version": "10.3.1-fork.14",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"@effect/ai-openai-compat": "4.0.4",
|
|
6
6
|
"@effect/ai-openrouter": "4.0.4"
|
|
7
7
|
},
|
|
8
8
|
"peerDependencies": {
|
|
9
|
-
"@confect/core": "npm:@gaganref/confect-core@10.3.1-fork.
|
|
9
|
+
"@confect/core": "npm:@gaganref/confect-core@10.3.1-fork.14",
|
|
10
10
|
"@effect/platform-node": "^4.0.0",
|
|
11
11
|
"convex": "^1.45.0",
|
|
12
12
|
"effect": "^4.0.4"
|
package/src/DatabaseWriter.ts
CHANGED
|
@@ -10,11 +10,14 @@ import { pipe } from "effect/Function";
|
|
|
10
10
|
import * as Context from "effect/Context";
|
|
11
11
|
import * as Effect from "effect/Effect";
|
|
12
12
|
import * as Layer from "effect/Layer";
|
|
13
|
+
import * as Predicate from "effect/Predicate";
|
|
13
14
|
import * as Record from "effect/Record";
|
|
15
|
+
import * as Result from "effect/Result";
|
|
14
16
|
import * as DatabaseSchema from "./DatabaseSchema.js";
|
|
15
17
|
import type * as DataModel from "./DataModel.js";
|
|
16
18
|
import type { DocumentByName as DocumentByName_ } from "./DataModel.js";
|
|
17
19
|
import * as Document from "./Document.js";
|
|
20
|
+
import * as DocumentPatch from "./internal/DocumentPatch.js";
|
|
18
21
|
import * as QueryInitializer from "./QueryInitializer.js";
|
|
19
22
|
import type * as Table from "./Table.js";
|
|
20
23
|
|
|
@@ -131,38 +134,108 @@ export const make = <DatabaseSchema_ extends DatabaseSchema.AnyWithProps>(
|
|
|
131
134
|
return id as unknown as DataModel.Id<DataModel_, TableName>;
|
|
132
135
|
});
|
|
133
136
|
|
|
137
|
+
// A codec whose encoding spans fields cannot encode a patch on its own, so
|
|
138
|
+
// the patch is merged into the decoded document and the result replaces it.
|
|
139
|
+
const replacePatched = (
|
|
140
|
+
id: DataModel.Id<DataModel_, TableName>,
|
|
141
|
+
patchedValues: PatchValue<
|
|
142
|
+
Document.WithoutSystemFields<DocumentByName_<DataModel_, TableName>>
|
|
143
|
+
>,
|
|
144
|
+
) =>
|
|
145
|
+
Effect.gen(function* () {
|
|
146
|
+
const originalDecodedDoc = yield* QueryInitializer.getById<
|
|
147
|
+
Table.AnyWithProps,
|
|
148
|
+
TableName
|
|
149
|
+
>(
|
|
150
|
+
convexDatabaseWriter as any,
|
|
151
|
+
tableDef,
|
|
152
|
+
)(id as unknown as GenericId<TableName>);
|
|
153
|
+
|
|
154
|
+
const updatedEncodedDoc = yield* pipe(
|
|
155
|
+
patchedValues,
|
|
156
|
+
Record.reduce(originalDecodedDoc, (acc, value, key) =>
|
|
157
|
+
value === undefined
|
|
158
|
+
? Record.remove(acc, key)
|
|
159
|
+
: Record.set(acc, key, value),
|
|
160
|
+
),
|
|
161
|
+
Document.encode(tableDef),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
yield* Effect.promise(() =>
|
|
165
|
+
convexDatabaseWriter.replace(
|
|
166
|
+
id as unknown as GenericId<TableName>,
|
|
167
|
+
updatedEncodedDoc as Expand<
|
|
168
|
+
BetterOmit<
|
|
169
|
+
DocumentByName<DataModel.ToConvex<DataModel_>, TableName>,
|
|
170
|
+
"_creationTime" | "_id"
|
|
171
|
+
>
|
|
172
|
+
>,
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
134
177
|
const patch = Effect.fn("DatabaseWriter.patch")(function* (
|
|
135
178
|
id: DataModel.Id<DataModel_, TableName>,
|
|
136
179
|
patchedValues: PatchValue<
|
|
137
180
|
Document.WithoutSystemFields<DocumentByName_<DataModel_, TableName>>
|
|
138
181
|
>,
|
|
139
182
|
) {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
183
|
+
const encodePatch = DocumentPatch.encoder(tableDef);
|
|
184
|
+
if (encodePatch === undefined) {
|
|
185
|
+
return yield* replacePatched(id, patchedValues);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const storedDoc = yield* pipe(
|
|
189
|
+
Effect.promise(() =>
|
|
190
|
+
convexDatabaseWriter.get(id as unknown as GenericId<TableName>),
|
|
191
|
+
),
|
|
192
|
+
Effect.andThen((value) =>
|
|
193
|
+
Effect.fromResult(
|
|
194
|
+
Result.fromNullishOr(
|
|
195
|
+
value,
|
|
196
|
+
() =>
|
|
197
|
+
new QueryInitializer.GetByIdFailure({
|
|
198
|
+
tableName,
|
|
199
|
+
id,
|
|
200
|
+
}),
|
|
201
|
+
),
|
|
202
|
+
),
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
// The patch must encode and the patched document must decode. When
|
|
206
|
+
// either fails, the stored document decides whose fault that is.
|
|
207
|
+
const encodedPatch = yield* pipe(
|
|
208
|
+
encodePatch(patchedValues),
|
|
209
|
+
Effect.mapError(({ message }) => message),
|
|
210
|
+
Effect.tap((delta) =>
|
|
211
|
+
pipe(
|
|
212
|
+
{ ...storedDoc, ...delta },
|
|
213
|
+
Record.filter(Predicate.isNotUndefined),
|
|
214
|
+
Document.decode(tableDef),
|
|
215
|
+
Effect.mapError(({ parseError }) => parseError),
|
|
216
|
+
),
|
|
217
|
+
),
|
|
218
|
+
Effect.catch((parseError) =>
|
|
219
|
+
pipe(
|
|
220
|
+
Document.decode(storedDoc, tableDef),
|
|
221
|
+
Effect.andThen(
|
|
222
|
+
Effect.fail(
|
|
223
|
+
new Document.DocumentEncodeError({
|
|
224
|
+
tableName,
|
|
225
|
+
id,
|
|
226
|
+
parseError,
|
|
227
|
+
}),
|
|
228
|
+
),
|
|
229
|
+
),
|
|
230
|
+
),
|
|
154
231
|
),
|
|
155
|
-
Document.encode(tableDef),
|
|
156
232
|
);
|
|
157
233
|
|
|
158
234
|
yield* Effect.promise(() =>
|
|
159
|
-
convexDatabaseWriter.
|
|
235
|
+
convexDatabaseWriter.patch(
|
|
160
236
|
id as unknown as GenericId<TableName>,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
DocumentByName<DataModel.ToConvex<DataModel_>, TableName>,
|
|
164
|
-
"_creationTime" | "_id"
|
|
165
|
-
>
|
|
237
|
+
encodedPatch as Partial<
|
|
238
|
+
DocumentByName<DataModel.ToConvex<DataModel_>, TableName>
|
|
166
239
|
>,
|
|
167
240
|
),
|
|
168
241
|
);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ReadonlyValue } from "@confect/core/SchemaToValidator";
|
|
2
|
+
import { pipe } from "effect/Function";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Record from "effect/Record";
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
6
|
+
import * as SchemaAST from "effect/SchemaAST";
|
|
7
|
+
import type * as Table from "../Table.js";
|
|
8
|
+
|
|
9
|
+
export type EncodedPatch = Record.ReadonlyRecord<
|
|
10
|
+
string,
|
|
11
|
+
ReadonlyValue | undefined
|
|
12
|
+
>;
|
|
13
|
+
|
|
14
|
+
export type PatchEncoder = (
|
|
15
|
+
patch: Record.ReadonlyRecord<string, unknown>,
|
|
16
|
+
) => Effect.Effect<EncodedPatch, Schema.SchemaError>;
|
|
17
|
+
|
|
18
|
+
const patchEncoders = new WeakMap<Table.AnyWithProps, PatchEncoder | null>();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns the encoder for patches to the table's documents, or `undefined` when
|
|
22
|
+
* the table's codec does not encode each field independently. The encoder maps
|
|
23
|
+
* every patched field of the table to its encoded value, or to `undefined` when
|
|
24
|
+
* the patch unsets the field or the field's codec encodes the value as an
|
|
25
|
+
* absent key. Keys outside the table's fields are left out.
|
|
26
|
+
*/
|
|
27
|
+
export const encoder = (
|
|
28
|
+
table: Table.AnyWithProps,
|
|
29
|
+
): PatchEncoder | undefined => {
|
|
30
|
+
let cached = patchEncoders.get(table);
|
|
31
|
+
if (cached === undefined) {
|
|
32
|
+
cached = makePatchEncoder(table);
|
|
33
|
+
patchEncoders.set(table, cached);
|
|
34
|
+
}
|
|
35
|
+
return cached ?? undefined;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// A struct with no encoding of its own encodes each field through that field's
|
|
39
|
+
// codec alone, so a patch encodes field by field. A union or a transformation
|
|
40
|
+
// over the whole struct can make one field's encoding depend on another field.
|
|
41
|
+
// A struct-level check must see the decoded fields before they are encoded,
|
|
42
|
+
// and keys under an index signature have no field codec of their own.
|
|
43
|
+
const makePatchEncoder = (table: Table.AnyWithProps): PatchEncoder | null => {
|
|
44
|
+
const ast = table.Fields.ast;
|
|
45
|
+
if (
|
|
46
|
+
!SchemaAST.isObjects(ast) ||
|
|
47
|
+
ast.encoding !== undefined ||
|
|
48
|
+
ast.checks !== undefined ||
|
|
49
|
+
ast.indexSignatures.length > 0
|
|
50
|
+
) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
// A field's codec runs only when the patch gives the field a value, since a
|
|
54
|
+
// codec can handle a missing key. Each comes from the table, whose codecs
|
|
55
|
+
// need no services.
|
|
56
|
+
const fieldEncoders: Record.ReadonlyRecord<
|
|
57
|
+
string,
|
|
58
|
+
(
|
|
59
|
+
value: unknown,
|
|
60
|
+
) => Effect.Effect<ReadonlyValue | undefined, Schema.SchemaError>
|
|
61
|
+
> = Object.fromEntries(
|
|
62
|
+
ast.propertySignatures.map(({ name, type }) => {
|
|
63
|
+
const key = String(name);
|
|
64
|
+
const encode = Schema.encodeUnknownEffect(
|
|
65
|
+
Schema.Struct({
|
|
66
|
+
[key]: Schema.optionalKey(
|
|
67
|
+
Schema.make<Schema.Codec<unknown, ReadonlyValue>>(type),
|
|
68
|
+
),
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
return [
|
|
72
|
+
key,
|
|
73
|
+
(value: unknown) =>
|
|
74
|
+
Effect.map(encode({ [key]: value }), (encoded) => encoded[key]),
|
|
75
|
+
];
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
return (patch) =>
|
|
79
|
+
pipe(
|
|
80
|
+
fieldEncoders,
|
|
81
|
+
Record.filter((_, name) => Object.hasOwn(patch, name)),
|
|
82
|
+
Record.map((encode, name) =>
|
|
83
|
+
patch[name] === undefined ? Effect.undefined : encode(patch[name]),
|
|
84
|
+
),
|
|
85
|
+
Effect.all,
|
|
86
|
+
);
|
|
87
|
+
};
|