@gettersethya/mira 0.1.0-alpha.2
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/src/app/app.d.ts +96 -0
- package/dist/src/app/app.d.ts.map +1 -0
- package/dist/src/app/app.js +139 -0
- package/dist/src/app/app.js.map +1 -0
- package/dist/src/app/builder.d.ts +106 -0
- package/dist/src/app/builder.d.ts.map +1 -0
- package/dist/src/app/builder.js +122 -0
- package/dist/src/app/builder.js.map +1 -0
- package/dist/src/app/index.d.ts +31 -0
- package/dist/src/app/index.d.ts.map +1 -0
- package/dist/src/app/index.js +29 -0
- package/dist/src/app/index.js.map +1 -0
- package/dist/src/app/types.d.ts +55 -0
- package/dist/src/app/types.d.ts.map +1 -0
- package/dist/src/app/types.js +2 -0
- package/dist/src/app/types.js.map +1 -0
- package/dist/src/cache/cached-collection.d.ts +30 -0
- package/dist/src/cache/cached-collection.d.ts.map +1 -0
- package/dist/src/cache/cached-collection.js +107 -0
- package/dist/src/cache/cached-collection.js.map +1 -0
- package/dist/src/cache/collection-cache.d.ts +17 -0
- package/dist/src/cache/collection-cache.d.ts.map +1 -0
- package/dist/src/cache/collection-cache.js +37 -0
- package/dist/src/cache/collection-cache.js.map +1 -0
- package/dist/src/cache/index.d.ts +4 -0
- package/dist/src/cache/index.d.ts.map +1 -0
- package/dist/src/cache/index.js +3 -0
- package/dist/src/cache/index.js.map +1 -0
- package/dist/src/cache/types.d.ts +12 -0
- package/dist/src/cache/types.d.ts.map +1 -0
- package/dist/src/cache/types.js +2 -0
- package/dist/src/cache/types.js.map +1 -0
- package/dist/src/collection-service/collection-service.d.ts +62 -0
- package/dist/src/collection-service/collection-service.d.ts.map +1 -0
- package/dist/src/collection-service/collection-service.js +267 -0
- package/dist/src/collection-service/collection-service.js.map +1 -0
- package/dist/src/collection-service/context.d.ts +17 -0
- package/dist/src/collection-service/context.d.ts.map +1 -0
- package/dist/src/collection-service/context.js +2 -0
- package/dist/src/collection-service/context.js.map +1 -0
- package/dist/src/collection-service/decode.d.ts +6 -0
- package/dist/src/collection-service/decode.d.ts.map +1 -0
- package/dist/src/collection-service/decode.js +26 -0
- package/dist/src/collection-service/decode.js.map +1 -0
- package/dist/src/collection-service/errors.d.ts +67 -0
- package/dist/src/collection-service/errors.d.ts.map +1 -0
- package/dist/src/collection-service/errors.js +43 -0
- package/dist/src/collection-service/errors.js.map +1 -0
- package/dist/src/collection-service/index.d.ts +5 -0
- package/dist/src/collection-service/index.d.ts.map +1 -0
- package/dist/src/collection-service/index.js +3 -0
- package/dist/src/collection-service/index.js.map +1 -0
- package/dist/src/collection-service/input-schema.d.ts +10 -0
- package/dist/src/collection-service/input-schema.d.ts.map +1 -0
- package/dist/src/collection-service/input-schema.js +109 -0
- package/dist/src/collection-service/input-schema.js.map +1 -0
- package/dist/src/collection-service/where.d.ts +35 -0
- package/dist/src/collection-service/where.d.ts.map +1 -0
- package/dist/src/collection-service/where.js +88 -0
- package/dist/src/collection-service/where.js.map +1 -0
- package/dist/src/config/config.d.ts +44 -0
- package/dist/src/config/config.d.ts.map +1 -0
- package/dist/src/config/config.js +89 -0
- package/dist/src/config/config.js.map +1 -0
- package/dist/src/config/index.d.ts +3 -0
- package/dist/src/config/index.d.ts.map +1 -0
- package/dist/src/config/index.js +2 -0
- package/dist/src/config/index.js.map +1 -0
- package/dist/src/config/types.d.ts +29 -0
- package/dist/src/config/types.d.ts.map +1 -0
- package/dist/src/config/types.js +2 -0
- package/dist/src/config/types.js.map +1 -0
- package/dist/src/crypto/crypto.d.ts +11 -0
- package/dist/src/crypto/crypto.d.ts.map +1 -0
- package/dist/src/crypto/crypto.js +4 -0
- package/dist/src/crypto/crypto.js.map +1 -0
- package/dist/src/crypto/index.d.ts +3 -0
- package/dist/src/crypto/index.d.ts.map +1 -0
- package/dist/src/crypto/index.js +3 -0
- package/dist/src/crypto/index.js.map +1 -0
- package/dist/src/crypto/node.d.ts +4 -0
- package/dist/src/crypto/node.d.ts.map +1 -0
- package/dist/src/crypto/node.js +9 -0
- package/dist/src/crypto/node.js.map +1 -0
- package/dist/src/databases/index.d.ts +2 -0
- package/dist/src/databases/index.d.ts.map +1 -0
- package/dist/src/databases/index.js +2 -0
- package/dist/src/databases/index.js.map +1 -0
- package/dist/src/databases/sqlite.d.ts +24 -0
- package/dist/src/databases/sqlite.d.ts.map +1 -0
- package/dist/src/databases/sqlite.js +27 -0
- package/dist/src/databases/sqlite.js.map +1 -0
- package/dist/src/http/auth-node.d.ts +4 -0
- package/dist/src/http/auth-node.d.ts.map +1 -0
- package/dist/src/http/auth-node.js +34 -0
- package/dist/src/http/auth-node.js.map +1 -0
- package/dist/src/http/auth.d.ts +38 -0
- package/dist/src/http/auth.d.ts.map +1 -0
- package/dist/src/http/auth.js +66 -0
- package/dist/src/http/auth.js.map +1 -0
- package/dist/src/http/errors.d.ts +31 -0
- package/dist/src/http/errors.d.ts.map +1 -0
- package/dist/src/http/errors.js +36 -0
- package/dist/src/http/errors.js.map +1 -0
- package/dist/src/http/file-serve.d.ts +10 -0
- package/dist/src/http/file-serve.d.ts.map +1 -0
- package/dist/src/http/file-serve.js +159 -0
- package/dist/src/http/file-serve.js.map +1 -0
- package/dist/src/http/file-token.d.ts +7 -0
- package/dist/src/http/file-token.d.ts.map +1 -0
- package/dist/src/http/file-token.js +55 -0
- package/dist/src/http/file-token.js.map +1 -0
- package/dist/src/http/files.d.ts +11 -0
- package/dist/src/http/files.d.ts.map +1 -0
- package/dist/src/http/files.js +85 -0
- package/dist/src/http/files.js.map +1 -0
- package/dist/src/http/index.d.ts +10 -0
- package/dist/src/http/index.d.ts.map +1 -0
- package/dist/src/http/index.js +8 -0
- package/dist/src/http/index.js.map +1 -0
- package/dist/src/http/ip-middleware.d.ts +4 -0
- package/dist/src/http/ip-middleware.d.ts.map +1 -0
- package/dist/src/http/ip-middleware.js +21 -0
- package/dist/src/http/ip-middleware.js.map +1 -0
- package/dist/src/http/params.d.ts +15 -0
- package/dist/src/http/params.d.ts.map +1 -0
- package/dist/src/http/params.js +48 -0
- package/dist/src/http/params.js.map +1 -0
- package/dist/src/http/router.d.ts +11 -0
- package/dist/src/http/router.d.ts.map +1 -0
- package/dist/src/http/router.js +216 -0
- package/dist/src/http/router.js.map +1 -0
- package/dist/src/http/server-factory-node.d.ts +4 -0
- package/dist/src/http/server-factory-node.d.ts.map +1 -0
- package/dist/src/http/server-factory-node.js +8 -0
- package/dist/src/http/server-factory-node.js.map +1 -0
- package/dist/src/http/server-factory.d.ts +11 -0
- package/dist/src/http/server-factory.d.ts.map +1 -0
- package/dist/src/http/server-factory.js +4 -0
- package/dist/src/http/server-factory.js.map +1 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +10 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/migrator/dialect-sqlite.d.ts +9 -0
- package/dist/src/migrator/dialect-sqlite.d.ts.map +1 -0
- package/dist/src/migrator/dialect-sqlite.js +102 -0
- package/dist/src/migrator/dialect-sqlite.js.map +1 -0
- package/dist/src/migrator/dialect.d.ts +24 -0
- package/dist/src/migrator/dialect.d.ts.map +1 -0
- package/dist/src/migrator/dialect.js +5 -0
- package/dist/src/migrator/dialect.js.map +1 -0
- package/dist/src/migrator/index.d.ts +8 -0
- package/dist/src/migrator/index.d.ts.map +1 -0
- package/dist/src/migrator/index.js +6 -0
- package/dist/src/migrator/index.js.map +1 -0
- package/dist/src/migrator/migrator.d.ts +36 -0
- package/dist/src/migrator/migrator.d.ts.map +1 -0
- package/dist/src/migrator/migrator.js +200 -0
- package/dist/src/migrator/migrator.js.map +1 -0
- package/dist/src/migrator/schema-diff.d.ts +25 -0
- package/dist/src/migrator/schema-diff.d.ts.map +1 -0
- package/dist/src/migrator/schema-diff.js +157 -0
- package/dist/src/migrator/schema-diff.js.map +1 -0
- package/dist/src/migrator/types.d.ts +91 -0
- package/dist/src/migrator/types.d.ts.map +1 -0
- package/dist/src/migrator/types.js +13 -0
- package/dist/src/migrator/types.js.map +1 -0
- package/dist/src/platforms/node.d.ts +40 -0
- package/dist/src/platforms/node.d.ts.map +1 -0
- package/dist/src/platforms/node.js +45 -0
- package/dist/src/platforms/node.js.map +1 -0
- package/dist/src/repository/id.d.ts +5 -0
- package/dist/src/repository/id.d.ts.map +1 -0
- package/dist/src/repository/id.js +9 -0
- package/dist/src/repository/id.js.map +1 -0
- package/dist/src/repository/index.d.ts +3 -0
- package/dist/src/repository/index.d.ts.map +1 -0
- package/dist/src/repository/index.js +2 -0
- package/dist/src/repository/index.js.map +1 -0
- package/dist/src/repository/repository.d.ts +53 -0
- package/dist/src/repository/repository.d.ts.map +1 -0
- package/dist/src/repository/repository.js +150 -0
- package/dist/src/repository/repository.js.map +1 -0
- package/dist/src/repository/types.d.ts +42 -0
- package/dist/src/repository/types.d.ts.map +1 -0
- package/dist/src/repository/types.js +2 -0
- package/dist/src/repository/types.js.map +1 -0
- package/dist/src/rule/compiler.d.ts +52 -0
- package/dist/src/rule/compiler.d.ts.map +1 -0
- package/dist/src/rule/compiler.js +113 -0
- package/dist/src/rule/compiler.js.map +1 -0
- package/dist/src/rule/enforcer.d.ts +47 -0
- package/dist/src/rule/enforcer.d.ts.map +1 -0
- package/dist/src/rule/enforcer.js +60 -0
- package/dist/src/rule/enforcer.js.map +1 -0
- package/dist/src/rule/index.d.ts +3 -0
- package/dist/src/rule/index.d.ts.map +1 -0
- package/dist/src/rule/index.js +3 -0
- package/dist/src/rule/index.js.map +1 -0
- package/dist/src/storage/index.d.ts +3 -0
- package/dist/src/storage/index.d.ts.map +1 -0
- package/dist/src/storage/index.js +3 -0
- package/dist/src/storage/index.js.map +1 -0
- package/dist/src/storage/local-preset.d.ts +21 -0
- package/dist/src/storage/local-preset.d.ts.map +1 -0
- package/dist/src/storage/local-preset.js +21 -0
- package/dist/src/storage/local-preset.js.map +1 -0
- package/dist/src/storage/storage.d.ts +83 -0
- package/dist/src/storage/storage.d.ts.map +1 -0
- package/dist/src/storage/storage.js +120 -0
- package/dist/src/storage/storage.js.map +1 -0
- package/dist/src/telemetry/index.d.ts +6 -0
- package/dist/src/telemetry/index.d.ts.map +1 -0
- package/dist/src/telemetry/index.js +7 -0
- package/dist/src/telemetry/index.js.map +1 -0
- package/dist/src/telemetry/logger.d.ts +4 -0
- package/dist/src/telemetry/logger.d.ts.map +1 -0
- package/dist/src/telemetry/logger.js +27 -0
- package/dist/src/telemetry/logger.js.map +1 -0
- package/dist/src/telemetry/tracer.d.ts +16 -0
- package/dist/src/telemetry/tracer.d.ts.map +1 -0
- package/dist/src/telemetry/tracer.js +106 -0
- package/dist/src/telemetry/tracer.js.map +1 -0
- package/dist/src/thumbnail/index.d.ts +4 -0
- package/dist/src/thumbnail/index.d.ts.map +1 -0
- package/dist/src/thumbnail/index.js +3 -0
- package/dist/src/thumbnail/index.js.map +1 -0
- package/dist/src/thumbnail/photon.d.ts +9 -0
- package/dist/src/thumbnail/photon.d.ts.map +1 -0
- package/dist/src/thumbnail/photon.js +73 -0
- package/dist/src/thumbnail/photon.js.map +1 -0
- package/dist/src/thumbnail/types.d.ts +23 -0
- package/dist/src/thumbnail/types.d.ts.map +1 -0
- package/dist/src/thumbnail/types.js +22 -0
- package/dist/src/thumbnail/types.js.map +1 -0
- package/dist/tests/app/app.test.d.ts +2 -0
- package/dist/tests/app/app.test.d.ts.map +1 -0
- package/dist/tests/app/app.test.js +78 -0
- package/dist/tests/app/app.test.js.map +1 -0
- package/dist/tests/app/builder.test.d.ts +2 -0
- package/dist/tests/app/builder.test.d.ts.map +1 -0
- package/dist/tests/app/builder.test.js +69 -0
- package/dist/tests/app/builder.test.js.map +1 -0
- package/dist/tests/cache/cached-collection.test.d.ts +2 -0
- package/dist/tests/cache/cached-collection.test.d.ts.map +1 -0
- package/dist/tests/cache/cached-collection.test.js +244 -0
- package/dist/tests/cache/cached-collection.test.js.map +1 -0
- package/dist/tests/cache/collection-cache.test.d.ts +2 -0
- package/dist/tests/cache/collection-cache.test.d.ts.map +1 -0
- package/dist/tests/cache/collection-cache.test.js +73 -0
- package/dist/tests/cache/collection-cache.test.js.map +1 -0
- package/dist/tests/collection-service/collection-service.test.d.ts +2 -0
- package/dist/tests/collection-service/collection-service.test.d.ts.map +1 -0
- package/dist/tests/collection-service/collection-service.test.js +805 -0
- package/dist/tests/collection-service/collection-service.test.js.map +1 -0
- package/dist/tests/collection-service/input-schema.test.d.ts +2 -0
- package/dist/tests/collection-service/input-schema.test.d.ts.map +1 -0
- package/dist/tests/collection-service/input-schema.test.js +260 -0
- package/dist/tests/collection-service/input-schema.test.js.map +1 -0
- package/dist/tests/config/config.test.d.ts +2 -0
- package/dist/tests/config/config.test.d.ts.map +1 -0
- package/dist/tests/config/config.test.js +84 -0
- package/dist/tests/config/config.test.js.map +1 -0
- package/dist/tests/crypto/crypto.test.d.ts +2 -0
- package/dist/tests/crypto/crypto.test.d.ts.map +1 -0
- package/dist/tests/crypto/crypto.test.js +56 -0
- package/dist/tests/crypto/crypto.test.js.map +1 -0
- package/dist/tests/http/file-serve.test.d.ts +2 -0
- package/dist/tests/http/file-serve.test.d.ts.map +1 -0
- package/dist/tests/http/file-serve.test.js +210 -0
- package/dist/tests/http/file-serve.test.js.map +1 -0
- package/dist/tests/http/file-token.test.d.ts +2 -0
- package/dist/tests/http/file-token.test.d.ts.map +1 -0
- package/dist/tests/http/file-token.test.js +155 -0
- package/dist/tests/http/file-token.test.js.map +1 -0
- package/dist/tests/http/ip-middleware.test.d.ts +2 -0
- package/dist/tests/http/ip-middleware.test.d.ts.map +1 -0
- package/dist/tests/http/ip-middleware.test.js +58 -0
- package/dist/tests/http/ip-middleware.test.js.map +1 -0
- package/dist/tests/http/router.test.d.ts +2 -0
- package/dist/tests/http/router.test.d.ts.map +1 -0
- package/dist/tests/http/router.test.js +252 -0
- package/dist/tests/http/router.test.js.map +1 -0
- package/dist/tests/migrator/dialect-sqlite.test.d.ts +2 -0
- package/dist/tests/migrator/dialect-sqlite.test.d.ts.map +1 -0
- package/dist/tests/migrator/dialect-sqlite.test.js +90 -0
- package/dist/tests/migrator/dialect-sqlite.test.js.map +1 -0
- package/dist/tests/migrator/integration.test.d.ts +2 -0
- package/dist/tests/migrator/integration.test.d.ts.map +1 -0
- package/dist/tests/migrator/integration.test.js +180 -0
- package/dist/tests/migrator/integration.test.js.map +1 -0
- package/dist/tests/migrator/migrator.test.d.ts +2 -0
- package/dist/tests/migrator/migrator.test.d.ts.map +1 -0
- package/dist/tests/migrator/migrator.test.js +225 -0
- package/dist/tests/migrator/migrator.test.js.map +1 -0
- package/dist/tests/migrator/schema-diff.test.d.ts +2 -0
- package/dist/tests/migrator/schema-diff.test.d.ts.map +1 -0
- package/dist/tests/migrator/schema-diff.test.js +323 -0
- package/dist/tests/migrator/schema-diff.test.js.map +1 -0
- package/dist/tests/repository/repository.test.d.ts +2 -0
- package/dist/tests/repository/repository.test.d.ts.map +1 -0
- package/dist/tests/repository/repository.test.js +255 -0
- package/dist/tests/repository/repository.test.js.map +1 -0
- package/dist/tests/rule/compiler.test.d.ts +2 -0
- package/dist/tests/rule/compiler.test.d.ts.map +1 -0
- package/dist/tests/rule/compiler.test.js +226 -0
- package/dist/tests/rule/compiler.test.js.map +1 -0
- package/dist/tests/rule/enforcer.test.d.ts +2 -0
- package/dist/tests/rule/enforcer.test.d.ts.map +1 -0
- package/dist/tests/rule/enforcer.test.js +190 -0
- package/dist/tests/rule/enforcer.test.js.map +1 -0
- package/dist/tests/storage/storage.test.d.ts +2 -0
- package/dist/tests/storage/storage.test.d.ts.map +1 -0
- package/dist/tests/storage/storage.test.js +76 -0
- package/dist/tests/storage/storage.test.js.map +1 -0
- package/dist/tests/telemetry/instrumentation.test.d.ts +2 -0
- package/dist/tests/telemetry/instrumentation.test.d.ts.map +1 -0
- package/dist/tests/telemetry/instrumentation.test.js +226 -0
- package/dist/tests/telemetry/instrumentation.test.js.map +1 -0
- package/dist/tests/telemetry/logger.test.d.ts +2 -0
- package/dist/tests/telemetry/logger.test.d.ts.map +1 -0
- package/dist/tests/telemetry/logger.test.js +56 -0
- package/dist/tests/telemetry/logger.test.js.map +1 -0
- package/dist/tests/telemetry/tracer.test.d.ts +2 -0
- package/dist/tests/telemetry/tracer.test.d.ts.map +1 -0
- package/dist/tests/telemetry/tracer.test.js +63 -0
- package/dist/tests/telemetry/tracer.test.js.map +1 -0
- package/dist/tests/thumbnail/photon.test.d.ts +2 -0
- package/dist/tests/thumbnail/photon.test.d.ts.map +1 -0
- package/dist/tests/thumbnail/photon.test.js +478 -0
- package/dist/tests/thumbnail/photon.test.js.map +1 -0
- package/dist/tests/thumbnail/thumbnail.test.d.ts +2 -0
- package/dist/tests/thumbnail/thumbnail.test.d.ts.map +1 -0
- package/dist/tests/thumbnail/thumbnail.test.js +35 -0
- package/dist/tests/thumbnail/thumbnail.test.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
import { SqlClient } from "@effect/sql";
|
|
2
|
+
import { SqliteClient } from "@effect/sql-sqlite-node";
|
|
3
|
+
import { Effect, Either, Layer, Match } from "effect";
|
|
4
|
+
import { describe, it } from "@effect/vitest";
|
|
5
|
+
import { expect } from "vitest";
|
|
6
|
+
import { BaseCollection } from "@gettersethya/mira-client";
|
|
7
|
+
import { Field } from "@gettersethya/mira-client";
|
|
8
|
+
import { ViewCollection } from "@gettersethya/mira-client";
|
|
9
|
+
import { RepositoryLive } from "../../src/repository/repository.js";
|
|
10
|
+
import { CollectionService, makeCollectionServiceLayer } from "../../src/collection-service/collection-service.js";
|
|
11
|
+
import { FileStorage, FileStorageNotFound } from "../../src/storage/storage.js";
|
|
12
|
+
import { NodeCryptoLayer } from "../../src/crypto/node.js";
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Test layer
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
const FileStorageTest = Layer.succeed(FileStorage, FileStorage.of({
|
|
17
|
+
upload: (key) => Effect.succeed(key),
|
|
18
|
+
delete: () => Effect.void,
|
|
19
|
+
url: (key) => `/files/${key}`,
|
|
20
|
+
read: (key) => Effect.fail(new FileStorageNotFound({ key })),
|
|
21
|
+
exists: () => Effect.succeed(false),
|
|
22
|
+
list: () => Effect.succeed([])
|
|
23
|
+
}));
|
|
24
|
+
const sqliteLayer = SqliteClient.layer({ filename: ":memory:" });
|
|
25
|
+
const collectionServiceWithDeps = makeCollectionServiceLayer([]).pipe(Layer.provide(RepositoryLive), Layer.provide(sqliteLayer), Layer.provide(FileStorageTest), Layer.provide(NodeCryptoLayer));
|
|
26
|
+
const testLayer = Layer.mergeAll(collectionServiceWithDeps, sqliteLayer, FileStorageTest, NodeCryptoLayer);
|
|
27
|
+
const noCtx = { headers: {}, query: {} };
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Table setup helpers
|
|
30
|
+
// seqId as INTEGER PRIMARY KEY AUTOINCREMENT so SQLite fills it on INSERT.
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
const setupPostsTable = Effect.gen(function* () {
|
|
33
|
+
const sql = yield* SqlClient.SqlClient;
|
|
34
|
+
yield* sql.unsafe(`
|
|
35
|
+
CREATE TABLE IF NOT EXISTS "posts" (
|
|
36
|
+
"seqId" INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
37
|
+
"id" TEXT NOT NULL UNIQUE,
|
|
38
|
+
"title" TEXT NOT NULL,
|
|
39
|
+
"published" INTEGER NOT NULL DEFAULT 0,
|
|
40
|
+
"authorId" TEXT,
|
|
41
|
+
"created" TEXT NOT NULL,
|
|
42
|
+
"updated" TEXT NOT NULL
|
|
43
|
+
)
|
|
44
|
+
`);
|
|
45
|
+
yield* sql.unsafe(`DELETE FROM "posts"`);
|
|
46
|
+
});
|
|
47
|
+
// View collection tests: a base table + a SQLite VIEW over it
|
|
48
|
+
const setupViewTables = Effect.gen(function* () {
|
|
49
|
+
const sql = yield* SqlClient.SqlClient;
|
|
50
|
+
yield* sql.unsafe(`
|
|
51
|
+
CREATE TABLE IF NOT EXISTS "posts_base" (
|
|
52
|
+
"seqId" INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
53
|
+
"id" TEXT NOT NULL UNIQUE,
|
|
54
|
+
"title" TEXT NOT NULL,
|
|
55
|
+
"status" TEXT NOT NULL DEFAULT 'draft',
|
|
56
|
+
"created" TEXT NOT NULL,
|
|
57
|
+
"updated" TEXT NOT NULL
|
|
58
|
+
)
|
|
59
|
+
`);
|
|
60
|
+
yield* sql.unsafe(`DELETE FROM "posts_base"`);
|
|
61
|
+
yield* sql.unsafe(`DROP VIEW IF EXISTS "active_posts"`);
|
|
62
|
+
yield* sql.unsafe(`
|
|
63
|
+
CREATE VIEW "active_posts" AS
|
|
64
|
+
SELECT seqId, id, title, status, created, updated FROM posts_base WHERE status = 'active'
|
|
65
|
+
`);
|
|
66
|
+
});
|
|
67
|
+
const setupScoredTable = Effect.gen(function* () {
|
|
68
|
+
const sql = yield* SqlClient.SqlClient;
|
|
69
|
+
yield* sql.unsafe(`
|
|
70
|
+
CREATE TABLE IF NOT EXISTS "scored" (
|
|
71
|
+
"seqId" INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
72
|
+
"id" TEXT NOT NULL UNIQUE,
|
|
73
|
+
"name" TEXT NOT NULL,
|
|
74
|
+
"score" REAL NOT NULL,
|
|
75
|
+
"created" TEXT NOT NULL,
|
|
76
|
+
"updated" TEXT NOT NULL
|
|
77
|
+
)
|
|
78
|
+
`);
|
|
79
|
+
yield* sql.unsafe(`DELETE FROM "scored"`);
|
|
80
|
+
});
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Collection definitions
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Base collection — all operations public
|
|
85
|
+
const Posts = BaseCollection.define("posts", {
|
|
86
|
+
title: Field.text({ maxLength: 100 }),
|
|
87
|
+
published: Field.boolean({ default: false }),
|
|
88
|
+
authorId: Field.text({ required: false })
|
|
89
|
+
}).rules((R) => ({
|
|
90
|
+
list: R.public(),
|
|
91
|
+
view: R.public(),
|
|
92
|
+
create: R.public(),
|
|
93
|
+
update: R.public(),
|
|
94
|
+
delete: R.public()
|
|
95
|
+
}));
|
|
96
|
+
// Base collection — rule-enforced: only authorId "user1" can access
|
|
97
|
+
const ProtectedPosts = BaseCollection.define("posts", {
|
|
98
|
+
title: Field.text(),
|
|
99
|
+
published: Field.boolean({ default: false }),
|
|
100
|
+
authorId: Field.text({ required: false })
|
|
101
|
+
}).rules((R) => ({
|
|
102
|
+
list: R.field("authorId").eq(R.literal("user1")),
|
|
103
|
+
view: R.field("authorId").eq(R.literal("user1")),
|
|
104
|
+
create: R.field("authorId").eq(R.literal("user1")),
|
|
105
|
+
update: R.field("authorId").eq(R.literal("user1")),
|
|
106
|
+
delete: R.field("authorId").eq(R.literal("user1"))
|
|
107
|
+
}));
|
|
108
|
+
// Base collection — no rules (deny all by default)
|
|
109
|
+
const NoRulePosts = BaseCollection.define("posts", {
|
|
110
|
+
title: Field.text()
|
|
111
|
+
});
|
|
112
|
+
// View collection — public list/view, no create/update/delete
|
|
113
|
+
const ActivePosts = ViewCollection.define("active_posts", "SELECT seqId, id, title, status, created, updated FROM posts_base WHERE status = 'active'", {
|
|
114
|
+
seqId: Field.integer().view(),
|
|
115
|
+
id: Field.text().view(),
|
|
116
|
+
title: Field.text().view(),
|
|
117
|
+
status: Field.text().view()
|
|
118
|
+
}).rules((R) => ({
|
|
119
|
+
list: R.public(),
|
|
120
|
+
view: R.public()
|
|
121
|
+
}));
|
|
122
|
+
// Collection with error callbacks for integration testing
|
|
123
|
+
const Scored = BaseCollection.define("scored", {
|
|
124
|
+
name: Field.text({
|
|
125
|
+
minLength: 2,
|
|
126
|
+
error: (kind) => Match.value(kind).pipe(Match.when("minLength", () => "Name too short"), Match.when("required", () => "Name is required"), Match.orElse(() => "Invalid name"))
|
|
127
|
+
}),
|
|
128
|
+
score: Field.number({
|
|
129
|
+
min: 0,
|
|
130
|
+
max: 100,
|
|
131
|
+
error: (kind) => Match.value(kind).pipe(Match.when("minimum", () => "Score below zero"), Match.when("maximum", () => "Score above 100"), Match.orElse(() => "Invalid score"))
|
|
132
|
+
})
|
|
133
|
+
}).rules((R) => ({
|
|
134
|
+
create: R.public(),
|
|
135
|
+
update: R.public(),
|
|
136
|
+
list: R.public(),
|
|
137
|
+
view: R.public(),
|
|
138
|
+
delete: R.public()
|
|
139
|
+
}));
|
|
140
|
+
// ScoredServiceLayer wires Scored into the collection service
|
|
141
|
+
const scoredServiceWithDeps = makeCollectionServiceLayer([Scored]).pipe(Layer.provide(RepositoryLive), Layer.provide(sqliteLayer), Layer.provide(FileStorageTest), Layer.provide(NodeCryptoLayer));
|
|
142
|
+
const scoredTestLayer = Layer.mergeAll(scoredServiceWithDeps, sqliteLayer, FileStorageTest, NodeCryptoLayer);
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Helpers
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
function isCursorPage(r) {
|
|
147
|
+
return typeof r === "object" && r !== null && "nextCursor" in r;
|
|
148
|
+
}
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// list
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
describe("list", () => {
|
|
153
|
+
it.effect("public base collection returns CursorPage shape", () => Effect.gen(function* () {
|
|
154
|
+
yield* setupPostsTable;
|
|
155
|
+
const svc = yield* CollectionService;
|
|
156
|
+
const result = yield* svc.list(Posts, null, 10, noCtx);
|
|
157
|
+
expect(isCursorPage(result)).toBe(true);
|
|
158
|
+
}).pipe(Effect.provide(testLayer)));
|
|
159
|
+
it.effect("returns all inserted rows", () => Effect.gen(function* () {
|
|
160
|
+
yield* setupPostsTable;
|
|
161
|
+
const svc = yield* CollectionService;
|
|
162
|
+
yield* svc.create(Posts, { title: "A" }, noCtx);
|
|
163
|
+
yield* svc.create(Posts, { title: "B" }, noCtx);
|
|
164
|
+
const result = yield* svc.list(Posts, null, 10, noCtx);
|
|
165
|
+
expect(isCursorPage(result) && result.items.length).toBe(2);
|
|
166
|
+
}).pipe(Effect.provide(testLayer)));
|
|
167
|
+
it.effect("seqId is NOT present in list items (x-hidden stripped)", () => Effect.gen(function* () {
|
|
168
|
+
yield* setupPostsTable;
|
|
169
|
+
const svc = yield* CollectionService;
|
|
170
|
+
yield* svc.create(Posts, { title: "X" }, noCtx);
|
|
171
|
+
const result = yield* svc.list(Posts, null, 10, noCtx);
|
|
172
|
+
if (!isCursorPage(result))
|
|
173
|
+
throw new Error("expected CursorPage");
|
|
174
|
+
expect("seqId" in result.items[0]).toBe(false);
|
|
175
|
+
}).pipe(Effect.provide(testLayer)));
|
|
176
|
+
it.effect("nextCursor is null when page is not full", () => Effect.gen(function* () {
|
|
177
|
+
yield* setupPostsTable;
|
|
178
|
+
const svc = yield* CollectionService;
|
|
179
|
+
yield* svc.create(Posts, { title: "A" }, noCtx);
|
|
180
|
+
const result = yield* svc.list(Posts, null, 10, noCtx);
|
|
181
|
+
if (!isCursorPage(result))
|
|
182
|
+
throw new Error("expected CursorPage");
|
|
183
|
+
expect(result.nextCursor).toBeNull();
|
|
184
|
+
}).pipe(Effect.provide(testLayer)));
|
|
185
|
+
it.effect("nextCursor is a number when page is full", () => Effect.gen(function* () {
|
|
186
|
+
yield* setupPostsTable;
|
|
187
|
+
const svc = yield* CollectionService;
|
|
188
|
+
yield* svc.create(Posts, { title: "A" }, noCtx);
|
|
189
|
+
yield* svc.create(Posts, { title: "B" }, noCtx);
|
|
190
|
+
yield* svc.create(Posts, { title: "C" }, noCtx);
|
|
191
|
+
const result = yield* svc.list(Posts, null, 2, noCtx);
|
|
192
|
+
if (!isCursorPage(result))
|
|
193
|
+
throw new Error("expected CursorPage");
|
|
194
|
+
expect(typeof result.nextCursor).toBe("number");
|
|
195
|
+
}).pipe(Effect.provide(testLayer)));
|
|
196
|
+
it.effect("cursor pagination: page 2 fetches rows not in page 1", () => Effect.gen(function* () {
|
|
197
|
+
yield* setupPostsTable;
|
|
198
|
+
const svc = yield* CollectionService;
|
|
199
|
+
yield* svc.create(Posts, { title: "A" }, noCtx);
|
|
200
|
+
yield* svc.create(Posts, { title: "B" }, noCtx);
|
|
201
|
+
yield* svc.create(Posts, { title: "C" }, noCtx);
|
|
202
|
+
yield* svc.create(Posts, { title: "D" }, noCtx);
|
|
203
|
+
yield* svc.create(Posts, { title: "E" }, noCtx);
|
|
204
|
+
const page1 = yield* svc.list(Posts, null, 2, noCtx);
|
|
205
|
+
if (!isCursorPage(page1))
|
|
206
|
+
throw new Error("expected CursorPage");
|
|
207
|
+
expect(page1.items.length).toBe(2);
|
|
208
|
+
expect(page1.nextCursor).not.toBeNull();
|
|
209
|
+
if (page1.nextCursor === null)
|
|
210
|
+
throw new Error("expected nextCursor");
|
|
211
|
+
const page2 = yield* svc.list(Posts, page1.nextCursor, 2, noCtx);
|
|
212
|
+
if (!isCursorPage(page2))
|
|
213
|
+
throw new Error("expected CursorPage");
|
|
214
|
+
expect(page2.items.length).toBe(2);
|
|
215
|
+
if (page2.nextCursor === null)
|
|
216
|
+
throw new Error("expected nextCursor");
|
|
217
|
+
const page3 = yield* svc.list(Posts, page2.nextCursor, 2, noCtx);
|
|
218
|
+
if (!isCursorPage(page3))
|
|
219
|
+
throw new Error("expected CursorPage");
|
|
220
|
+
expect(page3.items.length).toBe(1);
|
|
221
|
+
expect(page3.nextCursor).toBeNull();
|
|
222
|
+
const ids1 = page1.items.map((r) => r["id"]);
|
|
223
|
+
const ids2 = page2.items.map((r) => r["id"]);
|
|
224
|
+
expect(ids1.some((id) => ids2.includes(id))).toBe(false);
|
|
225
|
+
}).pipe(Effect.provide(testLayer)));
|
|
226
|
+
it.effect("rule-enforced list returns only matching rows", () => Effect.gen(function* () {
|
|
227
|
+
yield* setupPostsTable;
|
|
228
|
+
const svc = yield* CollectionService;
|
|
229
|
+
yield* svc.create(Posts, { title: "Mine", authorId: "user1" }, noCtx);
|
|
230
|
+
yield* svc.create(Posts, { title: "Theirs", authorId: "user2" }, noCtx);
|
|
231
|
+
const result = yield* svc.list(ProtectedPosts, null, 10, noCtx);
|
|
232
|
+
if (!isCursorPage(result))
|
|
233
|
+
throw new Error("expected CursorPage");
|
|
234
|
+
expect(result.items.length).toBe(1);
|
|
235
|
+
expect(result.items[0]["title"]).toBe("Mine");
|
|
236
|
+
}).pipe(Effect.provide(testLayer)));
|
|
237
|
+
it.effect("no rules → ForbiddenError", () => Effect.gen(function* () {
|
|
238
|
+
yield* setupPostsTable;
|
|
239
|
+
const svc = yield* CollectionService;
|
|
240
|
+
const result = yield* svc.list(NoRulePosts, null, 10, noCtx).pipe(Effect.either);
|
|
241
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
242
|
+
if (Either.isLeft(result)) {
|
|
243
|
+
expect(result.left._tag).toBe("ForbiddenError");
|
|
244
|
+
if (result.left._tag === "ForbiddenError") {
|
|
245
|
+
expect(result.left.action).toBe("list");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}).pipe(Effect.provide(testLayer)));
|
|
249
|
+
it.effect("view collection returns CursorPage shape", () => Effect.gen(function* () {
|
|
250
|
+
yield* setupViewTables;
|
|
251
|
+
const svc = yield* CollectionService;
|
|
252
|
+
const result = yield* svc.list(ActivePosts, null, 10, noCtx);
|
|
253
|
+
expect(isCursorPage(result)).toBe(true);
|
|
254
|
+
}).pipe(Effect.provide(testLayer)));
|
|
255
|
+
it.effect("view collection list returns only rows from the view", () => Effect.gen(function* () {
|
|
256
|
+
yield* setupViewTables;
|
|
257
|
+
const sql = yield* SqlClient.SqlClient;
|
|
258
|
+
// Insert directly into posts_base since repository uses different table name
|
|
259
|
+
yield* sql.unsafe(`INSERT INTO posts_base (id, title, status, created, updated) VALUES (?, ?, ?, datetime('now'), datetime('now'))`, ["id1", "Active Post", "active"]);
|
|
260
|
+
yield* sql.unsafe(`INSERT INTO posts_base (id, title, status, created, updated) VALUES (?, ?, ?, datetime('now'), datetime('now'))`, ["id2", "Draft Post", "draft"]);
|
|
261
|
+
const svc = yield* CollectionService;
|
|
262
|
+
const result = yield* svc.list(ActivePosts, null, 10, noCtx);
|
|
263
|
+
if (!isCursorPage(result))
|
|
264
|
+
throw new Error("expected CursorPage");
|
|
265
|
+
expect(result.items.length).toBe(1);
|
|
266
|
+
expect(result.items[0]["title"]).toBe("Active Post");
|
|
267
|
+
}).pipe(Effect.provide(testLayer)));
|
|
268
|
+
});
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// list with filter
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
describe("list with filter", () => {
|
|
273
|
+
it.effect("filters by eq on string field", () => Effect.gen(function* () {
|
|
274
|
+
yield* setupPostsTable;
|
|
275
|
+
const svc = yield* CollectionService;
|
|
276
|
+
yield* svc.create(Posts, { title: "Apple" }, noCtx);
|
|
277
|
+
yield* svc.create(Posts, { title: "Banana" }, noCtx);
|
|
278
|
+
yield* svc.create(Posts, { title: "Apple Pie" }, noCtx);
|
|
279
|
+
const result = yield* svc.list(Posts, null, 10, noCtx, {
|
|
280
|
+
op: "like",
|
|
281
|
+
field: "title",
|
|
282
|
+
value: "Apple%"
|
|
283
|
+
});
|
|
284
|
+
if (!isCursorPage(result))
|
|
285
|
+
throw new Error("expected CursorPage");
|
|
286
|
+
expect(result.items.length).toBe(2);
|
|
287
|
+
}).pipe(Effect.provide(testLayer)));
|
|
288
|
+
it.effect("filters by gt on integer field", () => Effect.gen(function* () {
|
|
289
|
+
yield* setupPostsTable;
|
|
290
|
+
const svc = yield* CollectionService;
|
|
291
|
+
yield* svc.create(Posts, { title: "A", authorId: "10" }, noCtx);
|
|
292
|
+
yield* svc.create(Posts, { title: "B", authorId: "20" }, noCtx);
|
|
293
|
+
yield* svc.create(Posts, { title: "C", authorId: "30" }, noCtx);
|
|
294
|
+
// authorId is text but we can still compare with gt textually
|
|
295
|
+
const result = yield* svc.list(Posts, null, 10, noCtx, {
|
|
296
|
+
op: "gt",
|
|
297
|
+
field: "authorId",
|
|
298
|
+
value: "20"
|
|
299
|
+
});
|
|
300
|
+
if (!isCursorPage(result))
|
|
301
|
+
throw new Error("expected CursorPage");
|
|
302
|
+
expect(result.items.length).toBe(1);
|
|
303
|
+
expect(result.items[0]["title"]).toBe("C");
|
|
304
|
+
}).pipe(Effect.provide(testLayer)));
|
|
305
|
+
it.effect("and filter combines conditions", () => Effect.gen(function* () {
|
|
306
|
+
yield* setupPostsTable;
|
|
307
|
+
const svc = yield* CollectionService;
|
|
308
|
+
yield* svc.create(Posts, { title: "Alpha", authorId: "user1" }, noCtx);
|
|
309
|
+
yield* svc.create(Posts, { title: "Beta", authorId: "user2" }, noCtx);
|
|
310
|
+
yield* svc.create(Posts, { title: "Gamma", authorId: "user1" }, noCtx);
|
|
311
|
+
// Filter: title starts with "A" AND authorId = "user1"
|
|
312
|
+
// We can approximate startsWith using like
|
|
313
|
+
const result = yield* svc.list(Posts, null, 10, noCtx, {
|
|
314
|
+
op: "and",
|
|
315
|
+
left: { op: "like", field: "title", value: "A%" },
|
|
316
|
+
right: { op: "eq", field: "authorId", value: "user1" }
|
|
317
|
+
});
|
|
318
|
+
if (!isCursorPage(result))
|
|
319
|
+
throw new Error("expected CursorPage");
|
|
320
|
+
expect(result.items.length).toBe(1);
|
|
321
|
+
expect(result.items[0]["title"]).toBe("Alpha");
|
|
322
|
+
}).pipe(Effect.provide(testLayer)));
|
|
323
|
+
it.effect("filter is ANDed with rule enforcement", () => Effect.gen(function* () {
|
|
324
|
+
yield* setupPostsTable;
|
|
325
|
+
const svc = yield* CollectionService;
|
|
326
|
+
yield* svc.create(Posts, { title: "X", authorId: "user1" }, noCtx);
|
|
327
|
+
yield* svc.create(Posts, { title: "Y", authorId: "user1" }, noCtx);
|
|
328
|
+
yield* svc.create(Posts, { title: "Z", authorId: "user2" }, noCtx);
|
|
329
|
+
// ProtectedPosts rule: authorId = "user1"
|
|
330
|
+
// Filter: title like "X" → should return only "X" (which is user1's)
|
|
331
|
+
const result = yield* svc.list(ProtectedPosts, null, 10, noCtx, {
|
|
332
|
+
op: "like",
|
|
333
|
+
field: "title",
|
|
334
|
+
value: "X"
|
|
335
|
+
});
|
|
336
|
+
if (!isCursorPage(result))
|
|
337
|
+
throw new Error("expected CursorPage");
|
|
338
|
+
expect(result.items.length).toBe(1);
|
|
339
|
+
expect(result.items[0]["title"]).toBe("X");
|
|
340
|
+
}).pipe(Effect.provide(testLayer)));
|
|
341
|
+
it.effect("in filter works", () => Effect.gen(function* () {
|
|
342
|
+
yield* setupPostsTable;
|
|
343
|
+
const svc = yield* CollectionService;
|
|
344
|
+
yield* svc.create(Posts, { title: "Red" }, noCtx);
|
|
345
|
+
yield* svc.create(Posts, { title: "Green" }, noCtx);
|
|
346
|
+
yield* svc.create(Posts, { title: "Blue" }, noCtx);
|
|
347
|
+
const result = yield* svc.list(Posts, null, 10, noCtx, {
|
|
348
|
+
op: "in",
|
|
349
|
+
field: "title",
|
|
350
|
+
values: ["Red", "Blue"]
|
|
351
|
+
});
|
|
352
|
+
if (!isCursorPage(result))
|
|
353
|
+
throw new Error("expected CursorPage");
|
|
354
|
+
expect(result.items.length).toBe(2);
|
|
355
|
+
const titles = result.items.map((r) => r["title"]).sort();
|
|
356
|
+
expect(titles).toEqual(["Blue", "Red"]);
|
|
357
|
+
}).pipe(Effect.provide(testLayer)));
|
|
358
|
+
it.effect("or filter works", () => Effect.gen(function* () {
|
|
359
|
+
yield* setupPostsTable;
|
|
360
|
+
const svc = yield* CollectionService;
|
|
361
|
+
yield* svc.create(Posts, { title: "Cat", authorId: "a" }, noCtx);
|
|
362
|
+
yield* svc.create(Posts, { title: "Dog", authorId: "b" }, noCtx);
|
|
363
|
+
yield* svc.create(Posts, { title: "Fish", authorId: "c" }, noCtx);
|
|
364
|
+
// title = "Cat" OR authorId = "b"
|
|
365
|
+
const result = yield* svc.list(Posts, null, 10, noCtx, {
|
|
366
|
+
op: "or",
|
|
367
|
+
left: { op: "eq", field: "title", value: "Cat" },
|
|
368
|
+
right: { op: "eq", field: "authorId", value: "b" }
|
|
369
|
+
});
|
|
370
|
+
if (!isCursorPage(result))
|
|
371
|
+
throw new Error("expected CursorPage");
|
|
372
|
+
expect(result.items.length).toBe(2);
|
|
373
|
+
const titles = result.items.map((r) => r["title"]).sort();
|
|
374
|
+
expect(titles).toEqual(["Cat", "Dog"]);
|
|
375
|
+
}).pipe(Effect.provide(testLayer)));
|
|
376
|
+
it.effect("not filter works", () => Effect.gen(function* () {
|
|
377
|
+
yield* setupPostsTable;
|
|
378
|
+
const svc = yield* CollectionService;
|
|
379
|
+
yield* svc.create(Posts, { title: "Keep" }, noCtx);
|
|
380
|
+
yield* svc.create(Posts, { title: "Skip" }, noCtx);
|
|
381
|
+
yield* svc.create(Posts, { title: "Keep2" }, noCtx);
|
|
382
|
+
// NOT (title = "Skip")
|
|
383
|
+
const result = yield* svc.list(Posts, null, 10, noCtx, {
|
|
384
|
+
op: "not",
|
|
385
|
+
node: { op: "eq", field: "title", value: "Skip" }
|
|
386
|
+
});
|
|
387
|
+
if (!isCursorPage(result))
|
|
388
|
+
throw new Error("expected CursorPage");
|
|
389
|
+
expect(result.items.length).toBe(2);
|
|
390
|
+
const titles = result.items.map((r) => r["title"]);
|
|
391
|
+
expect(titles).not.toContain("Skip");
|
|
392
|
+
}).pipe(Effect.provide(testLayer)));
|
|
393
|
+
});
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
// view
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
describe("view", () => {
|
|
398
|
+
it.effect("returns the row for an existing id", () => Effect.gen(function* () {
|
|
399
|
+
yield* setupPostsTable;
|
|
400
|
+
const svc = yield* CollectionService;
|
|
401
|
+
const created = yield* svc.create(Posts, { title: "Hello" }, noCtx);
|
|
402
|
+
const id = created["id"];
|
|
403
|
+
if (typeof id !== "string")
|
|
404
|
+
throw new Error("expected string id");
|
|
405
|
+
const result = yield* svc.view(Posts, id, noCtx);
|
|
406
|
+
expect(result["title"]).toBe("Hello");
|
|
407
|
+
}).pipe(Effect.provide(testLayer)));
|
|
408
|
+
it.effect("returns NotFoundError for an unknown id", () => Effect.gen(function* () {
|
|
409
|
+
yield* setupPostsTable;
|
|
410
|
+
const svc = yield* CollectionService;
|
|
411
|
+
const result = yield* svc.view(Posts, "nonexistent123", noCtx).pipe(Effect.either);
|
|
412
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
413
|
+
if (Either.isLeft(result)) {
|
|
414
|
+
expect(result.left._tag).toBe("NotFoundError");
|
|
415
|
+
}
|
|
416
|
+
}).pipe(Effect.provide(testLayer)));
|
|
417
|
+
it.effect("seqId is NOT in the returned row", () => Effect.gen(function* () {
|
|
418
|
+
yield* setupPostsTable;
|
|
419
|
+
const svc = yield* CollectionService;
|
|
420
|
+
const created = yield* svc.create(Posts, { title: "Hidden" }, noCtx);
|
|
421
|
+
const id = created["id"];
|
|
422
|
+
if (typeof id !== "string")
|
|
423
|
+
throw new Error("expected string id");
|
|
424
|
+
const result = yield* svc.view(Posts, id, noCtx);
|
|
425
|
+
expect("seqId" in result).toBe(false);
|
|
426
|
+
}).pipe(Effect.provide(testLayer)));
|
|
427
|
+
it.effect("row not matching rule → NotFoundError (indistinguishable from missing)", () => Effect.gen(function* () {
|
|
428
|
+
yield* setupPostsTable;
|
|
429
|
+
const svc = yield* CollectionService;
|
|
430
|
+
const created = yield* svc.create(Posts, { title: "Other", authorId: "user2" }, noCtx);
|
|
431
|
+
const id = created["id"];
|
|
432
|
+
if (typeof id !== "string")
|
|
433
|
+
throw new Error("expected string id");
|
|
434
|
+
const result = yield* svc.view(ProtectedPosts, id, noCtx).pipe(Effect.either);
|
|
435
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
436
|
+
if (Either.isLeft(result)) {
|
|
437
|
+
expect(result.left._tag).toBe("NotFoundError");
|
|
438
|
+
}
|
|
439
|
+
}).pipe(Effect.provide(testLayer)));
|
|
440
|
+
it.effect("no rules → ForbiddenError", () => Effect.gen(function* () {
|
|
441
|
+
yield* setupPostsTable;
|
|
442
|
+
const svc = yield* CollectionService;
|
|
443
|
+
const result = yield* svc.view(NoRulePosts, "any", noCtx).pipe(Effect.either);
|
|
444
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
445
|
+
if (Either.isLeft(result)) {
|
|
446
|
+
expect(result.left._tag).toBe("ForbiddenError");
|
|
447
|
+
}
|
|
448
|
+
}).pipe(Effect.provide(testLayer)));
|
|
449
|
+
});
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
// create
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
describe("create", () => {
|
|
454
|
+
it.effect("inserts a valid row and returns it with id, created, updated", () => Effect.gen(function* () {
|
|
455
|
+
yield* setupPostsTable;
|
|
456
|
+
const svc = yield* CollectionService;
|
|
457
|
+
const result = yield* svc.create(Posts, { title: "New Post" }, noCtx);
|
|
458
|
+
expect(typeof result["id"]).toBe("string");
|
|
459
|
+
expect(result["title"]).toBe("New Post");
|
|
460
|
+
expect(typeof result["created"]).toBe("string");
|
|
461
|
+
expect(typeof result["updated"]).toBe("string");
|
|
462
|
+
}).pipe(Effect.provide(testLayer)));
|
|
463
|
+
it.effect("seqId is NOT in the returned row", () => Effect.gen(function* () {
|
|
464
|
+
yield* setupPostsTable;
|
|
465
|
+
const svc = yield* CollectionService;
|
|
466
|
+
const result = yield* svc.create(Posts, { title: "X" }, noCtx);
|
|
467
|
+
expect("seqId" in result).toBe(false);
|
|
468
|
+
}).pipe(Effect.provide(testLayer)));
|
|
469
|
+
it.effect("ValidationError when required field is missing", () => Effect.gen(function* () {
|
|
470
|
+
yield* setupPostsTable;
|
|
471
|
+
const svc = yield* CollectionService;
|
|
472
|
+
const result = yield* svc.create(Posts, { published: true }, noCtx).pipe(Effect.either);
|
|
473
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
474
|
+
if (Either.isLeft(result)) {
|
|
475
|
+
expect(result.left._tag).toBe("ValidationError");
|
|
476
|
+
if (result.left._tag === "ValidationError") {
|
|
477
|
+
expect(result.left.issues.some((i) => i.includes("title"))).toBe(true);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}).pipe(Effect.provide(testLayer)));
|
|
481
|
+
it.effect("ValidationError when x-system field is in input", () => Effect.gen(function* () {
|
|
482
|
+
yield* setupPostsTable;
|
|
483
|
+
const svc = yield* CollectionService;
|
|
484
|
+
const result = yield* svc.create(Posts, { title: "X", id: "hacked" }, noCtx).pipe(Effect.either);
|
|
485
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
486
|
+
if (Either.isLeft(result)) {
|
|
487
|
+
expect(result.left._tag).toBe("ValidationError");
|
|
488
|
+
if (result.left._tag === "ValidationError") {
|
|
489
|
+
expect(result.left.issues.some((i) => i.includes("id"))).toBe(true);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}).pipe(Effect.provide(testLayer)));
|
|
493
|
+
it.effect("ValidationError when seqId is in input", () => Effect.gen(function* () {
|
|
494
|
+
yield* setupPostsTable;
|
|
495
|
+
const svc = yield* CollectionService;
|
|
496
|
+
const result = yield* svc.create(Posts, { title: "X", seqId: 99 }, noCtx).pipe(Effect.either);
|
|
497
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
498
|
+
if (Either.isLeft(result)) {
|
|
499
|
+
expect(result.left._tag).toBe("ValidationError");
|
|
500
|
+
}
|
|
501
|
+
}).pipe(Effect.provide(testLayer)));
|
|
502
|
+
it.effect("ValidationError when type is wrong", () => Effect.gen(function* () {
|
|
503
|
+
yield* setupPostsTable;
|
|
504
|
+
const svc = yield* CollectionService;
|
|
505
|
+
const result = yield* svc.create(Posts, { title: 42 }, noCtx).pipe(Effect.either);
|
|
506
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
507
|
+
if (Either.isLeft(result)) {
|
|
508
|
+
expect(result.left._tag).toBe("ValidationError");
|
|
509
|
+
}
|
|
510
|
+
}).pipe(Effect.provide(testLayer)));
|
|
511
|
+
it.effect("ValidationError when string exceeds maxLength", () => Effect.gen(function* () {
|
|
512
|
+
yield* setupPostsTable;
|
|
513
|
+
const svc = yield* CollectionService;
|
|
514
|
+
const result = yield* svc
|
|
515
|
+
.create(Posts, { title: "x".repeat(101) }, noCtx)
|
|
516
|
+
.pipe(Effect.either);
|
|
517
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
518
|
+
if (Either.isLeft(result)) {
|
|
519
|
+
expect(result.left._tag).toBe("ValidationError");
|
|
520
|
+
if (result.left._tag === "ValidationError") {
|
|
521
|
+
expect(result.left.issues.some((i) => i.includes("100"))).toBe(true);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}).pipe(Effect.provide(testLayer)));
|
|
525
|
+
it.effect("ForbiddenError when create rule pre-check fails", () => Effect.gen(function* () {
|
|
526
|
+
yield* setupPostsTable;
|
|
527
|
+
const svc = yield* CollectionService;
|
|
528
|
+
// ProtectedPosts requires authorId = "user1"; supply "user2" → rule fails
|
|
529
|
+
const result = yield* svc
|
|
530
|
+
.create(ProtectedPosts, { title: "X", authorId: "user2" }, noCtx)
|
|
531
|
+
.pipe(Effect.either);
|
|
532
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
533
|
+
if (Either.isLeft(result)) {
|
|
534
|
+
expect(result.left._tag).toBe("ForbiddenError");
|
|
535
|
+
}
|
|
536
|
+
}).pipe(Effect.provide(testLayer)));
|
|
537
|
+
it.effect("create succeeds when rule pre-check passes", () => Effect.gen(function* () {
|
|
538
|
+
yield* setupPostsTable;
|
|
539
|
+
const svc = yield* CollectionService;
|
|
540
|
+
const result = yield* svc.create(ProtectedPosts, { title: "X", authorId: "user1" }, noCtx);
|
|
541
|
+
expect(result["title"]).toBe("X");
|
|
542
|
+
}).pipe(Effect.provide(testLayer)));
|
|
543
|
+
it.effect("no rules → ForbiddenError", () => Effect.gen(function* () {
|
|
544
|
+
yield* setupPostsTable;
|
|
545
|
+
const svc = yield* CollectionService;
|
|
546
|
+
const result = yield* svc.create(NoRulePosts, { title: "X" }, noCtx).pipe(Effect.either);
|
|
547
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
548
|
+
if (Either.isLeft(result)) {
|
|
549
|
+
expect(result.left._tag).toBe("ForbiddenError");
|
|
550
|
+
}
|
|
551
|
+
}).pipe(Effect.provide(testLayer)));
|
|
552
|
+
it.effect("view collection → ReadOnlyError", () => Effect.gen(function* () {
|
|
553
|
+
yield* setupViewTables;
|
|
554
|
+
const svc = yield* CollectionService;
|
|
555
|
+
const result = yield* svc
|
|
556
|
+
.create(ActivePosts, { id: "x", title: "X", status: "active" }, noCtx)
|
|
557
|
+
.pipe(Effect.either);
|
|
558
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
559
|
+
if (Either.isLeft(result)) {
|
|
560
|
+
expect(result.left._tag).toBe("ReadOnlyError");
|
|
561
|
+
}
|
|
562
|
+
}).pipe(Effect.provide(testLayer)));
|
|
563
|
+
});
|
|
564
|
+
// ---------------------------------------------------------------------------
|
|
565
|
+
// update
|
|
566
|
+
// ---------------------------------------------------------------------------
|
|
567
|
+
describe("update", () => {
|
|
568
|
+
it.effect("updates supplied fields, leaves others unchanged", () => Effect.gen(function* () {
|
|
569
|
+
yield* setupPostsTable;
|
|
570
|
+
const svc = yield* CollectionService;
|
|
571
|
+
const created = yield* svc.create(Posts, { title: "Old", authorId: "user1" }, noCtx);
|
|
572
|
+
const id = created["id"];
|
|
573
|
+
if (typeof id !== "string")
|
|
574
|
+
throw new Error("expected string id");
|
|
575
|
+
const updated = yield* svc.update(Posts, id, { title: "New" }, noCtx);
|
|
576
|
+
expect(updated["title"]).toBe("New");
|
|
577
|
+
expect(updated["authorId"]).toBe("user1");
|
|
578
|
+
}).pipe(Effect.provide(testLayer)));
|
|
579
|
+
it.live("updated timestamp is refreshed; created is unchanged", () => Effect.gen(function* () {
|
|
580
|
+
yield* setupPostsTable;
|
|
581
|
+
const svc = yield* CollectionService;
|
|
582
|
+
const created = yield* svc.create(Posts, { title: "Time" }, noCtx);
|
|
583
|
+
yield* Effect.sleep(5);
|
|
584
|
+
const id = created["id"];
|
|
585
|
+
if (typeof id !== "string")
|
|
586
|
+
throw new Error("expected string id");
|
|
587
|
+
const updated = yield* svc.update(Posts, id, { title: "Time2" }, noCtx);
|
|
588
|
+
expect(updated["created"]).toBe(created["created"]);
|
|
589
|
+
expect(updated["updated"]).not.toBe(created["updated"]);
|
|
590
|
+
}).pipe(Effect.provide(testLayer)));
|
|
591
|
+
it.effect("ValidationError when x-system field is in patch", () => Effect.gen(function* () {
|
|
592
|
+
yield* setupPostsTable;
|
|
593
|
+
const svc = yield* CollectionService;
|
|
594
|
+
const created = yield* svc.create(Posts, { title: "X" }, noCtx);
|
|
595
|
+
const id = created["id"];
|
|
596
|
+
if (typeof id !== "string")
|
|
597
|
+
throw new Error("expected string id");
|
|
598
|
+
const result = yield* svc
|
|
599
|
+
.update(Posts, id, { title: "Y", id: "hacked", created: "1970" }, noCtx)
|
|
600
|
+
.pipe(Effect.either);
|
|
601
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
602
|
+
if (Either.isLeft(result)) {
|
|
603
|
+
expect(result.left._tag).toBe("ValidationError");
|
|
604
|
+
}
|
|
605
|
+
}).pipe(Effect.provide(testLayer)));
|
|
606
|
+
it.effect("NotFoundError for unknown id", () => Effect.gen(function* () {
|
|
607
|
+
yield* setupPostsTable;
|
|
608
|
+
const svc = yield* CollectionService;
|
|
609
|
+
const result = yield* svc
|
|
610
|
+
.update(Posts, "nosuchid1234567", { title: "X" }, noCtx)
|
|
611
|
+
.pipe(Effect.either);
|
|
612
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
613
|
+
if (Either.isLeft(result)) {
|
|
614
|
+
expect(result.left._tag).toBe("NotFoundError");
|
|
615
|
+
}
|
|
616
|
+
}).pipe(Effect.provide(testLayer)));
|
|
617
|
+
it.effect("ForbiddenError when rule blocks update", () => Effect.gen(function* () {
|
|
618
|
+
yield* setupPostsTable;
|
|
619
|
+
const svc = yield* CollectionService;
|
|
620
|
+
// Insert row with authorId user2 using public Posts, then try to update via ProtectedPosts
|
|
621
|
+
const created = yield* svc.create(Posts, { title: "X", authorId: "user2" }, noCtx);
|
|
622
|
+
const id = created["id"];
|
|
623
|
+
if (typeof id !== "string")
|
|
624
|
+
throw new Error("expected string id");
|
|
625
|
+
const result = yield* svc
|
|
626
|
+
.update(ProtectedPosts, id, { title: "Y" }, noCtx)
|
|
627
|
+
.pipe(Effect.either);
|
|
628
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
629
|
+
if (Either.isLeft(result)) {
|
|
630
|
+
expect(result.left._tag).toBe("ForbiddenError");
|
|
631
|
+
if (result.left._tag === "ForbiddenError") {
|
|
632
|
+
expect(result.left.action).toBe("update");
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}).pipe(Effect.provide(testLayer)));
|
|
636
|
+
it.effect("no rules → ForbiddenError", () => Effect.gen(function* () {
|
|
637
|
+
yield* setupPostsTable;
|
|
638
|
+
const svc = yield* CollectionService;
|
|
639
|
+
const result = yield* svc
|
|
640
|
+
.update(NoRulePosts, "anyid", { title: "X" }, noCtx)
|
|
641
|
+
.pipe(Effect.either);
|
|
642
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
643
|
+
if (Either.isLeft(result)) {
|
|
644
|
+
expect(result.left._tag).toBe("ForbiddenError");
|
|
645
|
+
}
|
|
646
|
+
}).pipe(Effect.provide(testLayer)));
|
|
647
|
+
it.effect("view collection → ReadOnlyError", () => Effect.gen(function* () {
|
|
648
|
+
yield* setupViewTables;
|
|
649
|
+
const svc = yield* CollectionService;
|
|
650
|
+
const result = yield* svc
|
|
651
|
+
.update(ActivePosts, "anyid", { title: "X" }, noCtx)
|
|
652
|
+
.pipe(Effect.either);
|
|
653
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
654
|
+
if (Either.isLeft(result)) {
|
|
655
|
+
expect(result.left._tag).toBe("ReadOnlyError");
|
|
656
|
+
}
|
|
657
|
+
}).pipe(Effect.provide(testLayer)));
|
|
658
|
+
});
|
|
659
|
+
// ---------------------------------------------------------------------------
|
|
660
|
+
// delete
|
|
661
|
+
// ---------------------------------------------------------------------------
|
|
662
|
+
describe("delete", () => {
|
|
663
|
+
it.effect("deletes an existing row and returns void", () => Effect.gen(function* () {
|
|
664
|
+
yield* setupPostsTable;
|
|
665
|
+
const svc = yield* CollectionService;
|
|
666
|
+
const created = yield* svc.create(Posts, { title: "Gone" }, noCtx);
|
|
667
|
+
const id = created["id"];
|
|
668
|
+
if (typeof id !== "string")
|
|
669
|
+
throw new Error("expected string id");
|
|
670
|
+
yield* svc.delete(Posts, id, noCtx);
|
|
671
|
+
}).pipe(Effect.provide(testLayer)));
|
|
672
|
+
it.effect("row is gone after delete (confirmed via view)", () => Effect.gen(function* () {
|
|
673
|
+
yield* setupPostsTable;
|
|
674
|
+
const svc = yield* CollectionService;
|
|
675
|
+
const created = yield* svc.create(Posts, { title: "Gone" }, noCtx);
|
|
676
|
+
const id = created["id"];
|
|
677
|
+
if (typeof id !== "string")
|
|
678
|
+
throw new Error("expected string id");
|
|
679
|
+
yield* svc.delete(Posts, id, noCtx);
|
|
680
|
+
const result = yield* svc.view(Posts, id, noCtx).pipe(Effect.either);
|
|
681
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
682
|
+
if (Either.isLeft(result)) {
|
|
683
|
+
expect(result.left._tag).toBe("NotFoundError");
|
|
684
|
+
}
|
|
685
|
+
}).pipe(Effect.provide(testLayer)));
|
|
686
|
+
it.effect("NotFoundError for unknown id", () => Effect.gen(function* () {
|
|
687
|
+
yield* setupPostsTable;
|
|
688
|
+
const svc = yield* CollectionService;
|
|
689
|
+
const result = yield* svc.delete(Posts, "nosuchid1234567", noCtx).pipe(Effect.either);
|
|
690
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
691
|
+
if (Either.isLeft(result)) {
|
|
692
|
+
expect(result.left._tag).toBe("NotFoundError");
|
|
693
|
+
}
|
|
694
|
+
}).pipe(Effect.provide(testLayer)));
|
|
695
|
+
it.effect("ForbiddenError when rule blocks delete", () => Effect.gen(function* () {
|
|
696
|
+
yield* setupPostsTable;
|
|
697
|
+
const svc = yield* CollectionService;
|
|
698
|
+
const created = yield* svc.create(Posts, { title: "X", authorId: "user2" }, noCtx);
|
|
699
|
+
const id = created["id"];
|
|
700
|
+
if (typeof id !== "string")
|
|
701
|
+
throw new Error("expected string id");
|
|
702
|
+
const result = yield* svc
|
|
703
|
+
.delete(ProtectedPosts, id, noCtx)
|
|
704
|
+
.pipe(Effect.either);
|
|
705
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
706
|
+
if (Either.isLeft(result)) {
|
|
707
|
+
expect(result.left._tag).toBe("ForbiddenError");
|
|
708
|
+
if (result.left._tag === "ForbiddenError") {
|
|
709
|
+
expect(result.left.action).toBe("delete");
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}).pipe(Effect.provide(testLayer)));
|
|
713
|
+
it.effect("no rules → ForbiddenError", () => Effect.gen(function* () {
|
|
714
|
+
yield* setupPostsTable;
|
|
715
|
+
const svc = yield* CollectionService;
|
|
716
|
+
const result = yield* svc.delete(NoRulePosts, "anyid", noCtx).pipe(Effect.either);
|
|
717
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
718
|
+
if (Either.isLeft(result)) {
|
|
719
|
+
expect(result.left._tag).toBe("ForbiddenError");
|
|
720
|
+
}
|
|
721
|
+
}).pipe(Effect.provide(testLayer)));
|
|
722
|
+
it.effect("view collection → ReadOnlyError", () => Effect.gen(function* () {
|
|
723
|
+
yield* setupViewTables;
|
|
724
|
+
const svc = yield* CollectionService;
|
|
725
|
+
const result = yield* svc.delete(ActivePosts, "anyid", noCtx).pipe(Effect.either);
|
|
726
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
727
|
+
if (Either.isLeft(result)) {
|
|
728
|
+
expect(result.left._tag).toBe("ReadOnlyError");
|
|
729
|
+
}
|
|
730
|
+
}).pipe(Effect.provide(testLayer)));
|
|
731
|
+
});
|
|
732
|
+
// ---------------------------------------------------------------------------
|
|
733
|
+
// error callback — integration tests
|
|
734
|
+
// ---------------------------------------------------------------------------
|
|
735
|
+
describe("error callback — integration", () => {
|
|
736
|
+
it.effect("minLength message propagates from create", () => Effect.gen(function* () {
|
|
737
|
+
yield* setupScoredTable;
|
|
738
|
+
const svc = yield* CollectionService;
|
|
739
|
+
const result = yield* svc.create(Scored, { name: "x", score: 50 }, noCtx).pipe(Effect.either);
|
|
740
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
741
|
+
if (Either.isLeft(result) && result.left._tag === "ValidationError") {
|
|
742
|
+
expect(result.left.issues.some((i) => i.includes("Name too short"))).toBe(true);
|
|
743
|
+
}
|
|
744
|
+
}).pipe(Effect.provide(scoredTestLayer)));
|
|
745
|
+
it.effect("minimum message propagates from create", () => Effect.gen(function* () {
|
|
746
|
+
yield* setupScoredTable;
|
|
747
|
+
const svc = yield* CollectionService;
|
|
748
|
+
const result = yield* svc.create(Scored, { name: "ok", score: -1 }, noCtx).pipe(Effect.either);
|
|
749
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
750
|
+
if (Either.isLeft(result) && result.left._tag === "ValidationError") {
|
|
751
|
+
expect(result.left.issues.some((i) => i.includes("Score below zero"))).toBe(true);
|
|
752
|
+
}
|
|
753
|
+
}).pipe(Effect.provide(scoredTestLayer)));
|
|
754
|
+
it.effect("maximum message propagates from create", () => Effect.gen(function* () {
|
|
755
|
+
yield* setupScoredTable;
|
|
756
|
+
const svc = yield* CollectionService;
|
|
757
|
+
const result = yield* svc.create(Scored, { name: "ok", score: 200 }, noCtx).pipe(Effect.either);
|
|
758
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
759
|
+
if (Either.isLeft(result) && result.left._tag === "ValidationError") {
|
|
760
|
+
expect(result.left.issues.some((i) => i.includes("Score above 100"))).toBe(true);
|
|
761
|
+
}
|
|
762
|
+
}).pipe(Effect.provide(scoredTestLayer)));
|
|
763
|
+
it.effect("required message propagates from create", () => Effect.gen(function* () {
|
|
764
|
+
yield* setupScoredTable;
|
|
765
|
+
const svc = yield* CollectionService;
|
|
766
|
+
const result = yield* svc.create(Scored, { score: 50 }, noCtx).pipe(Effect.either);
|
|
767
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
768
|
+
if (Either.isLeft(result) && result.left._tag === "ValidationError") {
|
|
769
|
+
expect(result.left.issues.some((i) => i.includes("Name is required"))).toBe(true);
|
|
770
|
+
}
|
|
771
|
+
}).pipe(Effect.provide(scoredTestLayer)));
|
|
772
|
+
it.effect("Match.orElse fallback fires for type mismatch", () => Effect.gen(function* () {
|
|
773
|
+
yield* setupScoredTable;
|
|
774
|
+
const svc = yield* CollectionService;
|
|
775
|
+
const result = yield* svc.create(Scored, { name: 999, score: 50 }, noCtx).pipe(Effect.either);
|
|
776
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
777
|
+
if (Either.isLeft(result) && result.left._tag === "ValidationError") {
|
|
778
|
+
expect(result.left.issues.some((i) => i.includes("Invalid name"))).toBe(true);
|
|
779
|
+
}
|
|
780
|
+
}).pipe(Effect.provide(scoredTestLayer)));
|
|
781
|
+
it.effect("update with too-short name produces default message (no custom error on update path)", () => Effect.gen(function* () {
|
|
782
|
+
yield* setupScoredTable;
|
|
783
|
+
const svc = yield* CollectionService;
|
|
784
|
+
const created = yield* svc.create(Scored, { name: "ok", score: 50 }, noCtx);
|
|
785
|
+
const id = created["id"];
|
|
786
|
+
if (typeof id !== "string")
|
|
787
|
+
throw new Error("expected string id");
|
|
788
|
+
const result = yield* svc.update(Scored, id, { name: "x" }, noCtx).pipe(Effect.either);
|
|
789
|
+
expect(Either.isLeft(result)).toBe(true);
|
|
790
|
+
if (Either.isLeft(result)) {
|
|
791
|
+
expect(result.left._tag).toBe("ValidationError");
|
|
792
|
+
if (result.left._tag === "ValidationError") {
|
|
793
|
+
expect(result.left.issues.length).toBeGreaterThan(0);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}).pipe(Effect.provide(scoredTestLayer)));
|
|
797
|
+
it.effect("valid data succeeds", () => Effect.gen(function* () {
|
|
798
|
+
yield* setupScoredTable;
|
|
799
|
+
const svc = yield* CollectionService;
|
|
800
|
+
const result = yield* svc.create(Scored, { name: "ok", score: 50 }, noCtx);
|
|
801
|
+
expect(result["name"]).toBe("ok");
|
|
802
|
+
expect(result["score"]).toBe(50);
|
|
803
|
+
}).pipe(Effect.provide(scoredTestLayer)));
|
|
804
|
+
});
|
|
805
|
+
//# sourceMappingURL=collection-service.test.js.map
|