@proseql/core 0.1.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/LICENSE +21 -0
- package/dist/errors/crud-errors.d.ts +98 -0
- package/dist/errors/crud-errors.d.ts.map +1 -0
- package/dist/errors/crud-errors.js +23 -0
- package/dist/errors/crud-errors.js.map +1 -0
- package/dist/errors/index.d.ts +16 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +12 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/migration-errors.d.ts +22 -0
- package/dist/errors/migration-errors.d.ts.map +1 -0
- package/dist/errors/migration-errors.js +14 -0
- package/dist/errors/migration-errors.js.map +1 -0
- package/dist/errors/plugin-errors.d.ts +15 -0
- package/dist/errors/plugin-errors.d.ts.map +1 -0
- package/dist/errors/plugin-errors.js +11 -0
- package/dist/errors/plugin-errors.js.map +1 -0
- package/dist/errors/query-errors.d.ts +31 -0
- package/dist/errors/query-errors.d.ts.map +1 -0
- package/dist/errors/query-errors.js +11 -0
- package/dist/errors/query-errors.js.map +1 -0
- package/dist/errors/storage-errors.d.ts +30 -0
- package/dist/errors/storage-errors.d.ts.map +1 -0
- package/dist/errors/storage-errors.js +11 -0
- package/dist/errors/storage-errors.js.map +1 -0
- package/dist/factories/crud-factory-with-relationships.d.ts +28 -0
- package/dist/factories/crud-factory-with-relationships.d.ts.map +1 -0
- package/dist/factories/crud-factory-with-relationships.js +8 -0
- package/dist/factories/crud-factory-with-relationships.js.map +1 -0
- package/dist/factories/crud-factory.d.ts +25 -0
- package/dist/factories/crud-factory.d.ts.map +1 -0
- package/dist/factories/crud-factory.js +8 -0
- package/dist/factories/crud-factory.js.map +1 -0
- package/dist/factories/database-effect.d.ts +241 -0
- package/dist/factories/database-effect.d.ts.map +1 -0
- package/dist/factories/database-effect.js +859 -0
- package/dist/factories/database-effect.js.map +1 -0
- package/dist/hooks/hook-runner.d.ts +60 -0
- package/dist/hooks/hook-runner.d.ts.map +1 -0
- package/dist/hooks/hook-runner.js +107 -0
- package/dist/hooks/hook-runner.js.map +1 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +110 -0
- package/dist/index.js.map +1 -0
- package/dist/indexes/index-lookup.d.ts +33 -0
- package/dist/indexes/index-lookup.d.ts.map +1 -0
- package/dist/indexes/index-lookup.js +180 -0
- package/dist/indexes/index-lookup.js.map +1 -0
- package/dist/indexes/index-manager.d.ts +118 -0
- package/dist/indexes/index-manager.d.ts.map +1 -0
- package/dist/indexes/index-manager.js +345 -0
- package/dist/indexes/index-manager.js.map +1 -0
- package/dist/indexes/search-index.d.ts +179 -0
- package/dist/indexes/search-index.d.ts.map +1 -0
- package/dist/indexes/search-index.js +405 -0
- package/dist/indexes/search-index.js.map +1 -0
- package/dist/migrations/migration-runner.d.ts +70 -0
- package/dist/migrations/migration-runner.d.ts.map +1 -0
- package/dist/migrations/migration-runner.js +271 -0
- package/dist/migrations/migration-runner.js.map +1 -0
- package/dist/migrations/migration-types.d.ts +63 -0
- package/dist/migrations/migration-types.d.ts.map +1 -0
- package/dist/migrations/migration-types.js +5 -0
- package/dist/migrations/migration-types.js.map +1 -0
- package/dist/operations/crud/create-with-relationships.d.ts +44 -0
- package/dist/operations/crud/create-with-relationships.d.ts.map +1 -0
- package/dist/operations/crud/create-with-relationships.js +483 -0
- package/dist/operations/crud/create-with-relationships.js.map +1 -0
- package/dist/operations/crud/create.d.ts +48 -0
- package/dist/operations/crud/create.d.ts.map +1 -0
- package/dist/operations/crud/create.js +333 -0
- package/dist/operations/crud/create.js.map +1 -0
- package/dist/operations/crud/delete-with-relationships.d.ts +63 -0
- package/dist/operations/crud/delete-with-relationships.d.ts.map +1 -0
- package/dist/operations/crud/delete-with-relationships.js +395 -0
- package/dist/operations/crud/delete-with-relationships.js.map +1 -0
- package/dist/operations/crud/delete.d.ts +58 -0
- package/dist/operations/crud/delete.d.ts.map +1 -0
- package/dist/operations/crud/delete.js +267 -0
- package/dist/operations/crud/delete.js.map +1 -0
- package/dist/operations/crud/unique-check.d.ts +114 -0
- package/dist/operations/crud/unique-check.d.ts.map +1 -0
- package/dist/operations/crud/unique-check.js +383 -0
- package/dist/operations/crud/unique-check.js.map +1 -0
- package/dist/operations/crud/update-with-relationships.d.ts +45 -0
- package/dist/operations/crud/update-with-relationships.d.ts.map +1 -0
- package/dist/operations/crud/update-with-relationships.js +516 -0
- package/dist/operations/crud/update-with-relationships.js.map +1 -0
- package/dist/operations/crud/update.d.ts +91 -0
- package/dist/operations/crud/update.d.ts.map +1 -0
- package/dist/operations/crud/update.js +505 -0
- package/dist/operations/crud/update.js.map +1 -0
- package/dist/operations/crud/upsert.d.ts +52 -0
- package/dist/operations/crud/upsert.d.ts.map +1 -0
- package/dist/operations/crud/upsert.js +386 -0
- package/dist/operations/crud/upsert.js.map +1 -0
- package/dist/operations/query/aggregate.d.ts +30 -0
- package/dist/operations/query/aggregate.d.ts.map +1 -0
- package/dist/operations/query/aggregate.js +227 -0
- package/dist/operations/query/aggregate.js.map +1 -0
- package/dist/operations/query/cursor-stream.d.ts +18 -0
- package/dist/operations/query/cursor-stream.d.ts.map +1 -0
- package/dist/operations/query/cursor-stream.js +199 -0
- package/dist/operations/query/cursor-stream.js.map +1 -0
- package/dist/operations/query/filter-stream.d.ts +12 -0
- package/dist/operations/query/filter-stream.d.ts.map +1 -0
- package/dist/operations/query/filter-stream.js +167 -0
- package/dist/operations/query/filter-stream.js.map +1 -0
- package/dist/operations/query/filter.d.ts +13 -0
- package/dist/operations/query/filter.d.ts.map +1 -0
- package/dist/operations/query/filter.js +267 -0
- package/dist/operations/query/filter.js.map +1 -0
- package/dist/operations/query/paginate-stream.d.ts +11 -0
- package/dist/operations/query/paginate-stream.d.ts.map +1 -0
- package/dist/operations/query/paginate-stream.js +22 -0
- package/dist/operations/query/paginate-stream.js.map +1 -0
- package/dist/operations/query/query-helpers.d.ts +14 -0
- package/dist/operations/query/query-helpers.d.ts.map +1 -0
- package/dist/operations/query/query-helpers.js +22 -0
- package/dist/operations/query/query-helpers.js.map +1 -0
- package/dist/operations/query/resolve-computed.d.ts +142 -0
- package/dist/operations/query/resolve-computed.d.ts.map +1 -0
- package/dist/operations/query/resolve-computed.js +197 -0
- package/dist/operations/query/resolve-computed.js.map +1 -0
- package/dist/operations/query/search.d.ts +110 -0
- package/dist/operations/query/search.d.ts.map +1 -0
- package/dist/operations/query/search.js +188 -0
- package/dist/operations/query/search.js.map +1 -0
- package/dist/operations/query/select-stream.d.ts +27 -0
- package/dist/operations/query/select-stream.d.ts.map +1 -0
- package/dist/operations/query/select-stream.js +88 -0
- package/dist/operations/query/select-stream.js.map +1 -0
- package/dist/operations/query/select.d.ts +54 -0
- package/dist/operations/query/select.d.ts.map +1 -0
- package/dist/operations/query/select.js +159 -0
- package/dist/operations/query/select.js.map +1 -0
- package/dist/operations/query/sort-stream.d.ts +46 -0
- package/dist/operations/query/sort-stream.d.ts.map +1 -0
- package/dist/operations/query/sort-stream.js +158 -0
- package/dist/operations/query/sort-stream.js.map +1 -0
- package/dist/operations/query/sort.d.ts +9 -0
- package/dist/operations/query/sort.d.ts.map +1 -0
- package/dist/operations/query/sort.js +58 -0
- package/dist/operations/query/sort.js.map +1 -0
- package/dist/operations/relationships/populate-stream.d.ts +29 -0
- package/dist/operations/relationships/populate-stream.d.ts.map +1 -0
- package/dist/operations/relationships/populate-stream.js +159 -0
- package/dist/operations/relationships/populate-stream.js.map +1 -0
- package/dist/operations/relationships/populate.d.ts +15 -0
- package/dist/operations/relationships/populate.d.ts.map +1 -0
- package/dist/operations/relationships/populate.js +228 -0
- package/dist/operations/relationships/populate.js.map +1 -0
- package/dist/plugins/plugin-hooks.d.ts +25 -0
- package/dist/plugins/plugin-hooks.d.ts.map +1 -0
- package/dist/plugins/plugin-hooks.js +64 -0
- package/dist/plugins/plugin-hooks.js.map +1 -0
- package/dist/plugins/plugin-registry.d.ts +26 -0
- package/dist/plugins/plugin-registry.d.ts.map +1 -0
- package/dist/plugins/plugin-registry.js +150 -0
- package/dist/plugins/plugin-registry.js.map +1 -0
- package/dist/plugins/plugin-types.d.ts +95 -0
- package/dist/plugins/plugin-types.d.ts.map +1 -0
- package/dist/plugins/plugin-types.js +6 -0
- package/dist/plugins/plugin-types.js.map +1 -0
- package/dist/plugins/plugin-validation.d.ts +49 -0
- package/dist/plugins/plugin-validation.d.ts.map +1 -0
- package/dist/plugins/plugin-validation.js +295 -0
- package/dist/plugins/plugin-validation.js.map +1 -0
- package/dist/reactive/change-event.d.ts +44 -0
- package/dist/reactive/change-event.d.ts.map +1 -0
- package/dist/reactive/change-event.js +49 -0
- package/dist/reactive/change-event.js.map +1 -0
- package/dist/reactive/change-pubsub.d.ts +32 -0
- package/dist/reactive/change-pubsub.d.ts.map +1 -0
- package/dist/reactive/change-pubsub.js +31 -0
- package/dist/reactive/change-pubsub.js.map +1 -0
- package/dist/reactive/evaluate-query.d.ts +62 -0
- package/dist/reactive/evaluate-query.d.ts.map +1 -0
- package/dist/reactive/evaluate-query.js +57 -0
- package/dist/reactive/evaluate-query.js.map +1 -0
- package/dist/reactive/watch-by-id.d.ts +53 -0
- package/dist/reactive/watch-by-id.d.ts.map +1 -0
- package/dist/reactive/watch-by-id.js +55 -0
- package/dist/reactive/watch-by-id.js.map +1 -0
- package/dist/reactive/watch.d.ts +78 -0
- package/dist/reactive/watch.d.ts.map +1 -0
- package/dist/reactive/watch.js +133 -0
- package/dist/reactive/watch.js.map +1 -0
- package/dist/serializers/codecs/hjson.d.ts +33 -0
- package/dist/serializers/codecs/hjson.d.ts.map +1 -0
- package/dist/serializers/codecs/hjson.js +40 -0
- package/dist/serializers/codecs/hjson.js.map +1 -0
- package/dist/serializers/codecs/json.d.ts +22 -0
- package/dist/serializers/codecs/json.d.ts.map +1 -0
- package/dist/serializers/codecs/json.js +28 -0
- package/dist/serializers/codecs/json.js.map +1 -0
- package/dist/serializers/codecs/json5.d.ts +26 -0
- package/dist/serializers/codecs/json5.d.ts.map +1 -0
- package/dist/serializers/codecs/json5.js +33 -0
- package/dist/serializers/codecs/json5.js.map +1 -0
- package/dist/serializers/codecs/jsonc.d.ts +29 -0
- package/dist/serializers/codecs/jsonc.d.ts.map +1 -0
- package/dist/serializers/codecs/jsonc.js +38 -0
- package/dist/serializers/codecs/jsonc.js.map +1 -0
- package/dist/serializers/codecs/jsonl.d.ts +17 -0
- package/dist/serializers/codecs/jsonl.d.ts.map +1 -0
- package/dist/serializers/codecs/jsonl.js +31 -0
- package/dist/serializers/codecs/jsonl.js.map +1 -0
- package/dist/serializers/codecs/prose.d.ts +419 -0
- package/dist/serializers/codecs/prose.d.ts.map +1 -0
- package/dist/serializers/codecs/prose.js +1060 -0
- package/dist/serializers/codecs/prose.js.map +1 -0
- package/dist/serializers/codecs/toml.d.ts +23 -0
- package/dist/serializers/codecs/toml.d.ts.map +1 -0
- package/dist/serializers/codecs/toml.js +66 -0
- package/dist/serializers/codecs/toml.js.map +1 -0
- package/dist/serializers/codecs/toon.d.ts +20 -0
- package/dist/serializers/codecs/toon.d.ts.map +1 -0
- package/dist/serializers/codecs/toon.js +33 -0
- package/dist/serializers/codecs/toon.js.map +1 -0
- package/dist/serializers/codecs/yaml.d.ts +24 -0
- package/dist/serializers/codecs/yaml.d.ts.map +1 -0
- package/dist/serializers/codecs/yaml.js +31 -0
- package/dist/serializers/codecs/yaml.js.map +1 -0
- package/dist/serializers/format-codec.d.ts +53 -0
- package/dist/serializers/format-codec.d.ts.map +1 -0
- package/dist/serializers/format-codec.js +148 -0
- package/dist/serializers/format-codec.js.map +1 -0
- package/dist/serializers/presets.d.ts +48 -0
- package/dist/serializers/presets.d.ts.map +1 -0
- package/dist/serializers/presets.js +72 -0
- package/dist/serializers/presets.js.map +1 -0
- package/dist/serializers/serializer-service.d.ts +11 -0
- package/dist/serializers/serializer-service.d.ts.map +1 -0
- package/dist/serializers/serializer-service.js +4 -0
- package/dist/serializers/serializer-service.js.map +1 -0
- package/dist/state/collection-state.d.ts +19 -0
- package/dist/state/collection-state.d.ts.map +1 -0
- package/dist/state/collection-state.js +15 -0
- package/dist/state/collection-state.js.map +1 -0
- package/dist/state/state-operations.d.ts +38 -0
- package/dist/state/state-operations.d.ts.map +1 -0
- package/dist/state/state-operations.js +65 -0
- package/dist/state/state-operations.js.map +1 -0
- package/dist/storage/in-memory-adapter-layer.d.ts +16 -0
- package/dist/storage/in-memory-adapter-layer.d.ts.map +1 -0
- package/dist/storage/in-memory-adapter-layer.js +81 -0
- package/dist/storage/in-memory-adapter-layer.js.map +1 -0
- package/dist/storage/persistence-effect.d.ts +244 -0
- package/dist/storage/persistence-effect.d.ts.map +1 -0
- package/dist/storage/persistence-effect.js +551 -0
- package/dist/storage/persistence-effect.js.map +1 -0
- package/dist/storage/storage-service.d.ts +22 -0
- package/dist/storage/storage-service.d.ts.map +1 -0
- package/dist/storage/storage-service.js +4 -0
- package/dist/storage/storage-service.js.map +1 -0
- package/dist/storage/transforms.d.ts +183 -0
- package/dist/storage/transforms.d.ts.map +1 -0
- package/dist/storage/transforms.js +263 -0
- package/dist/storage/transforms.js.map +1 -0
- package/dist/transactions/transaction.d.ts +87 -0
- package/dist/transactions/transaction.d.ts.map +1 -0
- package/dist/transactions/transaction.js +240 -0
- package/dist/transactions/transaction.js.map +1 -0
- package/dist/types/aggregate-types.d.ts +73 -0
- package/dist/types/aggregate-types.d.ts.map +1 -0
- package/dist/types/aggregate-types.js +14 -0
- package/dist/types/aggregate-types.js.map +1 -0
- package/dist/types/computed-types.d.ts +71 -0
- package/dist/types/computed-types.d.ts.map +1 -0
- package/dist/types/computed-types.js +8 -0
- package/dist/types/computed-types.js.map +1 -0
- package/dist/types/crud-relationship-types.d.ts +180 -0
- package/dist/types/crud-relationship-types.d.ts.map +1 -0
- package/dist/types/crud-relationship-types.js +17 -0
- package/dist/types/crud-relationship-types.js.map +1 -0
- package/dist/types/crud-types.d.ts +343 -0
- package/dist/types/crud-types.d.ts.map +1 -0
- package/dist/types/crud-types.js +43 -0
- package/dist/types/crud-types.js.map +1 -0
- package/dist/types/cursor-types.d.ts +52 -0
- package/dist/types/cursor-types.d.ts.map +1 -0
- package/dist/types/cursor-types.js +2 -0
- package/dist/types/cursor-types.js.map +1 -0
- package/dist/types/database-config-types.d.ts +196 -0
- package/dist/types/database-config-types.d.ts.map +1 -0
- package/dist/types/database-config-types.js +11 -0
- package/dist/types/database-config-types.js.map +1 -0
- package/dist/types/hook-types.d.ts +158 -0
- package/dist/types/hook-types.d.ts.map +1 -0
- package/dist/types/hook-types.js +6 -0
- package/dist/types/hook-types.js.map +1 -0
- package/dist/types/index-types.d.ts +42 -0
- package/dist/types/index-types.d.ts.map +1 -0
- package/dist/types/index-types.js +8 -0
- package/dist/types/index-types.js.map +1 -0
- package/dist/types/operators.d.ts +5 -0
- package/dist/types/operators.d.ts.map +1 -0
- package/dist/types/operators.js +297 -0
- package/dist/types/operators.js.map +1 -0
- package/dist/types/query-overloads.d.ts +54 -0
- package/dist/types/query-overloads.d.ts.map +1 -0
- package/dist/types/query-overloads.js +3 -0
- package/dist/types/query-overloads.js.map +1 -0
- package/dist/types/reactive-types.d.ts +75 -0
- package/dist/types/reactive-types.d.ts.map +1 -0
- package/dist/types/reactive-types.js +7 -0
- package/dist/types/reactive-types.js.map +1 -0
- package/dist/types/schema-types.d.ts +56 -0
- package/dist/types/schema-types.d.ts.map +1 -0
- package/dist/types/schema-types.js +8 -0
- package/dist/types/schema-types.js.map +1 -0
- package/dist/types/search-types.d.ts +82 -0
- package/dist/types/search-types.d.ts.map +1 -0
- package/dist/types/search-types.js +110 -0
- package/dist/types/search-types.js.map +1 -0
- package/dist/types/types.d.ts +286 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/types.js +2 -0
- package/dist/types/types.js.map +1 -0
- package/dist/utils/id-generator.d.ts +97 -0
- package/dist/utils/id-generator.d.ts.map +1 -0
- package/dist/utils/id-generator.js +247 -0
- package/dist/utils/id-generator.js.map +1 -0
- package/dist/utils/nested-path.d.ts +56 -0
- package/dist/utils/nested-path.d.ts.map +1 -0
- package/dist/utils/nested-path.js +119 -0
- package/dist/utils/nested-path.js.map +1 -0
- package/dist/utils/path.d.ts +16 -0
- package/dist/utils/path.d.ts.map +1 -0
- package/dist/utils/path.js +24 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/validators/foreign-key.d.ts +49 -0
- package/dist/validators/foreign-key.d.ts.map +1 -0
- package/dist/validators/foreign-key.js +153 -0
- package/dist/validators/foreign-key.js.map +1 -0
- package/dist/validators/schema-validator.d.ts +19 -0
- package/dist/validators/schema-validator.d.ts.map +1 -0
- package/dist/validators/schema-validator.js +34 -0
- package/dist/validators/schema-validator.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect-based database factory.
|
|
3
|
+
*
|
|
4
|
+
* Creates an in-memory database with typed collections, each backed by
|
|
5
|
+
* Ref<ReadonlyMap<string, T>> for O(1) ID lookup and atomic state updates.
|
|
6
|
+
*
|
|
7
|
+
* Query pipeline: Ref snapshot → Stream.fromIterable → filter → populate → sort → paginate → select
|
|
8
|
+
* CRUD: Effect-based operations with typed error channels
|
|
9
|
+
* Persistence: Optional debounced save after each CRUD mutation via Effect.fork
|
|
10
|
+
*/
|
|
11
|
+
import { Chunk, Effect, Layer, PubSub, Ref, Schema, Stream, } from "effect";
|
|
12
|
+
import { NotFoundError, OperationError, ValidationError, } from "../errors/crud-errors.js";
|
|
13
|
+
import { resolveWithIndex } from "../indexes/index-lookup.js";
|
|
14
|
+
import { buildIndexes, normalizeIndexes } from "../indexes/index-manager.js";
|
|
15
|
+
import { buildSearchIndex, resolveWithSearchIndex, } from "../indexes/search-index.js";
|
|
16
|
+
import { dryRunMigrations, validateMigrationRegistry, } from "../migrations/migration-runner.js";
|
|
17
|
+
import { create, createMany } from "../operations/crud/create.js";
|
|
18
|
+
import { createWithRelationships } from "../operations/crud/create-with-relationships.js";
|
|
19
|
+
import { del, deleteMany } from "../operations/crud/delete.js";
|
|
20
|
+
import { deleteManyWithRelationships, deleteWithRelationships, } from "../operations/crud/delete-with-relationships.js";
|
|
21
|
+
import { normalizeConstraints } from "../operations/crud/unique-check.js";
|
|
22
|
+
import { update, updateMany } from "../operations/crud/update.js";
|
|
23
|
+
import { updateWithRelationships } from "../operations/crud/update-with-relationships.js";
|
|
24
|
+
import { upsert, upsertMany } from "../operations/crud/upsert.js";
|
|
25
|
+
import { computeAggregates, computeGroupedAggregates, } from "../operations/query/aggregate.js";
|
|
26
|
+
import { applyCursor } from "../operations/query/cursor-stream.js";
|
|
27
|
+
import { applyFilter } from "../operations/query/filter-stream.js";
|
|
28
|
+
import { applyPagination } from "../operations/query/paginate-stream.js";
|
|
29
|
+
import { resolveComputedStreamWithLazySkip } from "../operations/query/resolve-computed.js";
|
|
30
|
+
import { applySelect, applySelectToArray, } from "../operations/query/select-stream.js";
|
|
31
|
+
import { applyRelevanceSort, applySort, attachSearchScores, extractSearchConfig, } from "../operations/query/sort-stream.js";
|
|
32
|
+
import { applyPopulate } from "../operations/relationships/populate-stream.js";
|
|
33
|
+
import { mergeGlobalHooks } from "../plugins/plugin-hooks.js";
|
|
34
|
+
import { buildPluginRegistry } from "../plugins/plugin-registry.js";
|
|
35
|
+
import { validateIdGeneratorReferences } from "../plugins/plugin-validation.js";
|
|
36
|
+
import { watch } from "../reactive/watch.js";
|
|
37
|
+
import { watchById } from "../reactive/watch-by-id.js";
|
|
38
|
+
import { mergeSerializerWithPluginCodecs, } from "../serializers/format-codec.js";
|
|
39
|
+
import { SerializerRegistry } from "../serializers/serializer-service.js";
|
|
40
|
+
import { createFileWatcher, loadData, saveData, } from "../storage/persistence-effect.js";
|
|
41
|
+
import { StorageAdapter } from "../storage/storage-service.js";
|
|
42
|
+
import { $transaction as $transactionImpl } from "../transactions/transaction.js";
|
|
43
|
+
import { isGroupedAggregateConfig, } from "../types/aggregate-types.js";
|
|
44
|
+
/**
|
|
45
|
+
* Attach a lazy `runPromise` getter to an Effect value.
|
|
46
|
+
* The effect is only executed when `.runPromise` is accessed.
|
|
47
|
+
*/
|
|
48
|
+
const withRunPromise = (effect) => {
|
|
49
|
+
let cached;
|
|
50
|
+
Object.defineProperty(effect, "runPromise", {
|
|
51
|
+
get() {
|
|
52
|
+
if (cached === undefined) {
|
|
53
|
+
cached = Effect.runPromise(effect);
|
|
54
|
+
}
|
|
55
|
+
return cached;
|
|
56
|
+
},
|
|
57
|
+
enumerable: false,
|
|
58
|
+
configurable: true,
|
|
59
|
+
});
|
|
60
|
+
return effect;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Attach a lazy `runPromise` getter to a Stream value.
|
|
64
|
+
* The stream is collected into an array when `.runPromise` is accessed.
|
|
65
|
+
*/
|
|
66
|
+
const withStreamRunPromise = (stream) => {
|
|
67
|
+
let cached;
|
|
68
|
+
Object.defineProperty(stream, "runPromise", {
|
|
69
|
+
get() {
|
|
70
|
+
if (cached === undefined) {
|
|
71
|
+
cached = Effect.runPromise(Stream.runCollect(stream).pipe(Effect.map(Chunk.toReadonlyArray)));
|
|
72
|
+
}
|
|
73
|
+
return cached;
|
|
74
|
+
},
|
|
75
|
+
enumerable: false,
|
|
76
|
+
configurable: true,
|
|
77
|
+
});
|
|
78
|
+
return stream;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Attach a lazy `runPromise` getter to an Effect returning CursorPageResult.
|
|
82
|
+
* The effect is only executed when `.runPromise` is accessed.
|
|
83
|
+
*/
|
|
84
|
+
const withCursorRunPromise = (effect) => {
|
|
85
|
+
let cached;
|
|
86
|
+
Object.defineProperty(effect, "runPromise", {
|
|
87
|
+
get() {
|
|
88
|
+
if (cached === undefined) {
|
|
89
|
+
cached = Effect.runPromise(effect);
|
|
90
|
+
}
|
|
91
|
+
return cached;
|
|
92
|
+
},
|
|
93
|
+
enumerable: false,
|
|
94
|
+
configurable: true,
|
|
95
|
+
});
|
|
96
|
+
return effect;
|
|
97
|
+
};
|
|
98
|
+
const createPersistenceTrigger = (delayMs, makeSaveEffect) => {
|
|
99
|
+
const pendingTimers = new Map();
|
|
100
|
+
const executeSave = (key) => Effect.runPromise(makeSaveEffect(key).pipe(Effect.catchAll(() => Effect.void)));
|
|
101
|
+
const schedule = (key) => {
|
|
102
|
+
// Cancel existing timer for this key
|
|
103
|
+
const existing = pendingTimers.get(key);
|
|
104
|
+
if (existing !== undefined) {
|
|
105
|
+
clearTimeout(existing);
|
|
106
|
+
}
|
|
107
|
+
// Schedule new debounced write
|
|
108
|
+
const timer = setTimeout(() => {
|
|
109
|
+
pendingTimers.delete(key);
|
|
110
|
+
executeSave(key);
|
|
111
|
+
}, delayMs);
|
|
112
|
+
pendingTimers.set(key, timer);
|
|
113
|
+
};
|
|
114
|
+
const flush = async () => {
|
|
115
|
+
// Take all pending keys, clear timers, execute saves
|
|
116
|
+
const keys = Array.from(pendingTimers.keys());
|
|
117
|
+
for (const [, timer] of pendingTimers) {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
}
|
|
120
|
+
pendingTimers.clear();
|
|
121
|
+
// Execute all saves
|
|
122
|
+
await Promise.all(keys.map((key) => executeSave(key)));
|
|
123
|
+
};
|
|
124
|
+
const pendingCount = () => pendingTimers.size;
|
|
125
|
+
const shutdown = () => {
|
|
126
|
+
for (const [, timer] of pendingTimers) {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
}
|
|
129
|
+
pendingTimers.clear();
|
|
130
|
+
};
|
|
131
|
+
return { schedule, flush, pendingCount, shutdown };
|
|
132
|
+
};
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// Extract Populate Config from Object-based Select
|
|
135
|
+
// ============================================================================
|
|
136
|
+
/**
|
|
137
|
+
* Normalize select config for lazy skip optimization.
|
|
138
|
+
* Returns the select config as a Record if it's object-based, or undefined if it's array-based.
|
|
139
|
+
* The lazy skip optimization only works with object-based select.
|
|
140
|
+
*/
|
|
141
|
+
function normalizeSelectForLazySkip(select) {
|
|
142
|
+
if (select === undefined) {
|
|
143
|
+
// undefined means select all, which includes computed fields
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
// Array.isArray works for ReadonlyArray too but TypeScript needs help narrowing
|
|
147
|
+
if (Array.isArray(select)) {
|
|
148
|
+
// Array-based select is rare and we don't optimize for it
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
// TypeScript now knows select is Record<string, unknown>
|
|
152
|
+
return select;
|
|
153
|
+
}
|
|
154
|
+
function extractPopulateFromSelect(select, relationships) {
|
|
155
|
+
const populate = {};
|
|
156
|
+
let hasPopulate = false;
|
|
157
|
+
for (const [key, value] of Object.entries(select)) {
|
|
158
|
+
if (key in relationships) {
|
|
159
|
+
if (value === true) {
|
|
160
|
+
populate[key] = true;
|
|
161
|
+
hasPopulate = true;
|
|
162
|
+
}
|
|
163
|
+
else if (typeof value === "object" &&
|
|
164
|
+
value !== null &&
|
|
165
|
+
!Array.isArray(value)) {
|
|
166
|
+
populate[key] = {
|
|
167
|
+
select: value,
|
|
168
|
+
...value,
|
|
169
|
+
};
|
|
170
|
+
hasPopulate = true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return hasPopulate ? populate : undefined;
|
|
175
|
+
}
|
|
176
|
+
const buildCollection = (collectionName, collectionConfig, ref, stateRefs, dbConfig, afterMutation, indexes, searchIndexRef, searchIndexFields, customOperators, idGeneratorMap, globalHooks, changePubSub, appendOnlyConfig) => {
|
|
177
|
+
const schema = collectionConfig.schema;
|
|
178
|
+
const relationships = collectionConfig.relationships;
|
|
179
|
+
// Merge global plugin hooks with collection-specific hooks.
|
|
180
|
+
// Global hooks run first (cross-cutting concerns), then collection hooks.
|
|
181
|
+
// Type narrowing: globalHooks is HooksConfig<Record<string, unknown>>,
|
|
182
|
+
// collectionHooks is HooksConfig<T>. mergeGlobalHooks returns HooksConfig<T>.
|
|
183
|
+
const collectionHooks = collectionConfig.hooks;
|
|
184
|
+
const hooks = mergeGlobalHooks(globalHooks, collectionHooks);
|
|
185
|
+
// Normalize unique fields constraints (default to empty array if not configured)
|
|
186
|
+
const uniqueFields = normalizeConstraints(collectionConfig.uniqueFields);
|
|
187
|
+
// Get computed fields config (undefined means no computed fields)
|
|
188
|
+
const computed = collectionConfig.computed;
|
|
189
|
+
// Get ID generator name from collection config (used with idGeneratorMap)
|
|
190
|
+
const idGeneratorName = collectionConfig.idGenerator;
|
|
191
|
+
// Build allRelationships map for delete (needs all collections' relationships)
|
|
192
|
+
const allRelationships = {};
|
|
193
|
+
for (const [name, config] of Object.entries(dbConfig)) {
|
|
194
|
+
allRelationships[name] = config.relationships;
|
|
195
|
+
}
|
|
196
|
+
// Query function: read Ref snapshot → Stream pipeline
|
|
197
|
+
// Returns RunnableStream for standard queries, RunnableCursorPage for cursor pagination
|
|
198
|
+
const queryFn = (options) => {
|
|
199
|
+
// Determine populate config: explicit populate or extract from object-based select
|
|
200
|
+
let populateConfig = options?.populate;
|
|
201
|
+
if (!populateConfig && options?.select && !Array.isArray(options.select)) {
|
|
202
|
+
populateConfig = extractPopulateFromSelect(options.select, relationships);
|
|
203
|
+
}
|
|
204
|
+
// Handle cursor pagination: validate and inject implicit sort if needed
|
|
205
|
+
const cursorConfig = options?.cursor;
|
|
206
|
+
let effectiveSort = options?.sort;
|
|
207
|
+
if (cursorConfig) {
|
|
208
|
+
const cursorKey = cursorConfig.key;
|
|
209
|
+
if (options?.sort) {
|
|
210
|
+
// Explicit sort provided: validate cursor key matches primary sort field
|
|
211
|
+
const sortKeys = Object.keys(options.sort);
|
|
212
|
+
if (sortKeys.length === 0) {
|
|
213
|
+
// Empty sort object: inject implicit ascending sort on cursor key
|
|
214
|
+
effectiveSort = { [cursorKey]: "asc" };
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
const primarySortKey = sortKeys[0];
|
|
218
|
+
if (primarySortKey !== cursorKey) {
|
|
219
|
+
// Sort mismatch: return effect that immediately fails
|
|
220
|
+
const errorEffect = Effect.fail(new ValidationError({
|
|
221
|
+
message: "Invalid cursor configuration",
|
|
222
|
+
issues: [
|
|
223
|
+
{
|
|
224
|
+
field: "cursor.key",
|
|
225
|
+
message: `cursor key '${cursorKey}' must match primary sort field '${primarySortKey}'`,
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
}));
|
|
229
|
+
return withCursorRunPromise(errorEffect);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
// No explicit sort: inject implicit ascending sort on cursor key
|
|
235
|
+
effectiveSort = { [cursorKey]: "asc" };
|
|
236
|
+
}
|
|
237
|
+
// Cursor pagination branch: populate → resolve computed → filter → sort → applyCursor → select
|
|
238
|
+
const cursorEffect = Effect.gen(function* () {
|
|
239
|
+
const map = yield* Ref.get(ref);
|
|
240
|
+
// Try index-accelerated lookup first (equality index, then search index)
|
|
241
|
+
let narrowed = indexes
|
|
242
|
+
? yield* resolveWithIndex(options?.where, indexes, map)
|
|
243
|
+
: undefined;
|
|
244
|
+
// If equality index didn't help, try search index
|
|
245
|
+
if (narrowed === undefined && searchIndexRef) {
|
|
246
|
+
narrowed = yield* resolveWithSearchIndex(options?.where, searchIndexRef, searchIndexFields, map);
|
|
247
|
+
}
|
|
248
|
+
const items = (narrowed ?? Array.from(map.values()));
|
|
249
|
+
let s = Stream.fromIterable(items);
|
|
250
|
+
// Apply pipeline stages: populate → resolve computed (with lazy skip) → filter → sort
|
|
251
|
+
s = applyPopulate(populateConfig, stateRefs, dbConfig, collectionName)(s);
|
|
252
|
+
s = resolveComputedStreamWithLazySkip(computed, normalizeSelectForLazySkip(options?.select))(s);
|
|
253
|
+
s = applyFilter(options?.where, customOperators)(s);
|
|
254
|
+
// When $search is active, compute and attach relevance scores after filtering
|
|
255
|
+
// (even though cursor pagination uses explicit sort, scores are still computed)
|
|
256
|
+
const cursorSearchConfig = extractSearchConfig(options?.where);
|
|
257
|
+
if (cursorSearchConfig) {
|
|
258
|
+
s = attachSearchScores(cursorSearchConfig)(s);
|
|
259
|
+
}
|
|
260
|
+
s = applySort(effectiveSort)(s);
|
|
261
|
+
// Collect via applyCursor (extracts cursor values from pre-select items)
|
|
262
|
+
const cursorResult = yield* applyCursor(cursorConfig)(s);
|
|
263
|
+
// Apply select to collected items (after cursor extraction)
|
|
264
|
+
const selectedItems = applySelectToArray(cursorResult.items, options?.select);
|
|
265
|
+
// Return CursorPageResult with projected items but original cursor metadata
|
|
266
|
+
return {
|
|
267
|
+
items: selectedItems,
|
|
268
|
+
pageInfo: cursorResult.pageInfo,
|
|
269
|
+
};
|
|
270
|
+
});
|
|
271
|
+
return withCursorRunPromise(cursorEffect);
|
|
272
|
+
}
|
|
273
|
+
// Standard stream branch: populate → resolve computed → filter → sort → paginate → select
|
|
274
|
+
const stream = Stream.unwrap(Effect.gen(function* () {
|
|
275
|
+
const map = yield* Ref.get(ref);
|
|
276
|
+
// Try index-accelerated lookup first (equality index, then search index)
|
|
277
|
+
let narrowed = indexes
|
|
278
|
+
? yield* resolveWithIndex(options?.where, indexes, map)
|
|
279
|
+
: undefined;
|
|
280
|
+
// If equality index didn't help, try search index
|
|
281
|
+
if (narrowed === undefined && searchIndexRef) {
|
|
282
|
+
narrowed = yield* resolveWithSearchIndex(options?.where, searchIndexRef, searchIndexFields, map);
|
|
283
|
+
}
|
|
284
|
+
const items = (narrowed ?? Array.from(map.values()));
|
|
285
|
+
let s = Stream.fromIterable(items);
|
|
286
|
+
// Apply pipeline stages: populate → resolve computed (with lazy skip) → filter → sort → paginate → select
|
|
287
|
+
s = applyPopulate(populateConfig, stateRefs, dbConfig, collectionName)(s);
|
|
288
|
+
s = resolveComputedStreamWithLazySkip(computed, normalizeSelectForLazySkip(options?.select))(s);
|
|
289
|
+
s = applyFilter(options?.where, customOperators)(s);
|
|
290
|
+
// When $search is active, compute and attach relevance scores after filtering
|
|
291
|
+
const searchConfig = extractSearchConfig(options?.where);
|
|
292
|
+
if (searchConfig) {
|
|
293
|
+
s = attachSearchScores(searchConfig)(s);
|
|
294
|
+
}
|
|
295
|
+
// When $search is active and no explicit sort provided, use relevance sort
|
|
296
|
+
if (searchConfig && !options?.sort) {
|
|
297
|
+
s = applyRelevanceSort(searchConfig)(s);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
s = applySort(effectiveSort)(s);
|
|
301
|
+
}
|
|
302
|
+
s = applyPagination(options?.offset, options?.limit)(s);
|
|
303
|
+
s = applySelect(options?.select)(s);
|
|
304
|
+
return s;
|
|
305
|
+
}));
|
|
306
|
+
return withStreamRunPromise(stream);
|
|
307
|
+
};
|
|
308
|
+
// Helper to wrap a function so its return value gets .runPromise.
|
|
309
|
+
// When afterMutation is configured, each CRUD method triggers a
|
|
310
|
+
// persistence save schedule after the mutation succeeds (synchronous,
|
|
311
|
+
// non-blocking — the actual save runs in a debounced setTimeout).
|
|
312
|
+
const wrapEffect = (fn) => (...args) => {
|
|
313
|
+
const effect = afterMutation
|
|
314
|
+
? fn(...args).pipe(Effect.tap(() => afterMutation()))
|
|
315
|
+
: fn(...args);
|
|
316
|
+
return withRunPromise(effect);
|
|
317
|
+
};
|
|
318
|
+
// Helper to create a forbidden operation for append-only collections
|
|
319
|
+
const forbiddenOp = (opName) => (..._args) => withRunPromise(Effect.fail(new OperationError({
|
|
320
|
+
operation: opName,
|
|
321
|
+
reason: "append-only",
|
|
322
|
+
message: `Operation '${opName}' is not allowed on append-only collection '${collectionName}'`,
|
|
323
|
+
})));
|
|
324
|
+
// Wire CRUD operations with runPromise convenience
|
|
325
|
+
const rawCreate = create(collectionName, schema, relationships, ref, stateRefs, indexes, hooks, uniqueFields, computed, searchIndexRef, searchIndexFields, idGeneratorName, idGeneratorMap, changePubSub);
|
|
326
|
+
const rawCreateMany = createMany(collectionName, schema, relationships, ref, stateRefs, indexes, hooks, uniqueFields, computed, searchIndexRef, searchIndexFields, idGeneratorName, idGeneratorMap, changePubSub);
|
|
327
|
+
// For append-only: wrap create to also append each entity to the file
|
|
328
|
+
const createFn = appendOnlyConfig
|
|
329
|
+
? (...args) => {
|
|
330
|
+
const effect = rawCreate(...args).pipe(Effect.tap((entity) => appendOnlyConfig.onEntityCreated(entity)));
|
|
331
|
+
return withRunPromise(effect);
|
|
332
|
+
}
|
|
333
|
+
: wrapEffect(rawCreate);
|
|
334
|
+
const createManyFn = appendOnlyConfig
|
|
335
|
+
? (...args) => {
|
|
336
|
+
const effect = rawCreateMany(...args).pipe(Effect.tap((result) => Effect.forEach(result.created, (entity) => appendOnlyConfig.onEntityCreated(entity))));
|
|
337
|
+
return withRunPromise(effect);
|
|
338
|
+
}
|
|
339
|
+
: wrapEffect(rawCreateMany);
|
|
340
|
+
// For append-only: update/updateMany/delete/deleteMany/upsert/upsertMany are forbidden
|
|
341
|
+
const updateFn = appendOnlyConfig
|
|
342
|
+
? // biome-ignore lint/suspicious/noExplicitAny: forbidden ops return OperationError regardless of input type
|
|
343
|
+
forbiddenOp("update")
|
|
344
|
+
: wrapEffect(update(collectionName, schema, relationships, ref, stateRefs, indexes, hooks, uniqueFields, computed, searchIndexRef, searchIndexFields, changePubSub));
|
|
345
|
+
const updateManyFn = appendOnlyConfig
|
|
346
|
+
? // biome-ignore lint/suspicious/noExplicitAny: forbidden ops return OperationError regardless of input type
|
|
347
|
+
forbiddenOp("updateMany")
|
|
348
|
+
: wrapEffect(updateMany(collectionName, schema, relationships, ref, stateRefs, indexes, hooks, uniqueFields, computed, searchIndexRef, searchIndexFields, changePubSub));
|
|
349
|
+
// Check if schema defines a deletedAt field for soft delete support
|
|
350
|
+
const supportsSoftDelete = "fields" in schema &&
|
|
351
|
+
"deletedAt" in
|
|
352
|
+
schema
|
|
353
|
+
.fields;
|
|
354
|
+
const deleteFn = appendOnlyConfig
|
|
355
|
+
? // biome-ignore lint/suspicious/noExplicitAny: forbidden ops return OperationError regardless of input type
|
|
356
|
+
forbiddenOp("delete")
|
|
357
|
+
: wrapEffect(del(collectionName, allRelationships, ref, stateRefs, supportsSoftDelete, indexes, hooks, searchIndexRef, searchIndexFields, changePubSub));
|
|
358
|
+
const deleteManyFn = appendOnlyConfig
|
|
359
|
+
? // biome-ignore lint/suspicious/noExplicitAny: forbidden ops return OperationError regardless of input type
|
|
360
|
+
forbiddenOp("deleteMany")
|
|
361
|
+
: wrapEffect(deleteMany(collectionName, allRelationships, ref, stateRefs, supportsSoftDelete, indexes, hooks, searchIndexRef, searchIndexFields, changePubSub));
|
|
362
|
+
const upsertFn = appendOnlyConfig
|
|
363
|
+
? // biome-ignore lint/suspicious/noExplicitAny: forbidden ops return OperationError regardless of input type
|
|
364
|
+
forbiddenOp("upsert")
|
|
365
|
+
: wrapEffect(upsert(collectionName, schema, relationships, ref, stateRefs, indexes, hooks, uniqueFields, searchIndexRef, searchIndexFields, changePubSub));
|
|
366
|
+
const upsertManyFn = appendOnlyConfig
|
|
367
|
+
? // biome-ignore lint/suspicious/noExplicitAny: forbidden ops return OperationError regardless of input type
|
|
368
|
+
forbiddenOp("upsertMany")
|
|
369
|
+
: wrapEffect(upsertMany(collectionName, schema, relationships, ref, stateRefs, indexes, hooks, uniqueFields, searchIndexRef, searchIndexFields, changePubSub));
|
|
370
|
+
const createWithRelsFn = wrapEffect(createWithRelationships(collectionName, schema, relationships, ref, stateRefs, dbConfig, computed, changePubSub));
|
|
371
|
+
const updateWithRelsFn = appendOnlyConfig
|
|
372
|
+
? // biome-ignore lint/suspicious/noExplicitAny: forbidden ops return OperationError regardless of input type
|
|
373
|
+
forbiddenOp("updateWithRelationships")
|
|
374
|
+
: wrapEffect(updateWithRelationships(collectionName, schema, relationships, ref, stateRefs, dbConfig, computed, changePubSub));
|
|
375
|
+
const deleteWithRelsFn = appendOnlyConfig
|
|
376
|
+
? // biome-ignore lint/suspicious/noExplicitAny: forbidden ops return OperationError regardless of input type
|
|
377
|
+
forbiddenOp("deleteWithRelationships")
|
|
378
|
+
: wrapEffect(deleteWithRelationships(collectionName, relationships, ref, stateRefs, dbConfig, changePubSub));
|
|
379
|
+
const deleteManyWithRelsFn = appendOnlyConfig
|
|
380
|
+
? // biome-ignore lint/suspicious/noExplicitAny: forbidden ops return OperationError regardless of input type
|
|
381
|
+
forbiddenOp("deleteManyWithRelationships")
|
|
382
|
+
: wrapEffect(deleteManyWithRelationships(collectionName, relationships, ref, stateRefs, dbConfig, changePubSub));
|
|
383
|
+
// findById: O(1) lookup directly from the ReadonlyMap
|
|
384
|
+
const findByIdFn = (id) => {
|
|
385
|
+
const effect = Effect.gen(function* () {
|
|
386
|
+
const map = yield* Ref.get(ref);
|
|
387
|
+
const entity = map.get(id);
|
|
388
|
+
if (entity === undefined) {
|
|
389
|
+
return yield* new NotFoundError({
|
|
390
|
+
collection: collectionName,
|
|
391
|
+
id,
|
|
392
|
+
message: `Entity with id "${id}" not found in collection "${collectionName}"`,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
return entity;
|
|
396
|
+
});
|
|
397
|
+
return withRunPromise(effect);
|
|
398
|
+
};
|
|
399
|
+
// aggregate: read Ref → filter → collect → delegate to aggregate functions
|
|
400
|
+
const aggregateFn = (config) => {
|
|
401
|
+
const effect = Effect.gen(function* () {
|
|
402
|
+
// 1. Read Ref snapshot
|
|
403
|
+
const map = yield* Ref.get(ref);
|
|
404
|
+
const items = Array.from(map.values());
|
|
405
|
+
// 2. Create stream and apply filter
|
|
406
|
+
let s = Stream.fromIterable(items);
|
|
407
|
+
s = applyFilter(config.where, customOperators)(s);
|
|
408
|
+
// 3. Collect filtered entities
|
|
409
|
+
const chunk = yield* Stream.runCollect(s);
|
|
410
|
+
const entities = Chunk.toReadonlyArray(chunk);
|
|
411
|
+
// 4. Delegate to appropriate aggregate function based on groupBy presence
|
|
412
|
+
if (isGroupedAggregateConfig(config)) {
|
|
413
|
+
return computeGroupedAggregates(entities, config);
|
|
414
|
+
}
|
|
415
|
+
return computeAggregates(entities, config);
|
|
416
|
+
});
|
|
417
|
+
// Type assertion needed because TypeScript can't infer the conditional return type
|
|
418
|
+
return withRunPromise(effect);
|
|
419
|
+
};
|
|
420
|
+
// watch: create reactive subscription to query results
|
|
421
|
+
// Requires changePubSub to be available; throws if called within a transaction
|
|
422
|
+
const watchFn = (config) => {
|
|
423
|
+
if (changePubSub === undefined) {
|
|
424
|
+
// This happens when called within a transaction context
|
|
425
|
+
// Reactive queries aren't supported within transactions since transaction
|
|
426
|
+
// data is isolated and temporary (rolled back or committed atomically)
|
|
427
|
+
return Effect.die(new Error(`watch() is not supported within transactions. ` +
|
|
428
|
+
`Reactive queries can only be used on the main database collections.`));
|
|
429
|
+
}
|
|
430
|
+
return watch(changePubSub, ref, collectionName, config);
|
|
431
|
+
};
|
|
432
|
+
// watchById: create reactive subscription for a single entity
|
|
433
|
+
// Requires changePubSub to be available; throws if called within a transaction
|
|
434
|
+
const watchByIdFn = (id) => {
|
|
435
|
+
if (changePubSub === undefined) {
|
|
436
|
+
// This happens when called within a transaction context
|
|
437
|
+
// Reactive queries aren't supported within transactions since transaction
|
|
438
|
+
// data is isolated and temporary (rolled back or committed atomically)
|
|
439
|
+
return Effect.die(new Error(`watchById() is not supported within transactions. ` +
|
|
440
|
+
`Reactive queries can only be used on the main database collections.`));
|
|
441
|
+
}
|
|
442
|
+
return watchById(changePubSub, ref, collectionName, id);
|
|
443
|
+
};
|
|
444
|
+
return {
|
|
445
|
+
query: queryFn,
|
|
446
|
+
findById: findByIdFn,
|
|
447
|
+
create: createFn,
|
|
448
|
+
createMany: createManyFn,
|
|
449
|
+
update: updateFn,
|
|
450
|
+
updateMany: updateManyFn,
|
|
451
|
+
delete: deleteFn,
|
|
452
|
+
deleteMany: deleteManyFn,
|
|
453
|
+
upsert: upsertFn,
|
|
454
|
+
upsertMany: upsertManyFn,
|
|
455
|
+
createWithRelationships: createWithRelsFn,
|
|
456
|
+
updateWithRelationships: updateWithRelsFn,
|
|
457
|
+
deleteWithRelationships: deleteWithRelsFn,
|
|
458
|
+
deleteManyWithRelationships: deleteManyWithRelsFn,
|
|
459
|
+
aggregate: aggregateFn,
|
|
460
|
+
watch: watchFn,
|
|
461
|
+
watchById: watchByIdFn,
|
|
462
|
+
};
|
|
463
|
+
};
|
|
464
|
+
/**
|
|
465
|
+
* Create a `buildCollectionForTx` callback that mirrors `buildCollection` but
|
|
466
|
+
* accepts a transaction-aware `afterMutation`. The returned callback creates
|
|
467
|
+
* collection accessors that record mutations to the transaction's set instead
|
|
468
|
+
* of triggering persistence writes.
|
|
469
|
+
*
|
|
470
|
+
* Used by `createTransaction` and `$transaction` to provide collection accessors
|
|
471
|
+
* that participate in transaction semantics.
|
|
472
|
+
*
|
|
473
|
+
* @param config - The database configuration
|
|
474
|
+
* @param stateRefs - Shared state refs for cross-collection access
|
|
475
|
+
* @param typedRefs - Typed refs for each collection
|
|
476
|
+
* @param collectionIndexes - Pre-built indexes for each collection
|
|
477
|
+
* @param searchIndexRefs - Pre-built search indexes for each collection (optional)
|
|
478
|
+
* @param searchIndexFields - Fields covered by search index for each collection (optional)
|
|
479
|
+
* @param customOperators - Custom operators from plugins for query filtering (optional)
|
|
480
|
+
* @param idGeneratorMap - ID generators from plugins for custom ID generation (optional)
|
|
481
|
+
* @returns A callback matching the BuildCollectionForTx type
|
|
482
|
+
*/
|
|
483
|
+
const makeBuildCollectionForTx = (config, stateRefs, typedRefs, collectionIndexes, searchIndexRefs, searchIndexFields, customOperators, idGeneratorMap, globalHooks) => {
|
|
484
|
+
return (collectionName, addMutation) => {
|
|
485
|
+
// Transaction-aware afterMutation: records mutation instead of scheduling persistence
|
|
486
|
+
const afterMutation = () => Effect.sync(() => addMutation(collectionName));
|
|
487
|
+
// Explicitly pass undefined for changePubSub to suppress reactive events during transactions.
|
|
488
|
+
// Individual mutations within a transaction should not publish change events;
|
|
489
|
+
// events are only published after commit (see task 7.3).
|
|
490
|
+
return buildCollection(collectionName, config[collectionName], typedRefs[collectionName], stateRefs, config, afterMutation, collectionIndexes[collectionName], searchIndexRefs?.[collectionName], searchIndexFields?.[collectionName], customOperators, idGeneratorMap, globalHooks, undefined);
|
|
491
|
+
};
|
|
492
|
+
};
|
|
493
|
+
// ============================================================================
|
|
494
|
+
// Database Factory
|
|
495
|
+
// ============================================================================
|
|
496
|
+
/**
|
|
497
|
+
* Create an Effect-based in-memory database.
|
|
498
|
+
*
|
|
499
|
+
* Accepts a DatabaseConfig and optional initial data (arrays keyed by collection name).
|
|
500
|
+
* Returns an Effect that initializes Ref state for each collection and wires up
|
|
501
|
+
* the query pipeline and CRUD methods.
|
|
502
|
+
*
|
|
503
|
+
* Optionally accepts plugins that provide custom codecs, operators, ID generators,
|
|
504
|
+
* and global lifecycle hooks.
|
|
505
|
+
*
|
|
506
|
+
* Usage:
|
|
507
|
+
* ```ts
|
|
508
|
+
* const db = yield* createEffectDatabase(config, {
|
|
509
|
+
* users: [{ id: "1", name: "Alice", age: 30 }],
|
|
510
|
+
* companies: [{ id: "c1", name: "TechCorp" }],
|
|
511
|
+
* })
|
|
512
|
+
*
|
|
513
|
+
* // Query
|
|
514
|
+
* const results = yield* Stream.runCollect(db.users.query({ where: { age: { $gt: 18 } } }))
|
|
515
|
+
*
|
|
516
|
+
* // CRUD
|
|
517
|
+
* const user = yield* db.users.create({ name: "Bob", age: 25 })
|
|
518
|
+
* ```
|
|
519
|
+
*
|
|
520
|
+
* With plugins:
|
|
521
|
+
* ```ts
|
|
522
|
+
* const db = yield* createEffectDatabase(config, initialData, {
|
|
523
|
+
* plugins: [regexPlugin, snowflakeIdPlugin]
|
|
524
|
+
* })
|
|
525
|
+
* ```
|
|
526
|
+
*/
|
|
527
|
+
export const createEffectDatabase = (config, initialData, options) => Effect.gen(function* () {
|
|
528
|
+
// 0. Validate migration registries for all versioned collections at startup
|
|
529
|
+
for (const collectionName of Object.keys(config)) {
|
|
530
|
+
const collectionConfig = config[collectionName];
|
|
531
|
+
if (collectionConfig.version !== undefined) {
|
|
532
|
+
yield* validateMigrationRegistry(collectionName, collectionConfig.version, collectionConfig.migrations ?? []);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// 0b. Build plugin registry (validates plugins, merges contributions)
|
|
536
|
+
const pluginRegistry = yield* buildPluginRegistry(options?.plugins);
|
|
537
|
+
// 0c. Validate that all idGenerator references in collection configs exist
|
|
538
|
+
yield* validateIdGeneratorReferences(config, pluginRegistry.idGenerators);
|
|
539
|
+
// 0d. Run plugin initialize effects
|
|
540
|
+
for (const plugin of options?.plugins ?? []) {
|
|
541
|
+
if (plugin.initialize !== undefined) {
|
|
542
|
+
yield* plugin.initialize();
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
// 1. Create transaction lock for single-writer isolation
|
|
546
|
+
const transactionLock = yield* Ref.make(false);
|
|
547
|
+
// 2. Create Ref for each collection from initial data
|
|
548
|
+
const stateRefs = {};
|
|
549
|
+
const typedRefs = {};
|
|
550
|
+
for (const collectionName of Object.keys(config)) {
|
|
551
|
+
const items = (initialData?.[collectionName] ??
|
|
552
|
+
[]);
|
|
553
|
+
const map = new Map(items.map((item) => [item.id, item]));
|
|
554
|
+
const ref = yield* Ref.make(map);
|
|
555
|
+
stateRefs[collectionName] = ref;
|
|
556
|
+
typedRefs[collectionName] = ref;
|
|
557
|
+
}
|
|
558
|
+
// 3. Build indexes for each collection from initial data
|
|
559
|
+
const collectionIndexes = {};
|
|
560
|
+
for (const collectionName of Object.keys(config)) {
|
|
561
|
+
const collectionConfig = config[collectionName];
|
|
562
|
+
const normalizedIndexes = normalizeIndexes(collectionConfig.indexes);
|
|
563
|
+
const items = (initialData?.[collectionName] ??
|
|
564
|
+
[]);
|
|
565
|
+
const indexes = yield* buildIndexes(normalizedIndexes, items);
|
|
566
|
+
collectionIndexes[collectionName] = indexes;
|
|
567
|
+
}
|
|
568
|
+
// 3b. Build search indexes for collections that have searchIndex configured
|
|
569
|
+
const searchIndexRefs = {};
|
|
570
|
+
const searchIndexFields = {};
|
|
571
|
+
for (const collectionName of Object.keys(config)) {
|
|
572
|
+
const collectionConfig = config[collectionName];
|
|
573
|
+
if (collectionConfig.searchIndex &&
|
|
574
|
+
collectionConfig.searchIndex.length > 0) {
|
|
575
|
+
const items = (initialData?.[collectionName] ??
|
|
576
|
+
[]);
|
|
577
|
+
const searchIdx = yield* buildSearchIndex(collectionConfig.searchIndex, items);
|
|
578
|
+
searchIndexRefs[collectionName] = searchIdx;
|
|
579
|
+
searchIndexFields[collectionName] = collectionConfig.searchIndex;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
// 4. Create shared PubSub for reactive change notifications (one per database)
|
|
583
|
+
// This PubSub is shared by all collections and broadcasts ChangeEvents to reactive subscribers
|
|
584
|
+
const changePubSub = yield* PubSub.unbounded();
|
|
585
|
+
// 5. Build each collection with its Ref, indexes, and shared state refs
|
|
586
|
+
// Pass plugin registry data: custom operators, ID generators, and global hooks
|
|
587
|
+
// Pass the shared changePubSub so CRUD operations publish ChangeEvents
|
|
588
|
+
const collections = {};
|
|
589
|
+
for (const collectionName of Object.keys(config)) {
|
|
590
|
+
collections[collectionName] = buildCollection(collectionName, config[collectionName], typedRefs[collectionName], stateRefs, config, undefined, // afterMutation
|
|
591
|
+
collectionIndexes[collectionName], searchIndexRefs[collectionName], searchIndexFields[collectionName], pluginRegistry.operators, pluginRegistry.idGenerators, pluginRegistry.globalHooks, changePubSub);
|
|
592
|
+
}
|
|
593
|
+
// 5. Build transaction support
|
|
594
|
+
const buildCollectionForTx = makeBuildCollectionForTx(config, stateRefs, typedRefs, collectionIndexes, searchIndexRefs, searchIndexFields, pluginRegistry.operators, pluginRegistry.idGenerators, pluginRegistry.globalHooks);
|
|
595
|
+
// Create the $transaction method
|
|
596
|
+
const $transactionMethod = (fn) => $transactionImpl(stateRefs, transactionLock, buildCollectionForTx, undefined, // no persistence trigger for in-memory database
|
|
597
|
+
changePubSub, // shared PubSub for reactive change notifications
|
|
598
|
+
fn);
|
|
599
|
+
// Return database with $transaction method
|
|
600
|
+
return Object.assign(collections, {
|
|
601
|
+
$transaction: $transactionMethod,
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
/**
|
|
605
|
+
* Create an Effect-based in-memory database with persistence.
|
|
606
|
+
*
|
|
607
|
+
* Like `createEffectDatabase`, but additionally wires debounced persistence hooks
|
|
608
|
+
* so that each CRUD mutation triggers a fire-and-forget save to disk.
|
|
609
|
+
*
|
|
610
|
+
* Collections with a `file` field in their config are persisted. Collections
|
|
611
|
+
* without a `file` are in-memory only.
|
|
612
|
+
*
|
|
613
|
+
* Requires `StorageAdapter` and `SerializerRegistry` services in the environment.
|
|
614
|
+
*
|
|
615
|
+
* Optionally accepts plugins that provide custom codecs, operators, ID generators,
|
|
616
|
+
* and global lifecycle hooks.
|
|
617
|
+
*
|
|
618
|
+
* Usage:
|
|
619
|
+
* ```ts
|
|
620
|
+
* const db = yield* createPersistentEffectDatabase(config, initialData, { writeDebounce: 200 })
|
|
621
|
+
* // CRUD mutations now trigger debounced saves
|
|
622
|
+
* yield* db.users.create({ name: "Alice", age: 30 })
|
|
623
|
+
* // Flush all pending writes before shutdown
|
|
624
|
+
* yield* db.flush()
|
|
625
|
+
* ```
|
|
626
|
+
*
|
|
627
|
+
* With plugins:
|
|
628
|
+
* ```ts
|
|
629
|
+
* const db = yield* createPersistentEffectDatabase(config, initialData, persistenceConfig, {
|
|
630
|
+
* plugins: [regexPlugin, snowflakeIdPlugin]
|
|
631
|
+
* })
|
|
632
|
+
* ```
|
|
633
|
+
*/
|
|
634
|
+
export const createPersistentEffectDatabase = (config, initialData, persistenceConfig, options) => Effect.gen(function* () {
|
|
635
|
+
// 0. Validate migration registries for all versioned collections at startup
|
|
636
|
+
for (const collectionName of Object.keys(config)) {
|
|
637
|
+
const collectionConfig = config[collectionName];
|
|
638
|
+
if (collectionConfig.version !== undefined) {
|
|
639
|
+
yield* validateMigrationRegistry(collectionName, collectionConfig.version, collectionConfig.migrations ?? []);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
// 0b. Build plugin registry (validates plugins, merges contributions)
|
|
643
|
+
const pluginRegistry = yield* buildPluginRegistry(options?.plugins);
|
|
644
|
+
// 0c. Validate that all idGenerator references in collection configs exist
|
|
645
|
+
yield* validateIdGeneratorReferences(config, pluginRegistry.idGenerators);
|
|
646
|
+
// 0d. Run plugin initialize effects
|
|
647
|
+
for (const plugin of options?.plugins ?? []) {
|
|
648
|
+
if (plugin.initialize !== undefined) {
|
|
649
|
+
yield* plugin.initialize();
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// 0e. Register plugin shutdown effects as scope finalizers.
|
|
653
|
+
// Register BEFORE the flush finalizer so they run AFTER flush (LIFO order).
|
|
654
|
+
// Iterate in registration order so that when finalizers run in LIFO order,
|
|
655
|
+
// plugins shut down in reverse registration order (last registered = first to shut down).
|
|
656
|
+
// This matches typical dependency patterns: if plugin A depends on plugin B,
|
|
657
|
+
// B is registered first, so A (registered later) shuts down first.
|
|
658
|
+
for (const plugin of options?.plugins ?? []) {
|
|
659
|
+
if (plugin.shutdown !== undefined) {
|
|
660
|
+
// Capture shutdown function to avoid optional chaining in the finalizer
|
|
661
|
+
const shutdownFn = plugin.shutdown;
|
|
662
|
+
yield* Effect.addFinalizer(() => shutdownFn().pipe(Effect.catchAll(() => Effect.void)));
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// 1. Resolve services from the environment and capture as a Layer
|
|
666
|
+
// so save effects can be executed outside the creation runtime.
|
|
667
|
+
const storageAdapter = yield* StorageAdapter;
|
|
668
|
+
const baseSerializerRegistry = yield* SerializerRegistry;
|
|
669
|
+
// 1a. Merge plugin codecs with the base serializer registry
|
|
670
|
+
// Plugin codecs from the registry take precedence, followed by any codecs
|
|
671
|
+
// passed via _pluginCodecs (for backwards compatibility)
|
|
672
|
+
const allPluginCodecs = [
|
|
673
|
+
...pluginRegistry.codecs,
|
|
674
|
+
...(persistenceConfig?._pluginCodecs ?? []),
|
|
675
|
+
];
|
|
676
|
+
const serializerRegistry = allPluginCodecs.length > 0
|
|
677
|
+
? mergeSerializerWithPluginCodecs(baseSerializerRegistry, allPluginCodecs)
|
|
678
|
+
: baseSerializerRegistry;
|
|
679
|
+
const serviceLayer = Layer.merge(Layer.succeed(StorageAdapter, storageAdapter), Layer.succeed(SerializerRegistry, serializerRegistry));
|
|
680
|
+
// 2. Create transaction lock for single-writer isolation
|
|
681
|
+
const transactionLock = yield* Ref.make(false);
|
|
682
|
+
// 3. Load data from files for persistent collections, then merge with initialData.
|
|
683
|
+
// initialData takes precedence (allows overriding file data for testing/seeding).
|
|
684
|
+
const stateRefs = {};
|
|
685
|
+
const typedRefs = {};
|
|
686
|
+
for (const collectionName of Object.keys(config)) {
|
|
687
|
+
const collectionConfig = config[collectionName];
|
|
688
|
+
const filePath = collectionConfig.file;
|
|
689
|
+
// Load from file if configured, passing version and migrations for auto-migration
|
|
690
|
+
let loadedData = new Map();
|
|
691
|
+
if (filePath) {
|
|
692
|
+
// Only pass version options when collection is versioned
|
|
693
|
+
// Build options object conditionally to satisfy exactOptionalPropertyTypes
|
|
694
|
+
const loadOptions = collectionConfig.version !== undefined
|
|
695
|
+
? collectionConfig.migrations !== undefined
|
|
696
|
+
? {
|
|
697
|
+
version: collectionConfig.version,
|
|
698
|
+
migrations: collectionConfig.migrations,
|
|
699
|
+
collectionName,
|
|
700
|
+
}
|
|
701
|
+
: { version: collectionConfig.version, collectionName }
|
|
702
|
+
: undefined;
|
|
703
|
+
loadedData = yield* loadData(filePath, collectionConfig.schema, loadOptions);
|
|
704
|
+
}
|
|
705
|
+
// Merge with initialData (initialData takes precedence)
|
|
706
|
+
const providedItems = (initialData?.[collectionName] ??
|
|
707
|
+
[]);
|
|
708
|
+
const mergedMap = new Map(loadedData);
|
|
709
|
+
for (const item of providedItems) {
|
|
710
|
+
mergedMap.set(item.id, item);
|
|
711
|
+
}
|
|
712
|
+
const ref = yield* Ref.make(mergedMap);
|
|
713
|
+
stateRefs[collectionName] = ref;
|
|
714
|
+
typedRefs[collectionName] = ref;
|
|
715
|
+
}
|
|
716
|
+
// 4. Build indexes for each collection from loaded/merged data
|
|
717
|
+
const collectionIndexes = {};
|
|
718
|
+
for (const collectionName of Object.keys(config)) {
|
|
719
|
+
const collectionConfig = config[collectionName];
|
|
720
|
+
const normalizedIndexes = normalizeIndexes(collectionConfig.indexes);
|
|
721
|
+
// Use actual data from the Ref (loaded from file + initialData)
|
|
722
|
+
const dataMap = yield* Ref.get(typedRefs[collectionName]);
|
|
723
|
+
const items = Array.from(dataMap.values());
|
|
724
|
+
const indexes = yield* buildIndexes(normalizedIndexes, items);
|
|
725
|
+
collectionIndexes[collectionName] = indexes;
|
|
726
|
+
}
|
|
727
|
+
// 4b. Build search indexes for collections that have searchIndex configured
|
|
728
|
+
const searchIndexRefs = {};
|
|
729
|
+
const searchIndexFields = {};
|
|
730
|
+
for (const collectionName of Object.keys(config)) {
|
|
731
|
+
const collectionConfig = config[collectionName];
|
|
732
|
+
if (collectionConfig.searchIndex &&
|
|
733
|
+
collectionConfig.searchIndex.length > 0) {
|
|
734
|
+
// Use actual data from the Ref (loaded from file + initialData)
|
|
735
|
+
const dataMap = yield* Ref.get(typedRefs[collectionName]);
|
|
736
|
+
const items = Array.from(dataMap.values());
|
|
737
|
+
const searchIdx = yield* buildSearchIndex(collectionConfig.searchIndex, items);
|
|
738
|
+
searchIndexRefs[collectionName] = searchIdx;
|
|
739
|
+
searchIndexFields[collectionName] = collectionConfig.searchIndex;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
// 5. Build the save effect factory. Each save reads the Ref at execution
|
|
743
|
+
// time (capturing latest state) and writes through saveData with services.
|
|
744
|
+
const collectionFilePaths = {};
|
|
745
|
+
for (const collectionName of Object.keys(config)) {
|
|
746
|
+
const filePath = config[collectionName].file;
|
|
747
|
+
if (filePath) {
|
|
748
|
+
collectionFilePaths[collectionName] = filePath;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const makeSaveEffect = (collectionName) => {
|
|
752
|
+
const filePath = collectionFilePaths[collectionName];
|
|
753
|
+
if (!filePath)
|
|
754
|
+
return Effect.void;
|
|
755
|
+
const collectionConfig = config[collectionName];
|
|
756
|
+
return Effect.provide(Effect.gen(function* () {
|
|
757
|
+
const currentData = yield* Ref.get(typedRefs[collectionName]);
|
|
758
|
+
yield* saveData(filePath, collectionConfig.schema, currentData,
|
|
759
|
+
// Pass version option to stamp _version in output for versioned collections
|
|
760
|
+
collectionConfig.version !== undefined
|
|
761
|
+
? { version: collectionConfig.version }
|
|
762
|
+
: undefined);
|
|
763
|
+
}), serviceLayer);
|
|
764
|
+
};
|
|
765
|
+
// 6. Create the runtime-independent persistence trigger
|
|
766
|
+
const trigger = createPersistenceTrigger(persistenceConfig?.writeDebounce ?? 100, makeSaveEffect);
|
|
767
|
+
// 7. Register scope finalizer: flush pending writes and shut down timers
|
|
768
|
+
yield* Effect.addFinalizer(() => Effect.promise(() => trigger.flush()).pipe(Effect.catchAll(() => Effect.void), Effect.tap(() => Effect.sync(() => trigger.shutdown()))));
|
|
769
|
+
// 8. Create shared PubSub for reactive change notifications (one per database)
|
|
770
|
+
// This PubSub is shared by all collections and broadcasts ChangeEvents to reactive subscribers
|
|
771
|
+
const changePubSub = yield* PubSub.unbounded();
|
|
772
|
+
// 9. Build each collection with its Ref, indexes, state refs, and persistence hooks
|
|
773
|
+
// Pass the shared changePubSub so CRUD operations publish ChangeEvents
|
|
774
|
+
const collections = {};
|
|
775
|
+
for (const collectionName of Object.keys(config)) {
|
|
776
|
+
const collectionConfig = config[collectionName];
|
|
777
|
+
const filePath = collectionConfig.file;
|
|
778
|
+
const isAppendOnly = collectionConfig.appendOnly === true;
|
|
779
|
+
// For append-only collections, afterMutation is undefined (no debounced full-file save).
|
|
780
|
+
// Instead, each create appends a single JSONL line via appendOnlyConfig.
|
|
781
|
+
const afterMutation = filePath && !isAppendOnly
|
|
782
|
+
? () => Ref.get(transactionLock).pipe(Effect.flatMap((isLocked) => isLocked
|
|
783
|
+
? Effect.void // Skip persistence during transactions
|
|
784
|
+
: Effect.sync(() => trigger.schedule(collectionName))))
|
|
785
|
+
: undefined;
|
|
786
|
+
// Build appendOnlyConfig for append-only persistent collections
|
|
787
|
+
let appendOnlyConfig;
|
|
788
|
+
if (isAppendOnly && filePath) {
|
|
789
|
+
const appendSchema = collectionConfig.schema;
|
|
790
|
+
appendOnlyConfig = {
|
|
791
|
+
onEntityCreated: (entity) => Effect.provide(Effect.gen(function* () {
|
|
792
|
+
const storage = yield* StorageAdapter;
|
|
793
|
+
const encode = Schema.encode(appendSchema);
|
|
794
|
+
const encoded = yield* encode(entity).pipe(Effect.catchAll(() => Effect.succeed(entity)));
|
|
795
|
+
const line = `${JSON.stringify(encoded)}\n`;
|
|
796
|
+
yield* storage.ensureDir(filePath);
|
|
797
|
+
yield* storage.append(filePath, line);
|
|
798
|
+
}).pipe(
|
|
799
|
+
// Catch storage errors to avoid propagating them to the caller.
|
|
800
|
+
// The entity was already created in memory; storage failure is logged but not fatal.
|
|
801
|
+
Effect.catchAll(() => Effect.void)), serviceLayer),
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
collections[collectionName] = buildCollection(collectionName, collectionConfig, typedRefs[collectionName], stateRefs, config, afterMutation, collectionIndexes[collectionName], searchIndexRefs[collectionName], searchIndexFields[collectionName], pluginRegistry.operators, pluginRegistry.idGenerators, pluginRegistry.globalHooks, changePubSub, appendOnlyConfig);
|
|
805
|
+
}
|
|
806
|
+
const db = collections;
|
|
807
|
+
// 10. Create file watchers for persistent collections to detect external file changes
|
|
808
|
+
// Each watcher monitors its file and reloads data into the Ref on changes,
|
|
809
|
+
// publishing a reload event to the changePubSub for reactive query subscribers.
|
|
810
|
+
// File watching is best-effort: if the storage adapter doesn't support watching
|
|
811
|
+
// (e.g., browser adapters in test environments), the database still functions
|
|
812
|
+
// without reactive file change detection.
|
|
813
|
+
for (const collectionName of Object.keys(config)) {
|
|
814
|
+
const collectionConfig = config[collectionName];
|
|
815
|
+
const filePath = collectionConfig.file;
|
|
816
|
+
if (filePath) {
|
|
817
|
+
yield* createFileWatcher({
|
|
818
|
+
filePath,
|
|
819
|
+
schema: collectionConfig.schema,
|
|
820
|
+
ref: typedRefs[collectionName],
|
|
821
|
+
changePubSub,
|
|
822
|
+
collectionName,
|
|
823
|
+
}).pipe(Effect.catchAll(() => Effect.void));
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
// Build the $dryRunMigrations method
|
|
827
|
+
const dryRunMigrationsFn = () => {
|
|
828
|
+
const effect = Effect.provide(dryRunMigrations(config, stateRefs), serviceLayer);
|
|
829
|
+
return withRunPromise(effect);
|
|
830
|
+
};
|
|
831
|
+
// Build transaction support
|
|
832
|
+
const buildCollectionForTx = makeBuildCollectionForTx(config, stateRefs, typedRefs, collectionIndexes, searchIndexRefs, searchIndexFields, pluginRegistry.operators, pluginRegistry.idGenerators, pluginRegistry.globalHooks);
|
|
833
|
+
// Create the $transaction method with persistence trigger
|
|
834
|
+
const $transactionMethod = (fn) => $transactionImpl(stateRefs, transactionLock, buildCollectionForTx, trigger, // persistence trigger for debounced saves on commit
|
|
835
|
+
changePubSub, // shared PubSub for reactive change notifications
|
|
836
|
+
fn);
|
|
837
|
+
// Build the flush method: flushes debounced writes AND writes canonical files
|
|
838
|
+
// for append-only collections (which don't use the debounced trigger).
|
|
839
|
+
const flushAll = async () => {
|
|
840
|
+
// Flush debounced writes for non-append-only collections
|
|
841
|
+
await trigger.flush();
|
|
842
|
+
// Write canonical JSONL files for append-only collections
|
|
843
|
+
const appendOnlyFlushes = [];
|
|
844
|
+
for (const collectionName of Object.keys(config)) {
|
|
845
|
+
const cc = config[collectionName];
|
|
846
|
+
if (cc.appendOnly && cc.file) {
|
|
847
|
+
appendOnlyFlushes.push(Effect.runPromise(makeSaveEffect(collectionName).pipe(Effect.catchAll(() => Effect.void))));
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
await Promise.all(appendOnlyFlushes);
|
|
851
|
+
};
|
|
852
|
+
return Object.assign(db, {
|
|
853
|
+
flush: flushAll,
|
|
854
|
+
pendingCount: () => trigger.pendingCount(),
|
|
855
|
+
$dryRunMigrations: dryRunMigrationsFn,
|
|
856
|
+
$transaction: $transactionMethod,
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
//# sourceMappingURL=database-effect.js.map
|