@gettersethya/mira 0.1.0-alpha.2 → 0.1.0-alpha.21
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/README.md +174 -0
- package/dist/src/app/app.d.ts +45 -26
- package/dist/src/app/app.d.ts.map +1 -1
- package/dist/src/app/app.js +138 -41
- package/dist/src/app/app.js.map +1 -1
- package/dist/src/app/builder.d.ts +56 -15
- package/dist/src/app/builder.d.ts.map +1 -1
- package/dist/src/app/builder.js +56 -4
- package/dist/src/app/builder.js.map +1 -1
- package/dist/src/app/define-rule.d.ts +70 -0
- package/dist/src/app/define-rule.d.ts.map +1 -0
- package/dist/src/app/define-rule.js +42 -0
- package/dist/src/app/define-rule.js.map +1 -0
- package/dist/src/app/index.d.ts +8 -3
- package/dist/src/app/index.d.ts.map +1 -1
- package/dist/src/app/index.js +3 -0
- package/dist/src/app/index.js.map +1 -1
- package/dist/src/app/plugin.d.ts +83 -0
- package/dist/src/app/plugin.d.ts.map +1 -0
- package/dist/src/app/plugin.js +20 -0
- package/dist/src/app/plugin.js.map +1 -0
- package/dist/src/app/types.d.ts +4 -4
- package/dist/src/app/types.d.ts.map +1 -1
- package/dist/src/cache/cached-collection.d.ts +7 -5
- package/dist/src/cache/cached-collection.d.ts.map +1 -1
- package/dist/src/cache/cached-collection.js +8 -5
- package/dist/src/cache/cached-collection.js.map +1 -1
- package/dist/src/cache/collection-cache.d.ts +1 -1
- package/dist/src/cache/collection-cache.d.ts.map +1 -1
- package/dist/src/cache/collection-cache.js +1 -1
- package/dist/src/cache/collection-cache.js.map +1 -1
- package/dist/src/cache/ctx-key.d.ts +15 -0
- package/dist/src/cache/ctx-key.d.ts.map +1 -0
- package/dist/src/cache/ctx-key.js +37 -0
- package/dist/src/cache/ctx-key.js.map +1 -0
- package/dist/src/cache/index.d.ts +2 -1
- package/dist/src/cache/index.d.ts.map +1 -1
- package/dist/src/cache/index.js +2 -1
- package/dist/src/cache/index.js.map +1 -1
- package/dist/src/collection-service/collection-service.d.ts +7 -5
- package/dist/src/collection-service/collection-service.d.ts.map +1 -1
- package/dist/src/collection-service/collection-service.js +201 -51
- package/dist/src/collection-service/collection-service.js.map +1 -1
- package/dist/src/collection-service/context.d.ts +2 -0
- package/dist/src/collection-service/context.d.ts.map +1 -1
- package/dist/src/collection-service/decode.d.ts +4 -2
- package/dist/src/collection-service/decode.d.ts.map +1 -1
- package/dist/src/collection-service/decode.js +32 -16
- package/dist/src/collection-service/decode.js.map +1 -1
- package/dist/src/collection-service/errors.d.ts +1 -1
- package/dist/src/collection-service/errors.d.ts.map +1 -1
- package/dist/src/collection-service/errors.js +1 -1
- package/dist/src/collection-service/errors.js.map +1 -1
- package/dist/src/collection-service/index.d.ts +1 -1
- package/dist/src/collection-service/index.d.ts.map +1 -1
- package/dist/src/collection-service/index.js.map +1 -1
- package/dist/src/collection-service/input-schema.d.ts +5 -4
- package/dist/src/collection-service/input-schema.d.ts.map +1 -1
- package/dist/src/collection-service/input-schema.js +31 -18
- package/dist/src/collection-service/input-schema.js.map +1 -1
- package/dist/src/collection-service/where.d.ts +14 -2
- package/dist/src/collection-service/where.d.ts.map +1 -1
- package/dist/src/collection-service/where.js +35 -1
- package/dist/src/collection-service/where.js.map +1 -1
- package/dist/src/config/config.d.ts +4 -4
- package/dist/src/config/config.d.ts.map +1 -1
- package/dist/src/config/config.js +34 -5
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/types.d.ts +9 -0
- package/dist/src/config/types.d.ts.map +1 -1
- package/dist/src/cron/cron-service.d.ts +14 -0
- package/dist/src/cron/cron-service.d.ts.map +1 -0
- package/dist/src/cron/cron-service.js +135 -0
- package/dist/src/cron/cron-service.js.map +1 -0
- package/dist/src/cron/index.d.ts +4 -0
- package/dist/src/cron/index.d.ts.map +1 -0
- package/dist/src/cron/index.js +3 -0
- package/dist/src/cron/index.js.map +1 -0
- package/dist/src/cron/persistence.d.ts +22 -0
- package/dist/src/cron/persistence.d.ts.map +1 -0
- package/dist/src/cron/persistence.js +48 -0
- package/dist/src/cron/persistence.js.map +1 -0
- package/dist/src/cron/types.d.ts +51 -0
- package/dist/src/cron/types.d.ts.map +1 -0
- package/dist/src/cron/types.js +4 -0
- package/dist/src/cron/types.js.map +1 -0
- package/dist/src/crypto/crypto.d.ts +3 -2
- package/dist/src/crypto/crypto.d.ts.map +1 -1
- package/dist/src/crypto/crypto.js +1 -1
- package/dist/src/crypto/crypto.js.map +1 -1
- package/dist/src/crypto/node.d.ts.map +1 -1
- package/dist/src/crypto/node.js.map +1 -1
- package/dist/src/databases/sqlite.d.ts.map +1 -1
- package/dist/src/databases/sqlite.js +2 -2
- package/dist/src/databases/sqlite.js.map +1 -1
- package/dist/src/dialect/dialect-sqlite.d.ts +9 -0
- package/dist/src/dialect/dialect-sqlite.d.ts.map +1 -0
- package/dist/src/dialect/dialect-sqlite.js +109 -0
- package/dist/src/dialect/dialect-sqlite.js.map +1 -0
- package/dist/src/dialect/dialect.d.ts +26 -0
- package/dist/src/dialect/dialect.d.ts.map +1 -0
- package/dist/src/dialect/dialect.js +5 -0
- package/dist/src/dialect/dialect.js.map +1 -0
- package/dist/src/hooks/hook-collection.d.ts +5 -0
- package/dist/src/hooks/hook-collection.d.ts.map +1 -0
- package/dist/src/hooks/hook-collection.js +77 -0
- package/dist/src/hooks/hook-collection.js.map +1 -0
- package/dist/src/hooks/hook-service.d.ts +38 -0
- package/dist/src/hooks/hook-service.d.ts.map +1 -0
- package/dist/src/hooks/hook-service.js +150 -0
- package/dist/src/hooks/hook-service.js.map +1 -0
- package/dist/src/hooks/index.d.ts +4 -0
- package/dist/src/hooks/index.d.ts.map +1 -0
- package/dist/src/hooks/index.js +3 -0
- package/dist/src/hooks/index.js.map +1 -0
- package/dist/src/hooks/types.d.ts +74 -0
- package/dist/src/hooks/types.d.ts.map +1 -0
- package/dist/src/hooks/types.js +2 -0
- package/dist/src/hooks/types.js.map +1 -0
- package/dist/src/http/auth-node.d.ts.map +1 -1
- package/dist/src/http/auth-node.js +1 -1
- package/dist/src/http/auth-node.js.map +1 -1
- package/dist/src/http/auth.d.ts +3 -1
- package/dist/src/http/auth.d.ts.map +1 -1
- package/dist/src/http/auth.js +12 -2
- package/dist/src/http/auth.js.map +1 -1
- package/dist/src/http/cors.d.ts +94 -0
- package/dist/src/http/cors.d.ts.map +1 -0
- package/dist/src/http/cors.js +57 -0
- package/dist/src/http/cors.js.map +1 -0
- package/dist/src/http/errors.d.ts +1 -1
- package/dist/src/http/errors.d.ts.map +1 -1
- package/dist/src/http/errors.js +7 -7
- package/dist/src/http/errors.js.map +1 -1
- package/dist/src/http/file-serve.d.ts +4 -4
- package/dist/src/http/file-serve.d.ts.map +1 -1
- package/dist/src/http/file-serve.js +39 -34
- package/dist/src/http/file-serve.js.map +1 -1
- package/dist/src/http/file-token.d.ts +4 -3
- package/dist/src/http/file-token.d.ts.map +1 -1
- package/dist/src/http/file-token.js +19 -17
- package/dist/src/http/file-token.js.map +1 -1
- package/dist/src/http/files.d.ts +3 -3
- package/dist/src/http/files.d.ts.map +1 -1
- package/dist/src/http/files.js +5 -5
- package/dist/src/http/files.js.map +1 -1
- package/dist/src/http/index.d.ts +8 -5
- package/dist/src/http/index.d.ts.map +1 -1
- package/dist/src/http/index.js +5 -3
- package/dist/src/http/index.js.map +1 -1
- package/dist/src/http/ip-middleware.d.ts +3 -2
- package/dist/src/http/ip-middleware.d.ts.map +1 -1
- package/dist/src/http/ip-middleware.js +7 -4
- package/dist/src/http/ip-middleware.js.map +1 -1
- package/dist/src/http/params.d.ts +3 -3
- package/dist/src/http/params.d.ts.map +1 -1
- package/dist/src/http/params.js +2 -2
- package/dist/src/http/params.js.map +1 -1
- package/dist/src/http/port-retry.d.ts +18 -0
- package/dist/src/http/port-retry.d.ts.map +1 -0
- package/dist/src/http/port-retry.js +43 -0
- package/dist/src/http/port-retry.js.map +1 -0
- package/dist/src/http/router.d.ts +10 -7
- package/dist/src/http/router.d.ts.map +1 -1
- package/dist/src/http/router.js +184 -63
- package/dist/src/http/router.js.map +1 -1
- package/dist/src/http/schema.d.ts +5 -0
- package/dist/src/http/schema.d.ts.map +1 -0
- package/dist/src/http/schema.js +18 -0
- package/dist/src/http/schema.js.map +1 -0
- package/dist/src/http/server-factory-node.d.ts.map +1 -1
- package/dist/src/http/server-factory-node.js +1 -1
- package/dist/src/http/server-factory-node.js.map +1 -1
- package/dist/src/http/server-factory.d.ts +3 -3
- package/dist/src/http/server-factory.d.ts.map +1 -1
- package/dist/src/http/server-factory.js +1 -1
- package/dist/src/http/server-factory.js.map +1 -1
- package/dist/src/http/telemetry-routes.d.ts +5 -0
- package/dist/src/http/telemetry-routes.d.ts.map +1 -0
- package/dist/src/http/telemetry-routes.js +220 -0
- package/dist/src/http/telemetry-routes.js.map +1 -0
- package/dist/src/index.d.ts +18 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +14 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/migrator/dialect-sqlite.d.ts.map +1 -1
- package/dist/src/migrator/dialect-sqlite.js +7 -1
- package/dist/src/migrator/dialect-sqlite.js.map +1 -1
- package/dist/src/migrator/index.d.ts +4 -4
- package/dist/src/migrator/index.d.ts.map +1 -1
- package/dist/src/migrator/index.js +2 -2
- package/dist/src/migrator/index.js.map +1 -1
- package/dist/src/migrator/migrator.d.ts +8 -8
- package/dist/src/migrator/migrator.d.ts.map +1 -1
- package/dist/src/migrator/migrator.js +16 -13
- package/dist/src/migrator/migrator.js.map +1 -1
- package/dist/src/migrator/schema-diff.d.ts +2 -2
- package/dist/src/migrator/schema-diff.d.ts.map +1 -1
- package/dist/src/migrator/schema-diff.js.map +1 -1
- package/dist/src/migrator/types.d.ts +5 -5
- package/dist/src/migrator/types.d.ts.map +1 -1
- package/dist/src/migrator/types.js +6 -7
- package/dist/src/migrator/types.js.map +1 -1
- package/dist/src/platforms/node.d.ts +1 -1
- package/dist/src/platforms/node.d.ts.map +1 -1
- package/dist/src/platforms/node.js +1 -1
- package/dist/src/platforms/node.js.map +1 -1
- package/dist/src/repository/id.d.ts +1 -1
- package/dist/src/repository/id.d.ts.map +1 -1
- package/dist/src/repository/id.js +15 -3
- package/dist/src/repository/id.js.map +1 -1
- package/dist/src/repository/repository.d.ts +3 -2
- package/dist/src/repository/repository.d.ts.map +1 -1
- package/dist/src/repository/repository.js +11 -7
- package/dist/src/repository/repository.js.map +1 -1
- package/dist/src/rule/compiler.d.ts.map +1 -1
- package/dist/src/rule/compiler.js +2 -0
- package/dist/src/rule/compiler.js.map +1 -1
- package/dist/src/rule/enforcer.d.ts.map +1 -1
- package/dist/src/rule/enforcer.js.map +1 -1
- package/dist/src/storage/index.d.ts +1 -1
- package/dist/src/storage/index.d.ts.map +1 -1
- package/dist/src/storage/index.js +1 -1
- package/dist/src/storage/index.js.map +1 -1
- package/dist/src/storage/local-preset.d.ts.map +1 -1
- package/dist/src/storage/local-preset.js.map +1 -1
- package/dist/src/storage/storage.d.ts +2 -2
- package/dist/src/storage/storage.d.ts.map +1 -1
- package/dist/src/storage/storage.js +8 -26
- package/dist/src/storage/storage.js.map +1 -1
- package/dist/src/telemetry/collections.d.ts +4 -0
- package/dist/src/telemetry/collections.d.ts.map +1 -0
- package/dist/src/telemetry/collections.js +19 -0
- package/dist/src/telemetry/collections.js.map +1 -0
- package/dist/src/telemetry/index.d.ts +17 -3
- package/dist/src/telemetry/index.d.ts.map +1 -1
- package/dist/src/telemetry/index.js +15 -5
- package/dist/src/telemetry/index.js.map +1 -1
- package/dist/src/telemetry/logger.d.ts +29 -2
- package/dist/src/telemetry/logger.d.ts.map +1 -1
- package/dist/src/telemetry/logger.js +52 -15
- package/dist/src/telemetry/logger.js.map +1 -1
- package/dist/src/telemetry/sqlite-logger.d.ts +36 -0
- package/dist/src/telemetry/sqlite-logger.d.ts.map +1 -0
- package/dist/src/telemetry/sqlite-logger.js +193 -0
- package/dist/src/telemetry/sqlite-logger.js.map +1 -0
- package/dist/src/telemetry/telemetry-sql-client.d.ts +11 -0
- package/dist/src/telemetry/telemetry-sql-client.d.ts.map +1 -0
- package/dist/src/telemetry/telemetry-sql-client.js +8 -0
- package/dist/src/telemetry/telemetry-sql-client.js.map +1 -0
- package/dist/src/telemetry/tracer.d.ts +16 -1
- package/dist/src/telemetry/tracer.d.ts.map +1 -1
- package/dist/src/telemetry/tracer.js +87 -70
- package/dist/src/telemetry/tracer.js.map +1 -1
- package/dist/src/telemetry/types.d.ts +11 -0
- package/dist/src/telemetry/types.d.ts.map +1 -0
- package/dist/src/telemetry/types.js +2 -0
- package/dist/src/telemetry/types.js.map +1 -0
- package/dist/src/thumbnail/index.d.ts +2 -2
- package/dist/src/thumbnail/index.d.ts.map +1 -1
- package/dist/src/thumbnail/index.js +1 -1
- package/dist/src/thumbnail/index.js.map +1 -1
- package/dist/src/thumbnail/photon.d.ts.map +1 -1
- package/dist/src/thumbnail/photon.js +1 -1
- package/dist/src/thumbnail/photon.js.map +1 -1
- package/dist/src/thumbnail/types.d.ts +1 -1
- package/dist/src/thumbnail/types.d.ts.map +1 -1
- package/dist/src/thumbnail/types.js +2 -2
- package/dist/src/thumbnail/types.js.map +1 -1
- package/dist/tests/app/app.test.js +101 -26
- package/dist/tests/app/app.test.js.map +1 -1
- package/dist/tests/app/builder.test.js +77 -9
- package/dist/tests/app/builder.test.js.map +1 -1
- package/dist/tests/app/define-rule.test.d.ts +2 -0
- package/dist/tests/app/define-rule.test.d.ts.map +1 -0
- package/dist/tests/app/define-rule.test.js +147 -0
- package/dist/tests/app/define-rule.test.js.map +1 -0
- package/dist/tests/cache/cached-collection.test.js +200 -40
- package/dist/tests/cache/cached-collection.test.js.map +1 -1
- package/dist/tests/cache/collection-cache.test.js +1 -1
- package/dist/tests/cache/collection-cache.test.js.map +1 -1
- package/dist/tests/collection-service/auth-credentials.test.d.ts +2 -0
- package/dist/tests/collection-service/auth-credentials.test.d.ts.map +1 -0
- package/dist/tests/collection-service/auth-credentials.test.js +172 -0
- package/dist/tests/collection-service/auth-credentials.test.js.map +1 -0
- package/dist/tests/collection-service/collection-service.test.js +374 -174
- package/dist/tests/collection-service/collection-service.test.js.map +1 -1
- package/dist/tests/collection-service/decode.test.d.ts +2 -0
- package/dist/tests/collection-service/decode.test.d.ts.map +1 -0
- package/dist/tests/collection-service/decode.test.js +37 -0
- package/dist/tests/collection-service/decode.test.js.map +1 -0
- package/dist/tests/collection-service/input-schema.test.js +162 -95
- package/dist/tests/collection-service/input-schema.test.js.map +1 -1
- package/dist/tests/config/config.test.js +15 -4
- package/dist/tests/config/config.test.js.map +1 -1
- package/dist/tests/cron/cron-integration.test.d.ts +2 -0
- package/dist/tests/cron/cron-integration.test.d.ts.map +1 -0
- package/dist/tests/cron/cron-integration.test.js +109 -0
- package/dist/tests/cron/cron-integration.test.js.map +1 -0
- package/dist/tests/cron/cron-persistence.test.d.ts +2 -0
- package/dist/tests/cron/cron-persistence.test.d.ts.map +1 -0
- package/dist/tests/cron/cron-persistence.test.js +123 -0
- package/dist/tests/cron/cron-persistence.test.js.map +1 -0
- package/dist/tests/cron/cron-service.test.d.ts +2 -0
- package/dist/tests/cron/cron-service.test.d.ts.map +1 -0
- package/dist/tests/cron/cron-service.test.js +124 -0
- package/dist/tests/cron/cron-service.test.js.map +1 -0
- package/dist/tests/crypto/crypto.test.js +2 -2
- package/dist/tests/crypto/crypto.test.js.map +1 -1
- package/dist/tests/hooks/hook-collection.test.d.ts +2 -0
- package/dist/tests/hooks/hook-collection.test.d.ts.map +1 -0
- package/dist/tests/hooks/hook-collection.test.js +110 -0
- package/dist/tests/hooks/hook-collection.test.js.map +1 -0
- package/dist/tests/hooks/hook-service.test.d.ts +2 -0
- package/dist/tests/hooks/hook-service.test.d.ts.map +1 -0
- package/dist/tests/hooks/hook-service.test.js +220 -0
- package/dist/tests/hooks/hook-service.test.js.map +1 -0
- package/dist/tests/http/cors.test.d.ts +2 -0
- package/dist/tests/http/cors.test.d.ts.map +1 -0
- package/dist/tests/http/cors.test.js +211 -0
- package/dist/tests/http/cors.test.js.map +1 -0
- package/dist/tests/http/file-serve.test.js +63 -54
- package/dist/tests/http/file-serve.test.js.map +1 -1
- package/dist/tests/http/file-token.test.js +69 -51
- package/dist/tests/http/file-token.test.js.map +1 -1
- package/dist/tests/http/ip-middleware.test.js +13 -13
- package/dist/tests/http/ip-middleware.test.js.map +1 -1
- package/dist/tests/http/port-retry.test.d.ts +2 -0
- package/dist/tests/http/port-retry.test.d.ts.map +1 -0
- package/dist/tests/http/port-retry.test.js +52 -0
- package/dist/tests/http/port-retry.test.js.map +1 -0
- package/dist/tests/http/router.test.js +260 -85
- package/dist/tests/http/router.test.js.map +1 -1
- package/dist/tests/migrator/dialect-sqlite.test.js +1 -1
- package/dist/tests/migrator/dialect-sqlite.test.js.map +1 -1
- package/dist/tests/migrator/integration.test.js +6 -6
- package/dist/tests/migrator/integration.test.js.map +1 -1
- package/dist/tests/migrator/migrator.test.js +5 -5
- package/dist/tests/migrator/migrator.test.js.map +1 -1
- package/dist/tests/migrator/schema-diff.test.js.map +1 -1
- package/dist/tests/repository/repository.test.js +71 -8
- package/dist/tests/repository/repository.test.js.map +1 -1
- package/dist/tests/rule/compiler.test.js +1 -1
- package/dist/tests/rule/compiler.test.js.map +1 -1
- package/dist/tests/rule/enforcer.test.js +9 -9
- package/dist/tests/rule/enforcer.test.js.map +1 -1
- package/dist/tests/storage/storage.test.js +15 -15
- package/dist/tests/storage/storage.test.js.map +1 -1
- package/dist/tests/telemetry/collections.test.d.ts +2 -0
- package/dist/tests/telemetry/collections.test.d.ts.map +1 -0
- package/dist/tests/telemetry/collections.test.js +67 -0
- package/dist/tests/telemetry/collections.test.js.map +1 -0
- package/dist/tests/telemetry/instrumentation.test.js +27 -20
- package/dist/tests/telemetry/instrumentation.test.js.map +1 -1
- package/dist/tests/telemetry/logger.test.js +38 -6
- package/dist/tests/telemetry/logger.test.js.map +1 -1
- package/dist/tests/telemetry/sqlite-logger.test.d.ts +2 -0
- package/dist/tests/telemetry/sqlite-logger.test.d.ts.map +1 -0
- package/dist/tests/telemetry/sqlite-logger.test.js +272 -0
- package/dist/tests/telemetry/sqlite-logger.test.js.map +1 -0
- package/dist/tests/telemetry/telemetry-routes.test.d.ts +2 -0
- package/dist/tests/telemetry/telemetry-routes.test.d.ts.map +1 -0
- package/dist/tests/telemetry/telemetry-routes.test.js +154 -0
- package/dist/tests/telemetry/telemetry-routes.test.js.map +1 -0
- package/dist/tests/telemetry/tracer.test.js +41 -4
- package/dist/tests/telemetry/tracer.test.js.map +1 -1
- package/dist/tests/thumbnail/photon.test.js +35 -35
- package/dist/tests/thumbnail/photon.test.js.map +1 -1
- package/dist/tests/thumbnail/thumbnail.test.js +7 -7
- package/dist/tests/thumbnail/thumbnail.test.js.map +1 -1
- package/package.json +9 -9
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { SqlClient } from "@effect/sql";
|
|
2
1
|
import { SqliteClient } from "@effect/sql-sqlite-node";
|
|
3
|
-
import { Effect, Either, Layer, Match } from "effect";
|
|
4
2
|
import { describe, it } from "@effect/vitest";
|
|
5
|
-
import {
|
|
6
|
-
import { BaseCollection } from "@gettersethya/mira-client";
|
|
3
|
+
import { AuthCollection, BaseCollection } from "@gettersethya/mira-client";
|
|
7
4
|
import { Field } from "@gettersethya/mira-client";
|
|
8
5
|
import { ViewCollection } from "@gettersethya/mira-client";
|
|
9
|
-
import {
|
|
6
|
+
import { Effect, Layer, Match, Result } from "effect";
|
|
7
|
+
import { SqlClient } from "effect/sql";
|
|
8
|
+
import { expect } from "vitest";
|
|
9
|
+
import { applyRulesToCollections, defineRule } from "../../src/app/index.js";
|
|
10
10
|
import { CollectionService, makeCollectionServiceLayer } from "../../src/collection-service/collection-service.js";
|
|
11
|
-
import { FileStorage, FileStorageNotFound } from "../../src/storage/storage.js";
|
|
12
11
|
import { NodeCryptoLayer } from "../../src/crypto/node.js";
|
|
12
|
+
import { Dialect } from "../../src/dialect/dialect.js";
|
|
13
|
+
import { sqliteDialect } from "../../src/dialect/dialect-sqlite.js";
|
|
14
|
+
import { NodeAuthServiceLayer } from "../../src/http/auth-node.js";
|
|
15
|
+
import { RepositoryLive } from "../../src/repository/repository.js";
|
|
16
|
+
import { FileStorage, FileStorageNotFound } from "../../src/storage/storage.js";
|
|
13
17
|
// ---------------------------------------------------------------------------
|
|
14
18
|
// Test layer
|
|
15
19
|
// ---------------------------------------------------------------------------
|
|
@@ -22,8 +26,7 @@ const FileStorageTest = Layer.succeed(FileStorage, FileStorage.of({
|
|
|
22
26
|
list: () => Effect.succeed([])
|
|
23
27
|
}));
|
|
24
28
|
const sqliteLayer = SqliteClient.layer({ filename: ":memory:" });
|
|
25
|
-
const
|
|
26
|
-
const testLayer = Layer.mergeAll(collectionServiceWithDeps, sqliteLayer, FileStorageTest, NodeCryptoLayer);
|
|
29
|
+
const DialectTest = Layer.succeed(Dialect, sqliteDialect);
|
|
27
30
|
const noCtx = { headers: {}, query: {} };
|
|
28
31
|
// ---------------------------------------------------------------------------
|
|
29
32
|
// Table setup helpers
|
|
@@ -31,50 +34,50 @@ const noCtx = { headers: {}, query: {} };
|
|
|
31
34
|
// ---------------------------------------------------------------------------
|
|
32
35
|
const setupPostsTable = Effect.gen(function* () {
|
|
33
36
|
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
|
-
)
|
|
37
|
+
yield* sql.unsafe(`
|
|
38
|
+
CREATE TABLE IF NOT EXISTS "posts" (
|
|
39
|
+
"seqId" INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
40
|
+
"id" TEXT NOT NULL UNIQUE,
|
|
41
|
+
"title" TEXT NOT NULL,
|
|
42
|
+
"published" INTEGER NOT NULL DEFAULT 0,
|
|
43
|
+
"authorId" TEXT,
|
|
44
|
+
"created" TEXT NOT NULL,
|
|
45
|
+
"updated" TEXT NOT NULL
|
|
46
|
+
)
|
|
44
47
|
`);
|
|
45
48
|
yield* sql.unsafe(`DELETE FROM "posts"`);
|
|
46
49
|
});
|
|
47
50
|
// View collection tests: a base table + a SQLite VIEW over it
|
|
48
51
|
const setupViewTables = Effect.gen(function* () {
|
|
49
52
|
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
|
-
)
|
|
53
|
+
yield* sql.unsafe(`
|
|
54
|
+
CREATE TABLE IF NOT EXISTS "posts_base" (
|
|
55
|
+
"seqId" INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
56
|
+
"id" TEXT NOT NULL UNIQUE,
|
|
57
|
+
"title" TEXT NOT NULL,
|
|
58
|
+
"status" TEXT NOT NULL DEFAULT 'draft',
|
|
59
|
+
"created" TEXT NOT NULL,
|
|
60
|
+
"updated" TEXT NOT NULL
|
|
61
|
+
)
|
|
59
62
|
`);
|
|
60
63
|
yield* sql.unsafe(`DELETE FROM "posts_base"`);
|
|
61
64
|
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
|
+
yield* sql.unsafe(`
|
|
66
|
+
CREATE VIEW "active_posts" AS
|
|
67
|
+
SELECT seqId, id, title, status, created, updated FROM posts_base WHERE status = 'active'
|
|
65
68
|
`);
|
|
66
69
|
});
|
|
67
70
|
const setupScoredTable = Effect.gen(function* () {
|
|
68
71
|
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
|
-
)
|
|
72
|
+
yield* sql.unsafe(`
|
|
73
|
+
CREATE TABLE IF NOT EXISTS "scored" (
|
|
74
|
+
"seqId" INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
75
|
+
"id" TEXT NOT NULL UNIQUE,
|
|
76
|
+
"name" TEXT NOT NULL,
|
|
77
|
+
"score" REAL NOT NULL,
|
|
78
|
+
"created" TEXT NOT NULL,
|
|
79
|
+
"updated" TEXT NOT NULL
|
|
80
|
+
)
|
|
78
81
|
`);
|
|
79
82
|
yield* sql.unsafe(`DELETE FROM "scored"`);
|
|
80
83
|
});
|
|
@@ -82,45 +85,51 @@ const setupScoredTable = Effect.gen(function* () {
|
|
|
82
85
|
// Collection definitions
|
|
83
86
|
// ---------------------------------------------------------------------------
|
|
84
87
|
// Base collection — all operations public
|
|
85
|
-
const
|
|
88
|
+
const postsDef = BaseCollection.define("posts", {
|
|
86
89
|
title: Field.text({ maxLength: 100 }),
|
|
87
90
|
published: Field.boolean({ default: false }),
|
|
88
91
|
authorId: Field.text({ required: false })
|
|
89
|
-
})
|
|
92
|
+
});
|
|
93
|
+
const postsRules = defineRule(postsDef, (R) => ({
|
|
90
94
|
list: R.public(),
|
|
91
95
|
view: R.public(),
|
|
92
96
|
create: R.public(),
|
|
93
97
|
update: R.public(),
|
|
94
98
|
delete: R.public()
|
|
95
99
|
}));
|
|
100
|
+
const Posts = applyRulesToCollections([postsDef], [postsRules])[0];
|
|
96
101
|
// Base collection — rule-enforced: only authorId "user1" can access
|
|
97
|
-
const
|
|
102
|
+
const protectedPostsDef = BaseCollection.define("posts", {
|
|
98
103
|
title: Field.text(),
|
|
99
104
|
published: Field.boolean({ default: false }),
|
|
100
105
|
authorId: Field.text({ required: false })
|
|
101
|
-
})
|
|
106
|
+
});
|
|
107
|
+
const protectedPostsRules = defineRule(protectedPostsDef, (R) => ({
|
|
102
108
|
list: R.field("authorId").eq(R.literal("user1")),
|
|
103
109
|
view: R.field("authorId").eq(R.literal("user1")),
|
|
104
110
|
create: R.field("authorId").eq(R.literal("user1")),
|
|
105
111
|
update: R.field("authorId").eq(R.literal("user1")),
|
|
106
112
|
delete: R.field("authorId").eq(R.literal("user1"))
|
|
107
113
|
}));
|
|
114
|
+
const ProtectedPosts = applyRulesToCollections([protectedPostsDef], [protectedPostsRules])[0];
|
|
108
115
|
// Base collection — no rules (deny all by default)
|
|
109
116
|
const NoRulePosts = BaseCollection.define("posts", {
|
|
110
117
|
title: Field.text()
|
|
111
118
|
});
|
|
112
119
|
// View collection — public list/view, no create/update/delete
|
|
113
|
-
const
|
|
120
|
+
const activePostsDef = ViewCollection.define("active_posts", "SELECT seqId, id, title, status, created, updated FROM posts_base WHERE status = 'active'", {
|
|
114
121
|
seqId: Field.integer().view(),
|
|
115
122
|
id: Field.text().view(),
|
|
116
123
|
title: Field.text().view(),
|
|
117
124
|
status: Field.text().view()
|
|
118
|
-
})
|
|
125
|
+
});
|
|
126
|
+
const activePostsRules = defineRule(activePostsDef, (R) => ({
|
|
119
127
|
list: R.public(),
|
|
120
128
|
view: R.public()
|
|
121
129
|
}));
|
|
130
|
+
const ActivePosts = applyRulesToCollections([activePostsDef], [activePostsRules])[0];
|
|
122
131
|
// Collection with error callbacks for integration testing
|
|
123
|
-
const
|
|
132
|
+
const scoredDef = BaseCollection.define("scored", {
|
|
124
133
|
name: Field.text({
|
|
125
134
|
minLength: 2,
|
|
126
135
|
error: (kind) => Match.value(kind).pipe(Match.when("minLength", () => "Name too short"), Match.when("required", () => "Name is required"), Match.orElse(() => "Invalid name"))
|
|
@@ -130,16 +139,36 @@ const Scored = BaseCollection.define("scored", {
|
|
|
130
139
|
max: 100,
|
|
131
140
|
error: (kind) => Match.value(kind).pipe(Match.when("minimum", () => "Score below zero"), Match.when("maximum", () => "Score above 100"), Match.orElse(() => "Invalid score"))
|
|
132
141
|
})
|
|
133
|
-
})
|
|
142
|
+
});
|
|
143
|
+
const scoredRules = defineRule(scoredDef, (R) => ({
|
|
144
|
+
create: R.public(),
|
|
145
|
+
update: R.public(),
|
|
146
|
+
list: R.public(),
|
|
147
|
+
view: R.public(),
|
|
148
|
+
delete: R.public()
|
|
149
|
+
}));
|
|
150
|
+
const Scored = applyRulesToCollections([scoredDef], [scoredRules])[0];
|
|
151
|
+
// Collection with a literalText field
|
|
152
|
+
const rolesDef = BaseCollection.define("roles", {
|
|
153
|
+
role: Field.literalText({ literal: ["admin", "agent", "readonly"] })
|
|
154
|
+
});
|
|
155
|
+
const rolesRules = defineRule(rolesDef, (R) => ({
|
|
134
156
|
create: R.public(),
|
|
135
157
|
update: R.public(),
|
|
136
158
|
list: R.public(),
|
|
137
159
|
view: R.public(),
|
|
138
160
|
delete: R.public()
|
|
139
161
|
}));
|
|
162
|
+
const Roles = applyRulesToCollections([rolesDef], [rolesRules])[0];
|
|
163
|
+
// PostsServiceLayer wires Posts into the collection service (registers `published` for boolean encode/decode)
|
|
164
|
+
const collectionServiceWithDeps = makeCollectionServiceLayer([Posts]).pipe(Layer.provide(RepositoryLive), Layer.provide(sqliteLayer), Layer.provide(FileStorageTest), Layer.provide(NodeCryptoLayer), Layer.provide(DialectTest), Layer.provide(NodeAuthServiceLayer));
|
|
165
|
+
const testLayer = Layer.mergeAll(collectionServiceWithDeps, sqliteLayer, FileStorageTest, NodeCryptoLayer);
|
|
140
166
|
// 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));
|
|
167
|
+
const scoredServiceWithDeps = makeCollectionServiceLayer([Scored]).pipe(Layer.provide(RepositoryLive), Layer.provide(sqliteLayer), Layer.provide(FileStorageTest), Layer.provide(NodeCryptoLayer), Layer.provide(DialectTest), Layer.provide(NodeAuthServiceLayer));
|
|
142
168
|
const scoredTestLayer = Layer.mergeAll(scoredServiceWithDeps, sqliteLayer, FileStorageTest, NodeCryptoLayer);
|
|
169
|
+
// RolesServiceLayer wires Roles into the collection service
|
|
170
|
+
const rolesServiceWithDeps = makeCollectionServiceLayer([Roles]).pipe(Layer.provide(RepositoryLive), Layer.provide(sqliteLayer), Layer.provide(FileStorageTest), Layer.provide(NodeCryptoLayer), Layer.provide(DialectTest), Layer.provide(NodeAuthServiceLayer));
|
|
171
|
+
const rolesTestLayer = Layer.mergeAll(rolesServiceWithDeps, sqliteLayer, FileStorageTest, NodeCryptoLayer);
|
|
143
172
|
// ---------------------------------------------------------------------------
|
|
144
173
|
// Helpers
|
|
145
174
|
// ---------------------------------------------------------------------------
|
|
@@ -237,12 +266,12 @@ describe("list", () => {
|
|
|
237
266
|
it.effect("no rules → ForbiddenError", () => Effect.gen(function* () {
|
|
238
267
|
yield* setupPostsTable;
|
|
239
268
|
const svc = yield* CollectionService;
|
|
240
|
-
const result = yield* svc.list(NoRulePosts, null, 10, noCtx).pipe(Effect.
|
|
241
|
-
expect(
|
|
242
|
-
if (
|
|
243
|
-
expect(result.
|
|
244
|
-
if (result.
|
|
245
|
-
expect(result.
|
|
269
|
+
const result = yield* svc.list(NoRulePosts, null, 10, noCtx).pipe(Effect.result);
|
|
270
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
271
|
+
if (Result.isFailure(result)) {
|
|
272
|
+
expect(result.failure._tag).toBe("ForbiddenError");
|
|
273
|
+
if (result.failure._tag === "ForbiddenError") {
|
|
274
|
+
expect(result.failure.action).toBe("list");
|
|
246
275
|
}
|
|
247
276
|
}
|
|
248
277
|
}).pipe(Effect.provide(testLayer)));
|
|
@@ -408,10 +437,10 @@ describe("view", () => {
|
|
|
408
437
|
it.effect("returns NotFoundError for an unknown id", () => Effect.gen(function* () {
|
|
409
438
|
yield* setupPostsTable;
|
|
410
439
|
const svc = yield* CollectionService;
|
|
411
|
-
const result = yield* svc.view(Posts, "nonexistent123", noCtx).pipe(Effect.
|
|
412
|
-
expect(
|
|
413
|
-
if (
|
|
414
|
-
expect(result.
|
|
440
|
+
const result = yield* svc.view(Posts, "nonexistent123", noCtx).pipe(Effect.result);
|
|
441
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
442
|
+
if (Result.isFailure(result)) {
|
|
443
|
+
expect(result.failure._tag).toBe("NotFoundError");
|
|
415
444
|
}
|
|
416
445
|
}).pipe(Effect.provide(testLayer)));
|
|
417
446
|
it.effect("seqId is NOT in the returned row", () => Effect.gen(function* () {
|
|
@@ -431,19 +460,19 @@ describe("view", () => {
|
|
|
431
460
|
const id = created["id"];
|
|
432
461
|
if (typeof id !== "string")
|
|
433
462
|
throw new Error("expected string id");
|
|
434
|
-
const result = yield* svc.view(ProtectedPosts, id, noCtx).pipe(Effect.
|
|
435
|
-
expect(
|
|
436
|
-
if (
|
|
437
|
-
expect(result.
|
|
463
|
+
const result = yield* svc.view(ProtectedPosts, id, noCtx).pipe(Effect.result);
|
|
464
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
465
|
+
if (Result.isFailure(result)) {
|
|
466
|
+
expect(result.failure._tag).toBe("NotFoundError");
|
|
438
467
|
}
|
|
439
468
|
}).pipe(Effect.provide(testLayer)));
|
|
440
469
|
it.effect("no rules → ForbiddenError", () => Effect.gen(function* () {
|
|
441
470
|
yield* setupPostsTable;
|
|
442
471
|
const svc = yield* CollectionService;
|
|
443
|
-
const result = yield* svc.view(NoRulePosts, "any", noCtx).pipe(Effect.
|
|
444
|
-
expect(
|
|
445
|
-
if (
|
|
446
|
-
expect(result.
|
|
472
|
+
const result = yield* svc.view(NoRulePosts, "any", noCtx).pipe(Effect.result);
|
|
473
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
474
|
+
if (Result.isFailure(result)) {
|
|
475
|
+
expect(result.failure._tag).toBe("ForbiddenError");
|
|
447
476
|
}
|
|
448
477
|
}).pipe(Effect.provide(testLayer)));
|
|
449
478
|
});
|
|
@@ -466,46 +495,58 @@ describe("create", () => {
|
|
|
466
495
|
const result = yield* svc.create(Posts, { title: "X" }, noCtx);
|
|
467
496
|
expect("seqId" in result).toBe(false);
|
|
468
497
|
}).pipe(Effect.provide(testLayer)));
|
|
498
|
+
it.effect("boolean field round-trips through SQLite encode/decode (true)", () => Effect.gen(function* () {
|
|
499
|
+
yield* setupPostsTable;
|
|
500
|
+
const svc = yield* CollectionService;
|
|
501
|
+
const result = yield* svc.create(Posts, { title: "Bool true", published: true }, noCtx);
|
|
502
|
+
expect(result["published"]).toBe(true);
|
|
503
|
+
}).pipe(Effect.provide(testLayer)));
|
|
504
|
+
it.effect("boolean field round-trips through SQLite encode/decode (false)", () => Effect.gen(function* () {
|
|
505
|
+
yield* setupPostsTable;
|
|
506
|
+
const svc = yield* CollectionService;
|
|
507
|
+
const result = yield* svc.create(Posts, { title: "Bool false", published: false }, noCtx);
|
|
508
|
+
expect(result["published"]).toBe(false);
|
|
509
|
+
}).pipe(Effect.provide(testLayer)));
|
|
469
510
|
it.effect("ValidationError when required field is missing", () => Effect.gen(function* () {
|
|
470
511
|
yield* setupPostsTable;
|
|
471
512
|
const svc = yield* CollectionService;
|
|
472
|
-
const result = yield* svc.create(Posts, { published: true }, noCtx).pipe(Effect.
|
|
473
|
-
expect(
|
|
474
|
-
if (
|
|
475
|
-
expect(result.
|
|
476
|
-
if (result.
|
|
477
|
-
expect(result.
|
|
513
|
+
const result = yield* svc.create(Posts, { published: true }, noCtx).pipe(Effect.result);
|
|
514
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
515
|
+
if (Result.isFailure(result)) {
|
|
516
|
+
expect(result.failure._tag).toBe("ValidationError");
|
|
517
|
+
if (result.failure._tag === "ValidationError") {
|
|
518
|
+
expect(result.failure.issues.some((i) => i.includes("title"))).toBe(true);
|
|
478
519
|
}
|
|
479
520
|
}
|
|
480
521
|
}).pipe(Effect.provide(testLayer)));
|
|
481
522
|
it.effect("ValidationError when x-system field is in input", () => Effect.gen(function* () {
|
|
482
523
|
yield* setupPostsTable;
|
|
483
524
|
const svc = yield* CollectionService;
|
|
484
|
-
const result = yield* svc.create(Posts, { title: "X", id: "hacked" }, noCtx).pipe(Effect.
|
|
485
|
-
expect(
|
|
486
|
-
if (
|
|
487
|
-
expect(result.
|
|
488
|
-
if (result.
|
|
489
|
-
expect(result.
|
|
525
|
+
const result = yield* svc.create(Posts, { title: "X", id: "hacked" }, noCtx).pipe(Effect.result);
|
|
526
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
527
|
+
if (Result.isFailure(result)) {
|
|
528
|
+
expect(result.failure._tag).toBe("ValidationError");
|
|
529
|
+
if (result.failure._tag === "ValidationError") {
|
|
530
|
+
expect(result.failure.issues.some((i) => i.includes("id"))).toBe(true);
|
|
490
531
|
}
|
|
491
532
|
}
|
|
492
533
|
}).pipe(Effect.provide(testLayer)));
|
|
493
534
|
it.effect("ValidationError when seqId is in input", () => Effect.gen(function* () {
|
|
494
535
|
yield* setupPostsTable;
|
|
495
536
|
const svc = yield* CollectionService;
|
|
496
|
-
const result = yield* svc.create(Posts, { title: "X", seqId: 99 }, noCtx).pipe(Effect.
|
|
497
|
-
expect(
|
|
498
|
-
if (
|
|
499
|
-
expect(result.
|
|
537
|
+
const result = yield* svc.create(Posts, { title: "X", seqId: 99 }, noCtx).pipe(Effect.result);
|
|
538
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
539
|
+
if (Result.isFailure(result)) {
|
|
540
|
+
expect(result.failure._tag).toBe("ValidationError");
|
|
500
541
|
}
|
|
501
542
|
}).pipe(Effect.provide(testLayer)));
|
|
502
543
|
it.effect("ValidationError when type is wrong", () => Effect.gen(function* () {
|
|
503
544
|
yield* setupPostsTable;
|
|
504
545
|
const svc = yield* CollectionService;
|
|
505
|
-
const result = yield* svc.create(Posts, { title: 42 }, noCtx).pipe(Effect.
|
|
506
|
-
expect(
|
|
507
|
-
if (
|
|
508
|
-
expect(result.
|
|
546
|
+
const result = yield* svc.create(Posts, { title: 42 }, noCtx).pipe(Effect.result);
|
|
547
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
548
|
+
if (Result.isFailure(result)) {
|
|
549
|
+
expect(result.failure._tag).toBe("ValidationError");
|
|
509
550
|
}
|
|
510
551
|
}).pipe(Effect.provide(testLayer)));
|
|
511
552
|
it.effect("ValidationError when string exceeds maxLength", () => Effect.gen(function* () {
|
|
@@ -513,12 +554,12 @@ describe("create", () => {
|
|
|
513
554
|
const svc = yield* CollectionService;
|
|
514
555
|
const result = yield* svc
|
|
515
556
|
.create(Posts, { title: "x".repeat(101) }, noCtx)
|
|
516
|
-
.pipe(Effect.
|
|
517
|
-
expect(
|
|
518
|
-
if (
|
|
519
|
-
expect(result.
|
|
520
|
-
if (result.
|
|
521
|
-
expect(result.
|
|
557
|
+
.pipe(Effect.result);
|
|
558
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
559
|
+
if (Result.isFailure(result)) {
|
|
560
|
+
expect(result.failure._tag).toBe("ValidationError");
|
|
561
|
+
if (result.failure._tag === "ValidationError") {
|
|
562
|
+
expect(result.failure.issues.some((i) => i.includes("100"))).toBe(true);
|
|
522
563
|
}
|
|
523
564
|
}
|
|
524
565
|
}).pipe(Effect.provide(testLayer)));
|
|
@@ -528,10 +569,10 @@ describe("create", () => {
|
|
|
528
569
|
// ProtectedPosts requires authorId = "user1"; supply "user2" → rule fails
|
|
529
570
|
const result = yield* svc
|
|
530
571
|
.create(ProtectedPosts, { title: "X", authorId: "user2" }, noCtx)
|
|
531
|
-
.pipe(Effect.
|
|
532
|
-
expect(
|
|
533
|
-
if (
|
|
534
|
-
expect(result.
|
|
572
|
+
.pipe(Effect.result);
|
|
573
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
574
|
+
if (Result.isFailure(result)) {
|
|
575
|
+
expect(result.failure._tag).toBe("ForbiddenError");
|
|
535
576
|
}
|
|
536
577
|
}).pipe(Effect.provide(testLayer)));
|
|
537
578
|
it.effect("create succeeds when rule pre-check passes", () => Effect.gen(function* () {
|
|
@@ -543,10 +584,10 @@ describe("create", () => {
|
|
|
543
584
|
it.effect("no rules → ForbiddenError", () => Effect.gen(function* () {
|
|
544
585
|
yield* setupPostsTable;
|
|
545
586
|
const svc = yield* CollectionService;
|
|
546
|
-
const result = yield* svc.create(NoRulePosts, { title: "X" }, noCtx).pipe(Effect.
|
|
547
|
-
expect(
|
|
548
|
-
if (
|
|
549
|
-
expect(result.
|
|
587
|
+
const result = yield* svc.create(NoRulePosts, { title: "X" }, noCtx).pipe(Effect.result);
|
|
588
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
589
|
+
if (Result.isFailure(result)) {
|
|
590
|
+
expect(result.failure._tag).toBe("ForbiddenError");
|
|
550
591
|
}
|
|
551
592
|
}).pipe(Effect.provide(testLayer)));
|
|
552
593
|
it.effect("view collection → ReadOnlyError", () => Effect.gen(function* () {
|
|
@@ -554,10 +595,10 @@ describe("create", () => {
|
|
|
554
595
|
const svc = yield* CollectionService;
|
|
555
596
|
const result = yield* svc
|
|
556
597
|
.create(ActivePosts, { id: "x", title: "X", status: "active" }, noCtx)
|
|
557
|
-
.pipe(Effect.
|
|
558
|
-
expect(
|
|
559
|
-
if (
|
|
560
|
-
expect(result.
|
|
598
|
+
.pipe(Effect.result);
|
|
599
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
600
|
+
if (Result.isFailure(result)) {
|
|
601
|
+
expect(result.failure._tag).toBe("ReadOnlyError");
|
|
561
602
|
}
|
|
562
603
|
}).pipe(Effect.provide(testLayer)));
|
|
563
604
|
});
|
|
@@ -576,6 +617,16 @@ describe("update", () => {
|
|
|
576
617
|
expect(updated["title"]).toBe("New");
|
|
577
618
|
expect(updated["authorId"]).toBe("user1");
|
|
578
619
|
}).pipe(Effect.provide(testLayer)));
|
|
620
|
+
it.effect("toggling a boolean field updates and round-trips correctly", () => Effect.gen(function* () {
|
|
621
|
+
yield* setupPostsTable;
|
|
622
|
+
const svc = yield* CollectionService;
|
|
623
|
+
const created = yield* svc.create(Posts, { title: "Toggle", published: false }, noCtx);
|
|
624
|
+
const id = created["id"];
|
|
625
|
+
if (typeof id !== "string")
|
|
626
|
+
throw new Error("expected string id");
|
|
627
|
+
const updated = yield* svc.update(Posts, id, { published: true }, noCtx);
|
|
628
|
+
expect(updated["published"]).toBe(true);
|
|
629
|
+
}).pipe(Effect.provide(testLayer)));
|
|
579
630
|
it.live("updated timestamp is refreshed; created is unchanged", () => Effect.gen(function* () {
|
|
580
631
|
yield* setupPostsTable;
|
|
581
632
|
const svc = yield* CollectionService;
|
|
@@ -597,10 +648,10 @@ describe("update", () => {
|
|
|
597
648
|
throw new Error("expected string id");
|
|
598
649
|
const result = yield* svc
|
|
599
650
|
.update(Posts, id, { title: "Y", id: "hacked", created: "1970" }, noCtx)
|
|
600
|
-
.pipe(Effect.
|
|
601
|
-
expect(
|
|
602
|
-
if (
|
|
603
|
-
expect(result.
|
|
651
|
+
.pipe(Effect.result);
|
|
652
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
653
|
+
if (Result.isFailure(result)) {
|
|
654
|
+
expect(result.failure._tag).toBe("ValidationError");
|
|
604
655
|
}
|
|
605
656
|
}).pipe(Effect.provide(testLayer)));
|
|
606
657
|
it.effect("NotFoundError for unknown id", () => Effect.gen(function* () {
|
|
@@ -608,10 +659,10 @@ describe("update", () => {
|
|
|
608
659
|
const svc = yield* CollectionService;
|
|
609
660
|
const result = yield* svc
|
|
610
661
|
.update(Posts, "nosuchid1234567", { title: "X" }, noCtx)
|
|
611
|
-
.pipe(Effect.
|
|
612
|
-
expect(
|
|
613
|
-
if (
|
|
614
|
-
expect(result.
|
|
662
|
+
.pipe(Effect.result);
|
|
663
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
664
|
+
if (Result.isFailure(result)) {
|
|
665
|
+
expect(result.failure._tag).toBe("NotFoundError");
|
|
615
666
|
}
|
|
616
667
|
}).pipe(Effect.provide(testLayer)));
|
|
617
668
|
it.effect("ForbiddenError when rule blocks update", () => Effect.gen(function* () {
|
|
@@ -624,12 +675,12 @@ describe("update", () => {
|
|
|
624
675
|
throw new Error("expected string id");
|
|
625
676
|
const result = yield* svc
|
|
626
677
|
.update(ProtectedPosts, id, { title: "Y" }, noCtx)
|
|
627
|
-
.pipe(Effect.
|
|
628
|
-
expect(
|
|
629
|
-
if (
|
|
630
|
-
expect(result.
|
|
631
|
-
if (result.
|
|
632
|
-
expect(result.
|
|
678
|
+
.pipe(Effect.result);
|
|
679
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
680
|
+
if (Result.isFailure(result)) {
|
|
681
|
+
expect(result.failure._tag).toBe("ForbiddenError");
|
|
682
|
+
if (result.failure._tag === "ForbiddenError") {
|
|
683
|
+
expect(result.failure.action).toBe("update");
|
|
633
684
|
}
|
|
634
685
|
}
|
|
635
686
|
}).pipe(Effect.provide(testLayer)));
|
|
@@ -638,10 +689,10 @@ describe("update", () => {
|
|
|
638
689
|
const svc = yield* CollectionService;
|
|
639
690
|
const result = yield* svc
|
|
640
691
|
.update(NoRulePosts, "anyid", { title: "X" }, noCtx)
|
|
641
|
-
.pipe(Effect.
|
|
642
|
-
expect(
|
|
643
|
-
if (
|
|
644
|
-
expect(result.
|
|
692
|
+
.pipe(Effect.result);
|
|
693
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
694
|
+
if (Result.isFailure(result)) {
|
|
695
|
+
expect(result.failure._tag).toBe("ForbiddenError");
|
|
645
696
|
}
|
|
646
697
|
}).pipe(Effect.provide(testLayer)));
|
|
647
698
|
it.effect("view collection → ReadOnlyError", () => Effect.gen(function* () {
|
|
@@ -649,10 +700,10 @@ describe("update", () => {
|
|
|
649
700
|
const svc = yield* CollectionService;
|
|
650
701
|
const result = yield* svc
|
|
651
702
|
.update(ActivePosts, "anyid", { title: "X" }, noCtx)
|
|
652
|
-
.pipe(Effect.
|
|
653
|
-
expect(
|
|
654
|
-
if (
|
|
655
|
-
expect(result.
|
|
703
|
+
.pipe(Effect.result);
|
|
704
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
705
|
+
if (Result.isFailure(result)) {
|
|
706
|
+
expect(result.failure._tag).toBe("ReadOnlyError");
|
|
656
707
|
}
|
|
657
708
|
}).pipe(Effect.provide(testLayer)));
|
|
658
709
|
});
|
|
@@ -677,19 +728,19 @@ describe("delete", () => {
|
|
|
677
728
|
if (typeof id !== "string")
|
|
678
729
|
throw new Error("expected string id");
|
|
679
730
|
yield* svc.delete(Posts, id, noCtx);
|
|
680
|
-
const result = yield* svc.view(Posts, id, noCtx).pipe(Effect.
|
|
681
|
-
expect(
|
|
682
|
-
if (
|
|
683
|
-
expect(result.
|
|
731
|
+
const result = yield* svc.view(Posts, id, noCtx).pipe(Effect.result);
|
|
732
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
733
|
+
if (Result.isFailure(result)) {
|
|
734
|
+
expect(result.failure._tag).toBe("NotFoundError");
|
|
684
735
|
}
|
|
685
736
|
}).pipe(Effect.provide(testLayer)));
|
|
686
737
|
it.effect("NotFoundError for unknown id", () => Effect.gen(function* () {
|
|
687
738
|
yield* setupPostsTable;
|
|
688
739
|
const svc = yield* CollectionService;
|
|
689
|
-
const result = yield* svc.delete(Posts, "nosuchid1234567", noCtx).pipe(Effect.
|
|
690
|
-
expect(
|
|
691
|
-
if (
|
|
692
|
-
expect(result.
|
|
740
|
+
const result = yield* svc.delete(Posts, "nosuchid1234567", noCtx).pipe(Effect.result);
|
|
741
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
742
|
+
if (Result.isFailure(result)) {
|
|
743
|
+
expect(result.failure._tag).toBe("NotFoundError");
|
|
693
744
|
}
|
|
694
745
|
}).pipe(Effect.provide(testLayer)));
|
|
695
746
|
it.effect("ForbiddenError when rule blocks delete", () => Effect.gen(function* () {
|
|
@@ -701,31 +752,31 @@ describe("delete", () => {
|
|
|
701
752
|
throw new Error("expected string id");
|
|
702
753
|
const result = yield* svc
|
|
703
754
|
.delete(ProtectedPosts, id, noCtx)
|
|
704
|
-
.pipe(Effect.
|
|
705
|
-
expect(
|
|
706
|
-
if (
|
|
707
|
-
expect(result.
|
|
708
|
-
if (result.
|
|
709
|
-
expect(result.
|
|
755
|
+
.pipe(Effect.result);
|
|
756
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
757
|
+
if (Result.isFailure(result)) {
|
|
758
|
+
expect(result.failure._tag).toBe("ForbiddenError");
|
|
759
|
+
if (result.failure._tag === "ForbiddenError") {
|
|
760
|
+
expect(result.failure.action).toBe("delete");
|
|
710
761
|
}
|
|
711
762
|
}
|
|
712
763
|
}).pipe(Effect.provide(testLayer)));
|
|
713
764
|
it.effect("no rules → ForbiddenError", () => Effect.gen(function* () {
|
|
714
765
|
yield* setupPostsTable;
|
|
715
766
|
const svc = yield* CollectionService;
|
|
716
|
-
const result = yield* svc.delete(NoRulePosts, "anyid", noCtx).pipe(Effect.
|
|
717
|
-
expect(
|
|
718
|
-
if (
|
|
719
|
-
expect(result.
|
|
767
|
+
const result = yield* svc.delete(NoRulePosts, "anyid", noCtx).pipe(Effect.result);
|
|
768
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
769
|
+
if (Result.isFailure(result)) {
|
|
770
|
+
expect(result.failure._tag).toBe("ForbiddenError");
|
|
720
771
|
}
|
|
721
772
|
}).pipe(Effect.provide(testLayer)));
|
|
722
773
|
it.effect("view collection → ReadOnlyError", () => Effect.gen(function* () {
|
|
723
774
|
yield* setupViewTables;
|
|
724
775
|
const svc = yield* CollectionService;
|
|
725
|
-
const result = yield* svc.delete(ActivePosts, "anyid", noCtx).pipe(Effect.
|
|
726
|
-
expect(
|
|
727
|
-
if (
|
|
728
|
-
expect(result.
|
|
776
|
+
const result = yield* svc.delete(ActivePosts, "anyid", noCtx).pipe(Effect.result);
|
|
777
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
778
|
+
if (Result.isFailure(result)) {
|
|
779
|
+
expect(result.failure._tag).toBe("ReadOnlyError");
|
|
729
780
|
}
|
|
730
781
|
}).pipe(Effect.provide(testLayer)));
|
|
731
782
|
});
|
|
@@ -736,46 +787,46 @@ describe("error callback — integration", () => {
|
|
|
736
787
|
it.effect("minLength message propagates from create", () => Effect.gen(function* () {
|
|
737
788
|
yield* setupScoredTable;
|
|
738
789
|
const svc = yield* CollectionService;
|
|
739
|
-
const result = yield* svc.create(Scored, { name: "x", score: 50 }, noCtx).pipe(Effect.
|
|
740
|
-
expect(
|
|
741
|
-
if (
|
|
742
|
-
expect(result.
|
|
790
|
+
const result = yield* svc.create(Scored, { name: "x", score: 50 }, noCtx).pipe(Effect.result);
|
|
791
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
792
|
+
if (Result.isFailure(result) && result.failure._tag === "ValidationError") {
|
|
793
|
+
expect(result.failure.issues.some((i) => i.includes("Name too short"))).toBe(true);
|
|
743
794
|
}
|
|
744
795
|
}).pipe(Effect.provide(scoredTestLayer)));
|
|
745
796
|
it.effect("minimum message propagates from create", () => Effect.gen(function* () {
|
|
746
797
|
yield* setupScoredTable;
|
|
747
798
|
const svc = yield* CollectionService;
|
|
748
|
-
const result = yield* svc.create(Scored, { name: "ok", score: -1 }, noCtx).pipe(Effect.
|
|
749
|
-
expect(
|
|
750
|
-
if (
|
|
751
|
-
expect(result.
|
|
799
|
+
const result = yield* svc.create(Scored, { name: "ok", score: -1 }, noCtx).pipe(Effect.result);
|
|
800
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
801
|
+
if (Result.isFailure(result) && result.failure._tag === "ValidationError") {
|
|
802
|
+
expect(result.failure.issues.some((i) => i.includes("Score below zero"))).toBe(true);
|
|
752
803
|
}
|
|
753
804
|
}).pipe(Effect.provide(scoredTestLayer)));
|
|
754
805
|
it.effect("maximum message propagates from create", () => Effect.gen(function* () {
|
|
755
806
|
yield* setupScoredTable;
|
|
756
807
|
const svc = yield* CollectionService;
|
|
757
|
-
const result = yield* svc.create(Scored, { name: "ok", score: 200 }, noCtx).pipe(Effect.
|
|
758
|
-
expect(
|
|
759
|
-
if (
|
|
760
|
-
expect(result.
|
|
808
|
+
const result = yield* svc.create(Scored, { name: "ok", score: 200 }, noCtx).pipe(Effect.result);
|
|
809
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
810
|
+
if (Result.isFailure(result) && result.failure._tag === "ValidationError") {
|
|
811
|
+
expect(result.failure.issues.some((i) => i.includes("Score above 100"))).toBe(true);
|
|
761
812
|
}
|
|
762
813
|
}).pipe(Effect.provide(scoredTestLayer)));
|
|
763
814
|
it.effect("required message propagates from create", () => Effect.gen(function* () {
|
|
764
815
|
yield* setupScoredTable;
|
|
765
816
|
const svc = yield* CollectionService;
|
|
766
|
-
const result = yield* svc.create(Scored, { score: 50 }, noCtx).pipe(Effect.
|
|
767
|
-
expect(
|
|
768
|
-
if (
|
|
769
|
-
expect(result.
|
|
817
|
+
const result = yield* svc.create(Scored, { score: 50 }, noCtx).pipe(Effect.result);
|
|
818
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
819
|
+
if (Result.isFailure(result) && result.failure._tag === "ValidationError") {
|
|
820
|
+
expect(result.failure.issues.some((i) => i.includes("Name is required"))).toBe(true);
|
|
770
821
|
}
|
|
771
822
|
}).pipe(Effect.provide(scoredTestLayer)));
|
|
772
823
|
it.effect("Match.orElse fallback fires for type mismatch", () => Effect.gen(function* () {
|
|
773
824
|
yield* setupScoredTable;
|
|
774
825
|
const svc = yield* CollectionService;
|
|
775
|
-
const result = yield* svc.create(Scored, { name: 999, score: 50 }, noCtx).pipe(Effect.
|
|
776
|
-
expect(
|
|
777
|
-
if (
|
|
778
|
-
expect(result.
|
|
826
|
+
const result = yield* svc.create(Scored, { name: 999, score: 50 }, noCtx).pipe(Effect.result);
|
|
827
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
828
|
+
if (Result.isFailure(result) && result.failure._tag === "ValidationError") {
|
|
829
|
+
expect(result.failure.issues.some((i) => i.includes("Invalid name"))).toBe(true);
|
|
779
830
|
}
|
|
780
831
|
}).pipe(Effect.provide(scoredTestLayer)));
|
|
781
832
|
it.effect("update with too-short name produces default message (no custom error on update path)", () => Effect.gen(function* () {
|
|
@@ -785,12 +836,12 @@ describe("error callback — integration", () => {
|
|
|
785
836
|
const id = created["id"];
|
|
786
837
|
if (typeof id !== "string")
|
|
787
838
|
throw new Error("expected string id");
|
|
788
|
-
const result = yield* svc.update(Scored, id, { name: "x" }, noCtx).pipe(Effect.
|
|
789
|
-
expect(
|
|
790
|
-
if (
|
|
791
|
-
expect(result.
|
|
792
|
-
if (result.
|
|
793
|
-
expect(result.
|
|
839
|
+
const result = yield* svc.update(Scored, id, { name: "x" }, noCtx).pipe(Effect.result);
|
|
840
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
841
|
+
if (Result.isFailure(result)) {
|
|
842
|
+
expect(result.failure._tag).toBe("ValidationError");
|
|
843
|
+
if (result.failure._tag === "ValidationError") {
|
|
844
|
+
expect(result.failure.issues.length).toBeGreaterThan(0);
|
|
794
845
|
}
|
|
795
846
|
}
|
|
796
847
|
}).pipe(Effect.provide(scoredTestLayer)));
|
|
@@ -802,4 +853,153 @@ describe("error callback — integration", () => {
|
|
|
802
853
|
expect(result["score"]).toBe(50);
|
|
803
854
|
}).pipe(Effect.provide(scoredTestLayer)));
|
|
804
855
|
});
|
|
856
|
+
// ---------------------------------------------------------------------------
|
|
857
|
+
// literalText integration tests
|
|
858
|
+
// ---------------------------------------------------------------------------
|
|
859
|
+
const setupRolesTable = Effect.gen(function* () {
|
|
860
|
+
const sql = yield* SqlClient.SqlClient;
|
|
861
|
+
yield* sql.unsafe(`
|
|
862
|
+
CREATE TABLE IF NOT EXISTS "roles" (
|
|
863
|
+
"seqId" INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
864
|
+
"id" TEXT NOT NULL UNIQUE,
|
|
865
|
+
"role" TEXT NOT NULL,
|
|
866
|
+
"created" TEXT NOT NULL,
|
|
867
|
+
"updated" TEXT NOT NULL
|
|
868
|
+
)
|
|
869
|
+
`);
|
|
870
|
+
yield* sql.unsafe(`DELETE FROM "roles"`);
|
|
871
|
+
});
|
|
872
|
+
describe("literalText integration", () => {
|
|
873
|
+
it.effect("create with valid literal value succeeds", () => Effect.gen(function* () {
|
|
874
|
+
yield* setupRolesTable;
|
|
875
|
+
const svc = yield* CollectionService;
|
|
876
|
+
const result = yield* svc.create(Roles, { role: "admin" }, noCtx);
|
|
877
|
+
expect(result["role"]).toBe("admin");
|
|
878
|
+
}).pipe(Effect.provide(rolesTestLayer)));
|
|
879
|
+
it.effect("create with invalid literal value fails with ValidationError", () => Effect.gen(function* () {
|
|
880
|
+
yield* setupRolesTable;
|
|
881
|
+
const svc = yield* CollectionService;
|
|
882
|
+
const result = yield* svc.create(Roles, { role: "superadmin" }, noCtx).pipe(Effect.result);
|
|
883
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
884
|
+
if (Result.isFailure(result)) {
|
|
885
|
+
expect(result.failure._tag).toBe("ValidationError");
|
|
886
|
+
}
|
|
887
|
+
}).pipe(Effect.provide(rolesTestLayer)));
|
|
888
|
+
it.effect("update with invalid literal value fails with ValidationError", () => Effect.gen(function* () {
|
|
889
|
+
yield* setupRolesTable;
|
|
890
|
+
const svc = yield* CollectionService;
|
|
891
|
+
const created = yield* svc.create(Roles, { role: "admin" }, noCtx);
|
|
892
|
+
const id = created["id"];
|
|
893
|
+
if (typeof id !== "string")
|
|
894
|
+
throw new Error("expected string id");
|
|
895
|
+
const result = yield* svc.update(Roles, id, { role: "superadmin" }, noCtx).pipe(Effect.result);
|
|
896
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
897
|
+
if (Result.isFailure(result)) {
|
|
898
|
+
expect(result.failure._tag).toBe("ValidationError");
|
|
899
|
+
}
|
|
900
|
+
}).pipe(Effect.provide(rolesTestLayer)));
|
|
901
|
+
it.effect("update with valid literal value succeeds", () => Effect.gen(function* () {
|
|
902
|
+
yield* setupRolesTable;
|
|
903
|
+
const svc = yield* CollectionService;
|
|
904
|
+
const created = yield* svc.create(Roles, { role: "admin" }, noCtx);
|
|
905
|
+
const id = created["id"];
|
|
906
|
+
if (typeof id !== "string")
|
|
907
|
+
throw new Error("expected string id");
|
|
908
|
+
const updated = yield* svc.update(Roles, id, { role: "agent" }, noCtx);
|
|
909
|
+
expect(updated["role"]).toBe("agent");
|
|
910
|
+
}).pipe(Effect.provide(rolesTestLayer)));
|
|
911
|
+
});
|
|
912
|
+
// ---------------------------------------------------------------------------
|
|
913
|
+
// AuthCollection create — regression: email/password were blocked by x-system
|
|
914
|
+
// ---------------------------------------------------------------------------
|
|
915
|
+
const superAdminDef = AuthCollection.define("_superadmin", {});
|
|
916
|
+
const superAdminRules = defineRule(superAdminDef, (R) => ({
|
|
917
|
+
list: R.field("email").eq(R.literal("")),
|
|
918
|
+
view: R.field("email").eq(R.literal("")),
|
|
919
|
+
create: R.public(),
|
|
920
|
+
update: R.field("email").eq(R.literal("")),
|
|
921
|
+
delete: R.field("email").eq(R.literal(""))
|
|
922
|
+
}));
|
|
923
|
+
const SuperAdmin = applyRulesToCollections([superAdminDef], [superAdminRules])[0];
|
|
924
|
+
const setupSuperAdminTable = Effect.gen(function* () {
|
|
925
|
+
const sql = yield* SqlClient.SqlClient;
|
|
926
|
+
yield* sql.unsafe(`
|
|
927
|
+
CREATE TABLE IF NOT EXISTS "_superadmin" (
|
|
928
|
+
"seqId" INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
929
|
+
"id" TEXT NOT NULL UNIQUE,
|
|
930
|
+
"email" TEXT NOT NULL UNIQUE,
|
|
931
|
+
"password" TEXT NOT NULL,
|
|
932
|
+
"emailVerified" INTEGER NOT NULL DEFAULT 0,
|
|
933
|
+
"created" TEXT NOT NULL,
|
|
934
|
+
"updated" TEXT NOT NULL
|
|
935
|
+
)
|
|
936
|
+
`);
|
|
937
|
+
yield* sql.unsafe(`DELETE FROM "_superadmin"`);
|
|
938
|
+
});
|
|
939
|
+
const superAdminTestLayer = Layer.mergeAll(makeCollectionServiceLayer([SuperAdmin]).pipe(Layer.provide(RepositoryLive), Layer.provide(sqliteLayer), Layer.provide(FileStorageTest), Layer.provide(NodeCryptoLayer), Layer.provide(DialectTest), Layer.provide(NodeAuthServiceLayer)), sqliteLayer, FileStorageTest, NodeCryptoLayer, NodeAuthServiceLayer);
|
|
940
|
+
const adminCtx = { headers: {}, query: {}, admin: true };
|
|
941
|
+
describe("AuthCollection create", () => {
|
|
942
|
+
it.effect("succeeds with email and password (regression: was blocked by x-system check)", () => Effect.gen(function* () {
|
|
943
|
+
yield* setupSuperAdminTable;
|
|
944
|
+
const svc = yield* CollectionService;
|
|
945
|
+
const record = yield* svc.create(SuperAdmin, { email: "admin@example.com", password: "hashed-pw" }, adminCtx);
|
|
946
|
+
expect(record["email"]).toBe("admin@example.com");
|
|
947
|
+
expect(typeof record["id"]).toBe("string");
|
|
948
|
+
expect(record["id"]).not.toBe("");
|
|
949
|
+
}).pipe(Effect.provide(superAdminTestLayer)));
|
|
950
|
+
it.effect("rejects attempt to set generated field id", () => Effect.gen(function* () {
|
|
951
|
+
yield* setupSuperAdminTable;
|
|
952
|
+
const svc = yield* CollectionService;
|
|
953
|
+
const result = yield* svc.create(SuperAdmin, { email: "a@b.com", password: "pw", id: "custom-id" }, adminCtx).pipe(Effect.result);
|
|
954
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
955
|
+
if (Result.isFailure(result)) {
|
|
956
|
+
expect(result.failure._tag).toBe("ValidationError");
|
|
957
|
+
}
|
|
958
|
+
}).pipe(Effect.provide(superAdminTestLayer)));
|
|
959
|
+
it.effect("fails validation when email is missing", () => Effect.gen(function* () {
|
|
960
|
+
yield* setupSuperAdminTable;
|
|
961
|
+
const svc = yield* CollectionService;
|
|
962
|
+
const result = yield* svc.create(SuperAdmin, { password: "pw" }, adminCtx).pipe(Effect.result);
|
|
963
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
964
|
+
if (Result.isFailure(result)) {
|
|
965
|
+
expect(result.failure._tag).toBe("ValidationError");
|
|
966
|
+
}
|
|
967
|
+
}).pipe(Effect.provide(superAdminTestLayer)));
|
|
968
|
+
it.effect("fails validation when email format is invalid", () => Effect.gen(function* () {
|
|
969
|
+
yield* setupSuperAdminTable;
|
|
970
|
+
const svc = yield* CollectionService;
|
|
971
|
+
const result = yield* svc.create(SuperAdmin, { email: "notanemail", password: "pw" }, adminCtx).pipe(Effect.result);
|
|
972
|
+
expect(Result.isFailure(result)).toBe(true);
|
|
973
|
+
if (Result.isFailure(result)) {
|
|
974
|
+
expect(result.failure._tag).toBe("ValidationError");
|
|
975
|
+
}
|
|
976
|
+
}).pipe(Effect.provide(superAdminTestLayer)));
|
|
977
|
+
it.effect("hashes the password on create — the stored value is not the plaintext", () => Effect.gen(function* () {
|
|
978
|
+
yield* setupSuperAdminTable;
|
|
979
|
+
const svc = yield* CollectionService;
|
|
980
|
+
const record = yield* svc.create(SuperAdmin, { email: "hash@example.com", password: "plaintext-pw" }, adminCtx);
|
|
981
|
+
const stored = record["password"];
|
|
982
|
+
expect(typeof stored).toBe("string");
|
|
983
|
+
expect(stored).not.toBe("plaintext-pw");
|
|
984
|
+
expect(String(stored).startsWith("scrypt$")).toBe(true);
|
|
985
|
+
}).pipe(Effect.provide(superAdminTestLayer)));
|
|
986
|
+
it.effect("hashes a new password on update", () => Effect.gen(function* () {
|
|
987
|
+
yield* setupSuperAdminTable;
|
|
988
|
+
const svc = yield* CollectionService;
|
|
989
|
+
const created = yield* svc.create(SuperAdmin, { email: "upd@example.com", password: "first-pw" }, adminCtx);
|
|
990
|
+
const id = created["id"];
|
|
991
|
+
const updated = yield* svc.update(SuperAdmin, id, { password: "second-pw" }, adminCtx);
|
|
992
|
+
expect(updated["password"]).not.toBe("second-pw");
|
|
993
|
+
expect(String(updated["password"]).startsWith("scrypt$")).toBe(true);
|
|
994
|
+
}).pipe(Effect.provide(superAdminTestLayer)));
|
|
995
|
+
it.effect("empty password on update is dropped and keeps the existing hash", () => Effect.gen(function* () {
|
|
996
|
+
yield* setupSuperAdminTable;
|
|
997
|
+
const svc = yield* CollectionService;
|
|
998
|
+
const created = yield* svc.create(SuperAdmin, { email: "keep@example.com", password: "keep-pw" }, adminCtx);
|
|
999
|
+
const id = created["id"];
|
|
1000
|
+
const before = created["password"];
|
|
1001
|
+
const updated = yield* svc.update(SuperAdmin, id, { password: "" }, adminCtx);
|
|
1002
|
+
expect(updated["password"]).toBe(before);
|
|
1003
|
+
}).pipe(Effect.provide(superAdminTestLayer)));
|
|
1004
|
+
});
|
|
805
1005
|
//# sourceMappingURL=collection-service.test.js.map
|