@shivaedev/sql 0.46.3 → 0.47.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 +18 -0
- package/README.md +61 -27
- package/dist/backfillPostgres.d.ts +1 -1
- package/dist/backfillPostgres.d.ts.map +1 -1
- package/dist/backfillPostgres.js +1 -1
- package/dist/backfillPostgres.js.map +1 -1
- package/dist/batchedDelete.d.ts +1 -1
- package/dist/compositeRepository.d.ts +3 -1
- package/dist/compositeRepository.d.ts.map +1 -1
- package/dist/compositeRepository.js +2 -2
- package/dist/compositeRepository.js.map +1 -1
- package/dist/databaseAddress.d.ts.map +1 -1
- package/dist/databaseAddress.js +34 -18
- package/dist/databaseAddress.js.map +1 -1
- package/dist/deleteGroupsBatched.d.ts +1 -1
- package/dist/deleteGroupsBatched.d.ts.map +1 -1
- package/dist/deleteGroupsBatched.js +1 -1
- package/dist/deleteGroupsBatched.js.map +1 -1
- package/dist/makeAggregate.d.ts +2 -0
- package/dist/makeAggregate.d.ts.map +1 -1
- package/dist/makeAggregate.js +1 -0
- package/dist/makeAggregate.js.map +1 -1
- package/dist/migrations.js +1 -1
- package/dist/migrations.js.map +1 -1
- package/dist/orderedChildStatements.d.ts +1 -1
- package/dist/postgresChanges.js +1 -1
- package/dist/postgresChanges.js.map +1 -1
- package/dist/readSql.d.ts +19 -0
- package/dist/readSql.d.ts.map +1 -0
- package/dist/readSql.js +37 -0
- package/dist/readSql.js.map +1 -0
- package/dist/recordRepositoryRead.d.ts +12 -0
- package/dist/recordRepositoryRead.d.ts.map +1 -0
- package/dist/recordRepositoryRead.js +102 -0
- package/dist/recordRepositoryRead.js.map +1 -0
- package/dist/repository.d.ts.map +1 -1
- package/dist/repository.js +6 -5
- package/dist/repository.js.map +1 -1
- package/dist/repositoryChanges.d.ts +2 -2
- package/dist/repositoryChanges.d.ts.map +1 -1
- package/dist/repositoryChanges.js +30 -14
- package/dist/repositoryChanges.js.map +1 -1
- package/dist/repositoryCollection.d.ts +2 -0
- package/dist/repositoryCollection.d.ts.map +1 -1
- package/dist/repositoryCollection.js +14 -1
- package/dist/repositoryCollection.js.map +1 -1
- package/dist/repositoryQueries.d.ts +2 -0
- package/dist/repositoryQueries.d.ts.map +1 -1
- package/dist/repositoryQueries.js +3 -0
- package/dist/repositoryQueries.js.map +1 -1
- package/dist/repositoryWrites.d.ts +1 -1
- package/dist/transact.d.ts +5 -2
- package/dist/transact.d.ts.map +1 -1
- package/dist/transact.js +7 -3
- package/dist/transact.js.map +1 -1
- package/package.json +16 -16
- package/src/backfillPostgres.ts +1 -1
- package/src/compositeRepository.ts +9 -2
- package/src/databaseAddress.ts +36 -18
- package/src/deleteGroupsBatched.ts +0 -1
- package/src/makeAggregate.ts +4 -2
- package/src/migrations.ts +1 -1
- package/src/postgresChanges.ts +1 -1
- package/src/readSql.ts +51 -0
- package/src/recordRepositoryRead.ts +126 -0
- package/src/repository.ts +6 -5
- package/src/repositoryChanges.ts +42 -15
- package/src/repositoryCollection.ts +18 -1
- package/src/repositoryQueries.ts +5 -0
- package/src/transact.ts +14 -15
package/dist/transact.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transact.d.ts","sourceRoot":"","sources":["../src/transact.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAc,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,KAAK,YAAY,IAAI,UAAU,EAAe,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAAoB,KAAK,QAAQ,EAAc,MAAM,+BAA+B,CAAC;AAC5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAG7D,MAAM,MAAM,gBAAgB,GAAG,SAAS,OAAO,EAAE,GAAG,cAAc,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC,CAAC;AAE/F,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC1C;AAED,KAAK,QAAQ,GAAG,SAAS,CAAC,SAAS,GAAG,UAAU,CAAC,UAAU,CAAC;AAE5D,eAAO,MAAM,cAAc,sCAA4G,CAAC;AACxI,eAAO,MAAM,YAAY,4DAEvB,CAAC;AACH,eAAO,MAAM,aAAa,2DAExB,CAAC;AA8CH,eAAO,MAAM,kBAAkB,6GAI7B,CAAC;AAEH,MAAM,WAAW,eAAe,CAAC,MAAM;IACtC,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,MAAM,CAAC;CACjD;AAED,eAAO,MAAM,QAAQ,EAAE;IACtB,CAAC,MAAM,
|
|
1
|
+
{"version":3,"file":"transact.d.ts","sourceRoot":"","sources":["../src/transact.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAc,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,KAAK,YAAY,IAAI,UAAU,EAAe,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAAoB,KAAK,QAAQ,EAAc,MAAM,+BAA+B,CAAC;AAC5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAG7D,MAAM,MAAM,gBAAgB,GAAG,SAAS,OAAO,EAAE,GAAG,cAAc,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC,CAAC;AAE/F,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC1C;AAED,KAAK,QAAQ,GAAG,SAAS,CAAC,SAAS,GAAG,UAAU,CAAC,UAAU,CAAC;AAE5D,eAAO,MAAM,cAAc,sCAA4G,CAAC;AACxI,eAAO,MAAM,YAAY,4DAEvB,CAAC;AACH,eAAO,MAAM,aAAa,2DAExB,CAAC;AA8CH,eAAO,MAAM,kBAAkB,6GAI7B,CAAC;AAEH,MAAM,WAAW,eAAe,CAAC,MAAM;IACtC,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,MAAM,CAAC;CACjD;AAED,KAAK,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,SAAS,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;AAEhG,eAAO,MAAM,QAAQ,EAAE;IACtB,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,UAAU,CAAC,CAAC,EAAE,CAAC,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAC;IAChF,CAAC,MAAM,EAAE,OAAO,EAAE,eAAe,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,UAAU,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,MAAM,EAAE,CAAC,CAAC,CAAC;IACzI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,EAAE,CAAC,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAC;IAC1E,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,eAAe,CAAC,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,MAAM,EAAE,CAAC,CAAC,CAAC;CAkBrI,CAAC"}
|
package/dist/transact.js
CHANGED
|
@@ -56,13 +56,17 @@ export const invalidateOnCommit = Effect.fn("Transact.invalidateOnCommit")(funct
|
|
|
56
56
|
.record(flatten(keys).map((key) => ({ key, publication: options?.publication ?? "shared" })))
|
|
57
57
|
.pipe(Effect.provideService(invalidations.Observer, observe));
|
|
58
58
|
});
|
|
59
|
-
export const transact = dual(
|
|
59
|
+
export const transact = dual((args) => Effect.isEffect(args[0]), Effect.fn("Transact.transact")(function* (effect, options) {
|
|
60
60
|
const sql = yield* SqlClient.SqlClient;
|
|
61
61
|
const native = yield* Effect.serviceOption(sql.transactionService);
|
|
62
|
-
|
|
62
|
+
const transacted = invalidations
|
|
63
63
|
.within((body, boundary) => native._tag === "Some"
|
|
64
64
|
? sql.withTransaction(body)
|
|
65
65
|
: boundary.restore(sql.withTransaction(Effect.tap(body, () => sql.afterCommit(boundary.committed)))))(effect)
|
|
66
|
-
.pipe(Effect.provideService(invalidations.Observer, observe), Effect.provideService(invalidations.CommittedSink, committed)
|
|
66
|
+
.pipe(Effect.provideService(invalidations.Observer, observe), Effect.provideService(invalidations.CommittedSink, committed));
|
|
67
|
+
if (options === undefined) {
|
|
68
|
+
return yield* transacted;
|
|
69
|
+
}
|
|
70
|
+
return yield* Effect.catchIf(transacted, isSqlError, (error) => Effect.fail(options.onSqlError(error)));
|
|
67
71
|
}));
|
|
68
72
|
//# sourceMappingURL=transact.js.map
|
package/dist/transact.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transact.js","sourceRoot":"","sources":["../src/transact.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAEvC,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,UAAU,EAAiB,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAmC,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAAmC,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAE5F,OAAO,EAAE,OAAO,EAAqB,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAU1E,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC,SAAS,CAAoB,+BAA+B,EAAE,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,UAAU,EAAE,CAAC,CAAC;AACxI,MAAM,CAAC,MAAM,YAAY,GAAG,OAAO,CAAC,SAAS,CAA0C,6BAA6B,EAAE;IACrH,YAAY,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI;CACrC,CAAC,CAAC;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,OAAO,CAAC,SAAS,CAAyC,8BAA8B,EAAE;IACtH,YAAY,EAAE,GAAG,EAAE,CAAC,SAAS;CAC7B,CAAC,CAAC;AAEH,SAAS,OAAO,CAAC,KAAgC;IAChD,OAAO,MAAM,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;AAC7G,CAAC;AAED,SAAS,UAAU,CAAC,IAAwB;IAC3C,OAAO,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACxG,CAAC;AAED,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,OAAgC;IAC9E,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,aAAa,CAAC;IAClC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,KAAK,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IAC3C,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC;IACnF,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,KAAK,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAChH,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,KAAK,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACrB,CAAC;AACF,CAAC,CAAC,CAAC;AAEH,MAAM,aAAa,GAAG,WAAW,CAAmC;IACnE,YAAY,EAAE,CAAC,OAAO,EAAE,EAAE;QACzB,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC;QACnF,OAAO,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IACjG,CAAC;IACD,GAAG,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC;IACxE,IAAI,EAAE,yBAAyB;IAC/B,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,kBAAkB,CAAC;IACvE,OAAO,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACjD,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAC5B,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC;QACvC,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QACnE,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC5B,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,GAAG,CACvB,IAAI,KAAK,CAAC,iHAAiH,CAAC,CAC5H,CAAC;QACH,CAAC;IACF,CAAC,CAAC;CACF,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC,EAAE,CAAC,6BAA6B,CAAC,CAAC,QAAQ,CAAC,EAAE,IAAsB,EAAE,OAA6B;IAC1I,KAAK,CAAC,CAAC,aAAa;SAClB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,IAAI,QAAQ,EAAE,CAAC,CAAC,CAAC;SAC5F,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;AAChE,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"transact.js","sourceRoot":"","sources":["../src/transact.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/sql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"description": "Schema-derived repositories over native Effect SQL",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect"
|
|
@@ -43,27 +43,27 @@
|
|
|
43
43
|
"README.md"
|
|
44
44
|
],
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@shivaedev/changes": "0.
|
|
47
|
-
"@shivaedev/service": "0.
|
|
46
|
+
"@shivaedev/changes": "0.12.0",
|
|
47
|
+
"@shivaedev/service": "0.9.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@effect/platform-node": "4.1.
|
|
51
|
-
"@effect/sql-pg": "4.1.
|
|
52
|
-
"@effect/sql-sqlite-node": "4.1.
|
|
53
|
-
"@shivaedev/contract": "0.
|
|
54
|
-
"@shivaedev/jobs": "0.
|
|
55
|
-
"@shivaedev/quality": "0.
|
|
56
|
-
"@shivaedev/test-kits": "0.
|
|
57
|
-
"@shivaedev/test-sql": "0.24.
|
|
58
|
-
"@shivaedev/test-story": "0.
|
|
50
|
+
"@effect/platform-node": "4.1.2",
|
|
51
|
+
"@effect/sql-pg": "4.1.2",
|
|
52
|
+
"@effect/sql-sqlite-node": "4.1.2",
|
|
53
|
+
"@shivaedev/contract": "0.44.0",
|
|
54
|
+
"@shivaedev/jobs": "0.42.2",
|
|
55
|
+
"@shivaedev/quality": "0.77.0",
|
|
56
|
+
"@shivaedev/test-kits": "0.12.0",
|
|
57
|
+
"@shivaedev/test-sql": "0.24.9",
|
|
58
|
+
"@shivaedev/test-story": "0.47.0",
|
|
59
59
|
"@types/node": "24.19.2",
|
|
60
|
-
"effect": "4.1.
|
|
60
|
+
"effect": "4.1.2",
|
|
61
61
|
"typescript": "7.0.2",
|
|
62
|
-
"vitest": "5.0
|
|
62
|
+
"vitest": "5.1.0"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
|
-
"@effect/sql-pg": "4.1.
|
|
66
|
-
"effect": "4.1.
|
|
65
|
+
"@effect/sql-pg": "4.1.2",
|
|
66
|
+
"effect": "4.1.2"
|
|
67
67
|
},
|
|
68
68
|
"peerDependenciesMeta": {
|
|
69
69
|
"@effect/sql-pg": {
|
package/src/backfillPostgres.ts
CHANGED
|
@@ -38,7 +38,7 @@ export const backfillPostgres = Effect.fn("Sql.backfillPostgres")(function* <TRo
|
|
|
38
38
|
yield* options.saveProgress(last, progress);
|
|
39
39
|
}
|
|
40
40
|
return rows.length;
|
|
41
|
-
}).pipe(transact(
|
|
41
|
+
}).pipe(transact());
|
|
42
42
|
processed += count;
|
|
43
43
|
if (count === 0) {
|
|
44
44
|
return processed;
|
|
@@ -5,6 +5,7 @@ import { SqlClient, SqlSchema } from "effect/sql";
|
|
|
5
5
|
import type { SqlError } from "effect/sql/SqlError";
|
|
6
6
|
import { storageSchema } from "#adapters/storageSchema.ts";
|
|
7
7
|
import type { BatchedDelete } from "#batchedDelete.ts";
|
|
8
|
+
import type { RecordRead } from "#recordRepositoryRead.ts";
|
|
8
9
|
import type { RepositoryChanges } from "#repositoryChanges.ts";
|
|
9
10
|
import type { RepositoryListFilter } from "#repositoryFilter.ts";
|
|
10
11
|
import type { Update, UpdateVoid } from "#repositoryUpdate.ts";
|
|
@@ -47,6 +48,7 @@ export const makeCompositeCrud = Effect.fnUntraced(function* (
|
|
|
47
48
|
model: Model.Any,
|
|
48
49
|
options: {
|
|
49
50
|
readonly columns: readonly string[];
|
|
51
|
+
readonly recordRead: RecordRead;
|
|
50
52
|
readonly tableName: string;
|
|
51
53
|
readonly spanPrefix: string;
|
|
52
54
|
},
|
|
@@ -54,7 +56,7 @@ export const makeCompositeCrud = Effect.fnUntraced(function* (
|
|
|
54
56
|
) {
|
|
55
57
|
const sql = yield* SqlClient.SqlClient;
|
|
56
58
|
const nameTransforms = yield* sqlNameTransforms;
|
|
57
|
-
const { columns, tableName, spanPrefix } = options;
|
|
59
|
+
const { columns, recordRead, tableName, spanPrefix } = options;
|
|
58
60
|
const entries: [string, Schema.Constraint][] = [];
|
|
59
61
|
for (const column of columns) {
|
|
60
62
|
const field = model.fields[column];
|
|
@@ -84,6 +86,11 @@ export const makeCompositeCrud = Effect.fnUntraced(function* (
|
|
|
84
86
|
|
|
85
87
|
return {
|
|
86
88
|
...(yield* repositoryWrites(model, { ...options, idColumns: columns }, changes)),
|
|
87
|
-
findById: (id: unknown) =>
|
|
89
|
+
findById: (id: unknown) =>
|
|
90
|
+
record(columns.length === 1 ? { [columns[0] ?? ""]: id } : id).pipe(
|
|
91
|
+
Effect.flatMap(recordRead),
|
|
92
|
+
Effect.andThen(find(id)),
|
|
93
|
+
Effect.withSpan(`${spanPrefix}.findById`),
|
|
94
|
+
),
|
|
88
95
|
};
|
|
89
96
|
});
|
package/src/databaseAddress.ts
CHANGED
|
@@ -1,25 +1,43 @@
|
|
|
1
1
|
import type { PgPoolConfig } from "@effect/sql-pg/PgClient";
|
|
2
2
|
import { Redacted } from "effect";
|
|
3
3
|
|
|
4
|
+
function isDecodableHost(hostname: string): boolean {
|
|
5
|
+
const runs = hostname.match(/(?:%[\da-f]{2})+/giu) ?? [];
|
|
6
|
+
if (hostname.replace(/(?:%[\da-f]{2})+/giu, "").includes("%")) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
const encoder = new TextEncoder();
|
|
10
|
+
const decoder = new TextDecoder();
|
|
11
|
+
return runs.every((run) => {
|
|
12
|
+
const bytes = Uint8Array.from(run.match(/[\da-f]{2}/giu) ?? [], (byte) => Number.parseInt(byte, 16));
|
|
13
|
+
const decoded = encoder.encode(decoder.decode(bytes));
|
|
14
|
+
return decoded.length === bytes.length && decoded.every((byte, index) => byte === bytes[index]);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
4
18
|
export function databaseAddress(value: Redacted.Redacted<string>): Pick<PgPoolConfig, "host" | "port"> {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
const address = Redacted.value(value);
|
|
20
|
+
if (!URL.canParse(address)) {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
const url = new URL(address);
|
|
24
|
+
if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
const rawHostname = url.hostname.startsWith("[") ? url.hostname.slice(1, -1) : url.hostname;
|
|
28
|
+
if (!isDecodableHost(rawHostname)) {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
const hostname = decodeURIComponent(rawHostname);
|
|
32
|
+
// PgConnection applies query parameters in order, after the authority.
|
|
33
|
+
const host = url.searchParams.getAll("host").at(-1) ?? hostname;
|
|
34
|
+
const rawPort = url.searchParams.getAll("port").at(-1) ?? url.port;
|
|
35
|
+
const port = rawPort === "" ? undefined : Number(rawPort);
|
|
36
|
+
if (host.includes(",") || (port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65_535))) {
|
|
23
37
|
return {};
|
|
24
38
|
}
|
|
39
|
+
return {
|
|
40
|
+
...(host === "" && !url.searchParams.has("host") ? {} : { host }),
|
|
41
|
+
...(port === undefined ? {} : { port }),
|
|
42
|
+
};
|
|
25
43
|
}
|
package/src/makeAggregate.ts
CHANGED
|
@@ -6,15 +6,16 @@ import type { Fragment } from "effect/sql/Statement";
|
|
|
6
6
|
import { aggregateColumn } from "#aggregateColumn.ts";
|
|
7
7
|
import type { Aggregate } from "#aggregateQuery.ts";
|
|
8
8
|
import { makeQueryClauses } from "#makeQueryClauses.ts";
|
|
9
|
+
import type { RecordRead } from "#recordRepositoryRead.ts";
|
|
9
10
|
import { type FindManyFilter, makeFilter } from "#repositoryFilter.ts";
|
|
10
11
|
|
|
11
12
|
export function makeAggregate<TModel extends Model.Any>(
|
|
12
13
|
model: TModel,
|
|
13
|
-
options: { readonly tableName: string; readonly spanPrefix: string },
|
|
14
|
+
options: { readonly recordRead: RecordRead; readonly tableName: string; readonly spanPrefix: string },
|
|
14
15
|
): Effect.Effect<Aggregate<TModel>, never, SqlClient.SqlClient>;
|
|
15
16
|
export function makeAggregate(
|
|
16
17
|
model: Model.Any,
|
|
17
|
-
options: { readonly tableName: string; readonly spanPrefix: string },
|
|
18
|
+
options: { readonly recordRead: RecordRead; readonly tableName: string; readonly spanPrefix: string },
|
|
18
19
|
): Effect.Effect<unknown, never, SqlClient.SqlClient> {
|
|
19
20
|
return Effect.fnUntraced(function* () {
|
|
20
21
|
const sql = yield* SqlClient.SqlClient;
|
|
@@ -56,6 +57,7 @@ export function makeAggregate(
|
|
|
56
57
|
const predicate = yield* where(query.where, true, true);
|
|
57
58
|
const order = yield* clauses.orderClause(query.orderBy);
|
|
58
59
|
const limit = yield* clauses.limitClause(query.limit);
|
|
60
|
+
yield* options.recordRead(query.where);
|
|
59
61
|
const partitions = sql.csv(groups.map((key) => sql`${sql(key)}`));
|
|
60
62
|
const rows =
|
|
61
63
|
yield* sql`select ${sql.csv(columns)} from ${sql(options.tableName)} where ${predicate} group by ${partitions} ${order} ${limit}`;
|
package/src/migrations.ts
CHANGED
|
@@ -78,7 +78,7 @@ export const migratePostgres = Effect.fn("EffectSql.migratePostgres")(function*
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
return yield* migrate({ loader: Effect.succeed(current), table });
|
|
81
|
-
}).pipe(transact(
|
|
81
|
+
}).pipe(transact(), Effect.provideService(Reactivity.Reactivity, reactivity));
|
|
82
82
|
const schemaComponents: readonly SchemaComponent<R, E | SqlError | Schema.SchemaError>[] = [
|
|
83
83
|
...components,
|
|
84
84
|
...(release === undefined ? [] : [postgresRelease(release, table)]),
|
package/src/postgresChanges.ts
CHANGED
|
@@ -59,7 +59,7 @@ export function postgresChanges(options: PostgresChangesOptions) {
|
|
|
59
59
|
const publication: CommandPublisher = {
|
|
60
60
|
publish: (keys) =>
|
|
61
61
|
invalidateOnCommit(keys).pipe(
|
|
62
|
-
transact(
|
|
62
|
+
transact(),
|
|
63
63
|
Effect.provideService(SqlClient.SqlClient, options.client),
|
|
64
64
|
Effect.catchCause((cause) => Effect.fail(new CommandPublicationError({ cause }))),
|
|
65
65
|
),
|
package/src/readSql.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Data, Effect, Schema } from "effect";
|
|
2
|
+
import { SqlClient } from "effect/sql";
|
|
3
|
+
import { type Composite, type CompositeBucketKey, type CompositeItemKey, isComposite } from "@shivaedev/changes/compositeKeys.ts";
|
|
4
|
+
import { type BucketKey, type ItemKey, Key, recordKey } from "@shivaedev/changes/keys.ts";
|
|
5
|
+
import { recordRead } from "@shivaedev/changes/readRecorder.ts";
|
|
6
|
+
|
|
7
|
+
export class MultiColumnKeyParameter extends Data.TaggedError("MultiColumnKeyParameter")<{
|
|
8
|
+
readonly key: CompositeBucketKey | CompositeItemKey;
|
|
9
|
+
}> {
|
|
10
|
+
override get message() {
|
|
11
|
+
const named =
|
|
12
|
+
this.key._tag === "Bucket"
|
|
13
|
+
? `the tuple bucket ${this.key.collection}.${this.key.field} ${this.key.value}`
|
|
14
|
+
: `the composite row ${recordKey(this.key.collection, this.key.id)}`;
|
|
15
|
+
return `readSql cannot bind ${named} as one parameter, because it spans several columns. Bind each column's value and declare the key in the query's reads.`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type Refused<TArgs extends readonly unknown[]> = {
|
|
20
|
+
readonly [TIndex in keyof TArgs]: TArgs[TIndex] extends { readonly [Composite]: true }
|
|
21
|
+
? "A tuple bucket or composite row key spans several columns, so it cannot be one parameter"
|
|
22
|
+
: TArgs[TIndex];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const isKey = Schema.is(Key);
|
|
26
|
+
|
|
27
|
+
function isReadKey(value: unknown): value is BucketKey | ItemKey {
|
|
28
|
+
return isKey(value) && value._tag !== "List";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function bound(value: unknown) {
|
|
32
|
+
if (!isReadKey(value)) {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
return value._tag === "Bucket" ? value.value : value.id;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const readMarked = Effect.fn("Sql.readMarked")(function* (strings: TemplateStringsArray, args: readonly unknown[]) {
|
|
39
|
+
const keys = args.filter(isReadKey);
|
|
40
|
+
const multiColumn = keys.find(isComposite);
|
|
41
|
+
if (multiColumn !== undefined) {
|
|
42
|
+
return yield* new MultiColumnKeyParameter({ key: multiColumn });
|
|
43
|
+
}
|
|
44
|
+
const sql = yield* SqlClient.SqlClient;
|
|
45
|
+
yield* recordRead(keys, { inferred: true });
|
|
46
|
+
return yield* sql(strings, ...args.map(bound));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export function readSql<const TArgs extends readonly unknown[]>(strings: TemplateStringsArray, ...args: TArgs & Refused<TArgs>) {
|
|
50
|
+
return readMarked(strings, args);
|
|
51
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import type { Model } from "effect/schema";
|
|
3
|
+
import { declaredBuckets } from "@shivaedev/changes/buckets.ts";
|
|
4
|
+
import { collection, compositeId, type Identity, type Key } from "@shivaedev/changes/keys.ts";
|
|
5
|
+
import { recordRead } from "@shivaedev/changes/readRecorder.ts";
|
|
6
|
+
import type { RepositoryCollection } from "#repositoryCollection.ts";
|
|
7
|
+
|
|
8
|
+
type Where = Readonly<Record<string, unknown>>;
|
|
9
|
+
|
|
10
|
+
export type RecordRead = (where?: Where) => Effect.Effect<void, Schema.SchemaError, unknown>;
|
|
11
|
+
|
|
12
|
+
const AnyIdentity = Schema.Union([Schema.String, Schema.Number]);
|
|
13
|
+
|
|
14
|
+
// Each key is its own registration, renewed on every rerun, so a read pinned to more keys listens on a broader bucket instead.
|
|
15
|
+
const MAX_BUCKET_KEYS = 1000;
|
|
16
|
+
|
|
17
|
+
function isIdentity(value: unknown): value is Identity {
|
|
18
|
+
return typeof value === "string" || typeof value === "number";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isPrimitive(value: unknown): value is Identity | bigint | boolean {
|
|
22
|
+
return isIdentity(value) || typeof value === "bigint" || typeof value === "boolean";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function combinations<T>(columns: readonly (readonly T[])[]): readonly (readonly T[])[] {
|
|
26
|
+
return columns.reduce<readonly (readonly T[])[]>((rows, values) => rows.flatMap((row) => values.map((value) => [...row, value])), [[]]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type Pin = readonly [column: string, value: Identity];
|
|
30
|
+
|
|
31
|
+
interface Bucket {
|
|
32
|
+
readonly columns: readonly string[];
|
|
33
|
+
readonly make: (pins: readonly Pin[]) => readonly Key[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function makeRecordRead(
|
|
37
|
+
model: Model.Any,
|
|
38
|
+
options: { readonly tableName: string; readonly idColumns: readonly string[]; readonly tuples?: RepositoryCollection["tuples"] },
|
|
39
|
+
): RecordRead {
|
|
40
|
+
const fields = declaredBuckets(model.fields).map(({ field }) => field);
|
|
41
|
+
const keys = collection(options.tableName, AnyIdentity, Object.fromEntries(fields.map((field) => [field, AnyIdentity])));
|
|
42
|
+
const tupleKeys = collection(options.tableName, AnyIdentity, {}, options.tuples);
|
|
43
|
+
const buckets: readonly Bucket[] = [
|
|
44
|
+
...Object.entries(tupleKeys.tuples).flatMap(([tuple, columns]) => {
|
|
45
|
+
const make = tupleKeys.bucket[tuple];
|
|
46
|
+
return make === undefined ? [] : [{ columns, make: (pins: readonly Pin[]) => [make(Object.fromEntries(pins))] }];
|
|
47
|
+
}),
|
|
48
|
+
...fields.flatMap((field) => {
|
|
49
|
+
const make = keys.bucket[field];
|
|
50
|
+
return make === undefined ? [] : [{ columns: [field], make: (pins: readonly Pin[]) => pins.map(([, value]) => make(value)) }];
|
|
51
|
+
}),
|
|
52
|
+
];
|
|
53
|
+
function pinned(where: Where, column: string): readonly unknown[] | undefined {
|
|
54
|
+
const field = model.fields[column];
|
|
55
|
+
const value = where[column];
|
|
56
|
+
if (field === undefined || value === undefined || value === null) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const acceptsValue: (input: unknown) => boolean = Schema.is(field);
|
|
60
|
+
if (acceptsValue(value)) {
|
|
61
|
+
return [value];
|
|
62
|
+
}
|
|
63
|
+
return typeof value === "object" && "in" in value && Array.isArray(value.in) && value.in.every((entry) => entry !== null && entry !== undefined)
|
|
64
|
+
? value.in
|
|
65
|
+
: undefined;
|
|
66
|
+
}
|
|
67
|
+
// Writes key a row by the values the driver returns, which the encoded value matches unless the codec wraps it in a driver parameter.
|
|
68
|
+
const storedPart = Effect.fnUntraced(function* (column: string, value: unknown) {
|
|
69
|
+
const field = model.fields[column];
|
|
70
|
+
const encoded = field === undefined ? undefined : yield* Schema.encodeUnknownEffect(field)(value);
|
|
71
|
+
return [encoded, value].find(isPrimitive);
|
|
72
|
+
});
|
|
73
|
+
const rowId = Effect.fnUntraced(function* (row: readonly unknown[]) {
|
|
74
|
+
const [single] = row;
|
|
75
|
+
if (row.length === 1 && isIdentity(single)) {
|
|
76
|
+
return single;
|
|
77
|
+
}
|
|
78
|
+
const parts = yield* Effect.forEach(row, (value, index) => storedPart(options.idColumns[index] ?? "", value));
|
|
79
|
+
if (!parts.every(isPrimitive)) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
return parts.length === 1 ? String(parts[0]) : compositeId(parts);
|
|
83
|
+
});
|
|
84
|
+
const rowKeys = Effect.fnUntraced(function* (where: Where) {
|
|
85
|
+
const columns: (readonly unknown[])[] = [];
|
|
86
|
+
for (const column of options.idColumns) {
|
|
87
|
+
const values = pinned(where, column);
|
|
88
|
+
if (values === undefined) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
columns.push(values);
|
|
92
|
+
}
|
|
93
|
+
const ids = yield* Effect.forEach(combinations(columns), rowId);
|
|
94
|
+
return ids.every((id) => id !== undefined) ? ids.map((id): Key => keys.item(id)) : undefined;
|
|
95
|
+
});
|
|
96
|
+
function pinnedKeys(where: Where, { columns, make }: Bucket) {
|
|
97
|
+
const pins: (readonly Pin[])[] = [];
|
|
98
|
+
let count = 1;
|
|
99
|
+
for (const column of columns) {
|
|
100
|
+
const values = pinned(where, column);
|
|
101
|
+
if (values === undefined || !values.every(isIdentity)) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
count *= values.length;
|
|
105
|
+
pins.push(values.map((value): Pin => [column, value]));
|
|
106
|
+
}
|
|
107
|
+
return count > MAX_BUCKET_KEYS ? undefined : { columns, keys: combinations(pins).flatMap(make) };
|
|
108
|
+
}
|
|
109
|
+
function bucketKeys(where: Where): readonly Key[] | undefined {
|
|
110
|
+
const pinnedBuckets = buckets.flatMap((bucket) => pinnedKeys(where, bucket) ?? []);
|
|
111
|
+
let narrowest: readonly Key[] | undefined;
|
|
112
|
+
for (const { columns, keys: candidate } of pinnedBuckets) {
|
|
113
|
+
const covered = pinnedBuckets.some(
|
|
114
|
+
(other) => other.columns.length > columns.length && columns.every((column) => other.columns.includes(column)),
|
|
115
|
+
);
|
|
116
|
+
if (!covered && (narrowest === undefined || candidate.length < narrowest.length)) {
|
|
117
|
+
narrowest = candidate;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return narrowest;
|
|
121
|
+
}
|
|
122
|
+
return (where = {}) =>
|
|
123
|
+
Effect.gen(function* () {
|
|
124
|
+
yield* recordRead((yield* rowKeys(where)) ?? bucketKeys(where) ?? [keys.list], { inferred: true });
|
|
125
|
+
});
|
|
126
|
+
}
|
package/src/repository.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { type BatchWrites, makeInsertMany } from "#batchWrites.ts";
|
|
|
6
6
|
import { makeCompositeCrud, type PrimaryKey, type Repository } from "#compositeRepository.ts";
|
|
7
7
|
import { type ConflictColumns, type ConflictWrites, makeConflictWrites } from "#conflictWrites.ts";
|
|
8
8
|
import { makeInsertFromSelect, type SelectWrites } from "#insertFromSelect.ts";
|
|
9
|
+
import { makeRecordRead } from "#recordRepositoryRead.ts";
|
|
9
10
|
import { repositoryChanges } from "#repositoryChanges.ts";
|
|
10
11
|
import { type BucketRegistrationError, type RepositoryOptions, validateRepositoryCollection } from "#repositoryCollection.ts";
|
|
11
12
|
import type { FindManyFilter, RepositoryFilter, RepositoryListFilter } from "#repositoryFilter.ts";
|
|
@@ -118,14 +119,14 @@ export function makeRepository<TModel extends Model.Any, TId extends IdColumn<TM
|
|
|
118
119
|
): Effect.Effect<unknown, BucketRegistrationError, SqlClient.SqlClient> {
|
|
119
120
|
return Effect.fnUntraced(function* () {
|
|
120
121
|
yield* validateRepositoryCollection(model.fields, options);
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const
|
|
122
|
+
const idColumns = (typeof options.idColumn === "object" ? options.idColumn : [options.idColumn]).map(String);
|
|
123
|
+
const changes = yield* repositoryChanges(options.tableName, idColumns, model.fields, options.collection?.tuples);
|
|
124
|
+
const recordRead = makeRecordRead(model, { idColumns, tableName: options.tableName, tuples: options.collection?.tuples });
|
|
124
125
|
const crud = {
|
|
125
|
-
...(yield* makeCompositeCrud(model, { ...options, columns: idColumns }, changes)),
|
|
126
|
+
...(yield* makeCompositeCrud(model, { ...options, columns: idColumns, recordRead }, changes)),
|
|
126
127
|
deleteBatched: yield* makeBatchedDelete(model, options, changes),
|
|
127
128
|
};
|
|
128
|
-
const queries = yield* makeQueries(model, options);
|
|
129
|
+
const queries = yield* makeQueries(model, { ...options, recordRead });
|
|
129
130
|
return {
|
|
130
131
|
...crud,
|
|
131
132
|
...queries,
|
package/src/repositoryChanges.ts
CHANGED
|
@@ -1,34 +1,61 @@
|
|
|
1
1
|
import { Effect, Option, type Schema } from "effect";
|
|
2
2
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
3
3
|
import { SqlClient } from "effect/sql";
|
|
4
|
-
import { declaredBuckets } from "@shivaedev/changes/buckets.ts";
|
|
5
|
-
import { collectionInvalidationKeys, compositeId, invalidationKeys, type Key, recordKey } from "@shivaedev/changes/keys.ts";
|
|
4
|
+
import { declaredBuckets, decodeBucketValue } from "@shivaedev/changes/buckets.ts";
|
|
5
|
+
import { collectionInvalidationKeys, compositeId, type Identity, invalidationKeys, type Key, recordKey } from "@shivaedev/changes/keys.ts";
|
|
6
6
|
import { ChangeDeclarations } from "#ChangeDeclarations.ts";
|
|
7
7
|
import { invalidateOnCommit, transact } from "#transact.ts";
|
|
8
8
|
|
|
9
9
|
export type RepositoryChanges = Effect.Success<ReturnType<typeof repositoryChanges>>;
|
|
10
10
|
|
|
11
|
-
export const repositoryChanges = Effect.fnUntraced(function* (
|
|
11
|
+
export const repositoryChanges = Effect.fnUntraced(function* (
|
|
12
|
+
table: string,
|
|
13
|
+
columns: readonly string[],
|
|
14
|
+
fields: Schema.Struct.Fields = {},
|
|
15
|
+
tuples: Readonly<Record<string, readonly string[]>> = {},
|
|
16
|
+
) {
|
|
12
17
|
const sql = yield* SqlClient.SqlClient;
|
|
13
|
-
const
|
|
18
|
+
const declared = [
|
|
19
|
+
...declaredBuckets(fields).map(({ field }) => ({ columns: [field], field, tuple: false })),
|
|
20
|
+
...Object.entries(tuples).map(([field, tupleColumns]) => ({ columns: tupleColumns, field, tuple: true })),
|
|
21
|
+
];
|
|
22
|
+
const columnParts = new Map(
|
|
23
|
+
[...new Set(declared.flatMap((bucket) => bucket.columns))].flatMap((column, index) => {
|
|
24
|
+
const schema = fields[column];
|
|
25
|
+
return schema === undefined ? [] : [[column, { column, decode: decodeBucketValue(schema), old: `shivaeOldBucket${index}` }] as const];
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
const buckets = declared.map((bucket) => ({ ...bucket, parts: bucket.columns.flatMap((column) => columnParts.get(column) ?? []) }));
|
|
29
|
+
const bucketValue = Effect.fnUntraced(function* (
|
|
30
|
+
row: Readonly<Record<string, unknown>>,
|
|
31
|
+
{ parts, tuple }: (typeof buckets)[number],
|
|
32
|
+
side: "column" | "old",
|
|
33
|
+
) {
|
|
34
|
+
const values: Identity[] = [];
|
|
35
|
+
for (const part of parts) {
|
|
36
|
+
const stored = row[part[side]];
|
|
37
|
+
const decoded = stored === null || stored === undefined ? undefined : yield* part.decode(stored);
|
|
38
|
+
if (decoded === null || decoded === undefined) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
values.push(decoded);
|
|
42
|
+
}
|
|
43
|
+
return tuple ? compositeId(values.map(String)) : values[0];
|
|
44
|
+
});
|
|
14
45
|
const bucketKeys = Effect.fnUntraced(function* (row: Readonly<Record<string, unknown>>) {
|
|
15
46
|
const keys: Key[] = [];
|
|
16
|
-
for (const
|
|
17
|
-
for (const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const decoded = yield* decode(value);
|
|
22
|
-
if (decoded === null || decoded === undefined) {
|
|
23
|
-
continue;
|
|
47
|
+
for (const bucket of buckets) {
|
|
48
|
+
for (const side of ["column", "old"] as const) {
|
|
49
|
+
const value = yield* bucketValue(row, bucket, side);
|
|
50
|
+
if (value !== undefined) {
|
|
51
|
+
keys.push({ _tag: "Bucket", collection: table, field: bucket.field, value });
|
|
24
52
|
}
|
|
25
|
-
keys.push({ _tag: "Bucket", collection: table, field, value: decoded });
|
|
26
53
|
}
|
|
27
54
|
}
|
|
28
55
|
return invalidationKeys(keys);
|
|
29
56
|
});
|
|
30
57
|
function returning(base = sql`*`) {
|
|
31
|
-
return sql.csv([base, ...
|
|
58
|
+
return sql.csv([base, ...[...columnParts.values()].map(({ column, old }) => sql`old.${sql(column)} as ${sql(old)}`)]);
|
|
32
59
|
}
|
|
33
60
|
const initial = yield* Effect.serviceOption(Reactivity.Reactivity);
|
|
34
61
|
const fallback = Option.isSome(initial) ? initial.value : yield* Reactivity.make;
|
|
@@ -60,7 +87,7 @@ export const repositoryChanges = Effect.fnUntraced(function* (table: string, col
|
|
|
60
87
|
yield* provide(invalidateOnCommit([...keys]));
|
|
61
88
|
});
|
|
62
89
|
function write<A, E, R>(effect: Effect.Effect<A, E, R>) {
|
|
63
|
-
return provide(transact(effect
|
|
90
|
+
return provide(transact(effect));
|
|
64
91
|
}
|
|
65
92
|
const recordTable = Effect.fnUntraced(function* (rows: number) {
|
|
66
93
|
if (rows === 0) {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Data, Effect, Order, type Schema } from "effect";
|
|
2
2
|
import { declaredBuckets } from "@shivaedev/changes/buckets.ts";
|
|
3
|
+
import { isBucketSchema } from "@shivaedev/changes/keys.ts";
|
|
3
4
|
|
|
4
5
|
export interface RepositoryCollection {
|
|
5
6
|
readonly bucket: Readonly<Record<string, unknown>>;
|
|
6
7
|
readonly name: string;
|
|
8
|
+
readonly tuples?: Readonly<Record<string, readonly [string, string, ...string[]]>>;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
export interface RepositoryOptions<TId> {
|
|
@@ -19,11 +21,15 @@ export class BucketRegistrationError extends Data.TaggedError("BucketRegistratio
|
|
|
19
21
|
readonly modelFields: readonly string[];
|
|
20
22
|
readonly collectionFields: readonly string[];
|
|
21
23
|
readonly invalidFields: readonly string[];
|
|
24
|
+
readonly invalidTupleColumns: readonly string[];
|
|
22
25
|
}> {
|
|
23
26
|
override get message() {
|
|
24
27
|
if (this.invalidFields.length > 0) {
|
|
25
28
|
return `Bucket registration for ${this.tableName}: fields ${JSON.stringify(this.invalidFields)} must decode to strings, numbers or absent values`;
|
|
26
29
|
}
|
|
30
|
+
if (this.invalidTupleColumns.length > 0) {
|
|
31
|
+
return `Bucket registration for ${this.tableName}: tuple columns ${JSON.stringify(this.invalidTupleColumns)} must be model fields that decode to strings, numbers or absent values`;
|
|
32
|
+
}
|
|
27
33
|
return `Bucket registration for ${this.tableName}: model fields ${JSON.stringify(this.modelFields)} disagree with collection ${this.collectionName ?? "(missing)"} fields ${JSON.stringify(this.collectionFields)}`;
|
|
28
34
|
}
|
|
29
35
|
}
|
|
@@ -37,10 +43,20 @@ export const validateRepositoryCollection = Effect.fnUntraced(function* (
|
|
|
37
43
|
.filter(({ supported }) => !supported)
|
|
38
44
|
.map(({ field }) => field)
|
|
39
45
|
.sort(Order.String);
|
|
46
|
+
const tuples = options.collection?.tuples ?? {};
|
|
47
|
+
const invalidTupleColumns = Object.entries(tuples).flatMap(([tuple, columns]) =>
|
|
48
|
+
columns.flatMap((column) => {
|
|
49
|
+
const schema = fields[column];
|
|
50
|
+
return schema !== undefined && isBucketSchema(schema) ? [] : [`${tuple}.${column}`];
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
40
53
|
const modelFields = buckets.map(({ field }) => field).sort(Order.String);
|
|
41
|
-
const collectionFields = Object.keys(options.collection?.bucket ?? {})
|
|
54
|
+
const collectionFields = Object.keys(options.collection?.bucket ?? {})
|
|
55
|
+
.filter((field) => !Object.hasOwn(tuples, field))
|
|
56
|
+
.sort(Order.String);
|
|
42
57
|
if (
|
|
43
58
|
invalidFields.length === 0
|
|
59
|
+
&& invalidTupleColumns.length === 0
|
|
44
60
|
&& ((options.collection === undefined && modelFields.length === 0)
|
|
45
61
|
|| (options.collection?.name === options.tableName
|
|
46
62
|
&& modelFields.length === collectionFields.length
|
|
@@ -53,6 +69,7 @@ export const validateRepositoryCollection = Effect.fnUntraced(function* (
|
|
|
53
69
|
collectionFields,
|
|
54
70
|
collectionName: options.collection?.name,
|
|
55
71
|
invalidFields,
|
|
72
|
+
invalidTupleColumns,
|
|
56
73
|
modelFields,
|
|
57
74
|
tableName: options.tableName,
|
|
58
75
|
}),
|