@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,6 +1,7 @@
|
|
|
1
1
|
import { Effect, Layer, Option } from "effect";
|
|
2
2
|
import { CollectionService, makeCollectionServiceLayer } from "../collection-service/collection-service.js";
|
|
3
3
|
import { CollectionCache, makeCollectionCacheLayer } from "./collection-cache.js";
|
|
4
|
+
import { buildCtxCacheTag } from "./ctx-key.js";
|
|
4
5
|
function serializeFilterNode(node) {
|
|
5
6
|
switch (node.op) {
|
|
6
7
|
case "eq":
|
|
@@ -69,7 +70,8 @@ export function makeCachedCollectionServiceLayer(allCollections, config = defaul
|
|
|
69
70
|
const cache = yield* CollectionCache;
|
|
70
71
|
return CollectionService.of({
|
|
71
72
|
list: (collection, cursor, perPage, ctx, filter, sort, select, expand) => {
|
|
72
|
-
const
|
|
73
|
+
const ctxTag = buildCtxCacheTag(collection, "list", ctx);
|
|
74
|
+
const key = `${collection.name}:${stableListKey(cursor, perPage, filter, sort, select, expand)}::${ctxTag}`;
|
|
73
75
|
return Effect.gen(function* () {
|
|
74
76
|
const cached = yield* cache.getList(key);
|
|
75
77
|
yield* Effect.currentSpan.pipe(Effect.tap((span) => Effect.sync(() => span.attribute("cache.hit", Option.isSome(cached)))), Effect.ignore);
|
|
@@ -84,7 +86,8 @@ export function makeCachedCollectionServiceLayer(allCollections, config = defaul
|
|
|
84
86
|
}));
|
|
85
87
|
},
|
|
86
88
|
view: (collection, id, ctx, select, expand) => {
|
|
87
|
-
const
|
|
89
|
+
const ctxTag = buildCtxCacheTag(collection, "view", ctx);
|
|
90
|
+
const key = `${stableViewKey(collection.name, id, select, expand)}::${ctxTag}`;
|
|
88
91
|
return Effect.gen(function* () {
|
|
89
92
|
const cached = yield* cache.getRecord(key);
|
|
90
93
|
yield* Effect.currentSpan.pipe(Effect.tap((span) => Effect.sync(() => span.attribute("cache.hit", Option.isSome(cached)))), Effect.ignore);
|
|
@@ -98,9 +101,9 @@ export function makeCachedCollectionServiceLayer(allCollections, config = defaul
|
|
|
98
101
|
attributes: { collection: collection.name },
|
|
99
102
|
}));
|
|
100
103
|
},
|
|
101
|
-
create: (collection, data, ctx) => svc.create(collection, data, ctx).pipe(Effect.tap(() => cache.nukeListsFor(collection.name))),
|
|
102
|
-
update: (collection, id, data, ctx) => svc.update(collection, id, data, ctx).pipe(Effect.tap(() => Effect.all([cache.invalidateRecord(collection.name, id), cache.nukeListsFor(collection.name)], { concurrency: "unbounded" }))),
|
|
103
|
-
delete: (collection, id, ctx) => svc.delete(collection, id, ctx).pipe(Effect.tap(() => Effect.all([cache.invalidateRecord(collection.name, id), cache.nukeListsFor(collection.name)], { concurrency: "unbounded" }))),
|
|
104
|
+
create: (collection, data, ctx) => svc.create(collection, data, ctx).pipe(Effect.tap(() => cache.nukeListsFor(collection.name)), Effect.withSpan("cache.create", { kind: "internal", attributes: { collection: collection.name } })),
|
|
105
|
+
update: (collection, id, data, ctx) => svc.update(collection, id, data, ctx).pipe(Effect.tap(() => Effect.all([cache.invalidateRecord(collection.name, id), cache.nukeListsFor(collection.name)], { concurrency: "unbounded" })), Effect.withSpan("cache.update", { kind: "internal", attributes: { collection: collection.name } })),
|
|
106
|
+
delete: (collection, id, ctx) => svc.delete(collection, id, ctx).pipe(Effect.tap(() => Effect.all([cache.invalidateRecord(collection.name, id), cache.nukeListsFor(collection.name)], { concurrency: "unbounded" })), Effect.withSpan("cache.delete", { kind: "internal", attributes: { collection: collection.name } })),
|
|
104
107
|
});
|
|
105
108
|
})).pipe(Layer.provide(Layer.merge(makeCollectionServiceLayer(allCollections), makeCollectionCacheLayer(config))));
|
|
106
109
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cached-collection.js","sourceRoot":"","sources":["../../../src/cache/cached-collection.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cached-collection.js","sourceRoot":"","sources":["../../../src/cache/cached-collection.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { Context, Effect, Layer, Option } from "effect";
|
|
|
2
2
|
import type { CursorPage } from "../collection-service/context.js";
|
|
3
3
|
import type { RepoRecord } from "../repository/types.js";
|
|
4
4
|
import type { CollectionCacheConfigValues } from "./types.js";
|
|
5
|
-
declare const CollectionCache_base: Context.
|
|
5
|
+
declare const CollectionCache_base: Context.ServiceClass<CollectionCache, "CollectionCache", {
|
|
6
6
|
getRecord(key: string): Effect.Effect<Option.Option<RepoRecord>>;
|
|
7
7
|
putRecord(key: string, v: RepoRecord): Effect.Effect<void>;
|
|
8
8
|
invalidateRecord(col: string, id: string): Effect.Effect<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection-cache.d.ts","sourceRoot":"","sources":["../../../src/cache/collection-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAW,KAAK,EAAE,MAAM,EAAO,MAAM,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"collection-cache.d.ts","sourceRoot":"","sources":["../../../src/cache/collection-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAW,KAAK,EAAE,MAAM,EAAO,MAAM,QAAQ,CAAA;AAErE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AACjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAEvD,OAAO,KAAK,EAAc,2BAA2B,EAAE,MAAM,YAAY,CAAA;;mBAKtD,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;mBACjD,MAAM,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;0BACpC,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;iBACjD,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;iBACjD,MAAM,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;sBACtC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;;AARlD,qBAAa,eAAgB,SAAQ,oBAUf;CAAG;AAoCzB,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,2BAA2B,GAClC,KAAK,CAAC,KAAK,CAAC,eAAe,CAAC,CA4B9B"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Context, Effect, HashMap, Layer, Option, Ref } from "effect";
|
|
2
|
-
export class CollectionCache extends Context.
|
|
2
|
+
export class CollectionCache extends Context.Service()("CollectionCache") {
|
|
3
3
|
}
|
|
4
4
|
const get = (ref, key) => Ref.get(ref).pipe(Effect.map(HashMap.get(key)), Effect.map(Option.filter((e) => e.expiresAt > Date.now())), Effect.map(Option.map((e) => e.value)));
|
|
5
5
|
const put = (ref, key, value, ttlMs, maxSize) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection-cache.js","sourceRoot":"","sources":["../../../src/cache/collection-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"collection-cache.js","sourceRoot":"","sources":["../../../src/cache/collection-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AnyCollectionDef } from "@gettersethya/mira-client";
|
|
2
|
+
import type { RequestCtx } from "../collection-service/context.js";
|
|
3
|
+
/**
|
|
4
|
+
* Derives the portion of a cache key that captures everything a collection's
|
|
5
|
+
* row-level access rule for `action` could depend on in `ctx`: the admin flag,
|
|
6
|
+
* the authenticated identity, and only the specific request query/header keys
|
|
7
|
+
* the compiled rule actually references.
|
|
8
|
+
*
|
|
9
|
+
* Two requests that produce the same tag are guaranteed to be authorized
|
|
10
|
+
* identically by the rule — so caching by `recordKey::ctxTag` cannot let one
|
|
11
|
+
* requester's cached result leak to a requester the rule would otherwise deny
|
|
12
|
+
* or show a different field set to.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildCtxCacheTag(collection: AnyCollectionDef, action: "view" | "list", ctx: RequestCtx): string;
|
|
15
|
+
//# sourceMappingURL=ctx-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ctx-key.d.ts","sourceRoot":"","sources":["../../../src/cache/ctx-key.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AAEjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAIjE;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,gBAAgB,EAC5B,MAAM,EAAE,MAAM,GAAG,MAAM,EACvB,GAAG,EAAE,UAAU,GACd,MAAM,CA+BR"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { extractCtxRefs } from "../collection-service/where.js";
|
|
2
|
+
import { enforcerForAction } from "../rule/enforcer.js";
|
|
3
|
+
/**
|
|
4
|
+
* Derives the portion of a cache key that captures everything a collection's
|
|
5
|
+
* row-level access rule for `action` could depend on in `ctx`: the admin flag,
|
|
6
|
+
* the authenticated identity, and only the specific request query/header keys
|
|
7
|
+
* the compiled rule actually references.
|
|
8
|
+
*
|
|
9
|
+
* Two requests that produce the same tag are guaranteed to be authorized
|
|
10
|
+
* identically by the rule — so caching by `recordKey::ctxTag` cannot let one
|
|
11
|
+
* requester's cached result leak to a requester the rule would otherwise deny
|
|
12
|
+
* or show a different field set to.
|
|
13
|
+
*/
|
|
14
|
+
export function buildCtxCacheTag(collection, action, ctx) {
|
|
15
|
+
if (ctx.admin === true)
|
|
16
|
+
return "admin";
|
|
17
|
+
const compiled = enforcerForAction(collection.schema, action);
|
|
18
|
+
if (compiled === null)
|
|
19
|
+
return "denied";
|
|
20
|
+
const refs = extractCtxRefs(compiled.sql);
|
|
21
|
+
const authPart = refs.authFields.length > 0
|
|
22
|
+
? `auth:${refs.authFields
|
|
23
|
+
.map((field) => field === "collection"
|
|
24
|
+
? `collection=${ctx.auth?.collection ?? ""}`
|
|
25
|
+
: `${field}=${String(ctx.auth?.record[field] ?? "")}`)
|
|
26
|
+
.join(",")}`
|
|
27
|
+
: "";
|
|
28
|
+
const queryPart = refs.queryKeys.length > 0
|
|
29
|
+
? `q:${refs.queryKeys.map((key) => `${key}=${String(ctx.query[key] ?? "")}`).join(",")}`
|
|
30
|
+
: "";
|
|
31
|
+
const headerPart = refs.headerKeys.length > 0
|
|
32
|
+
? `h:${refs.headerKeys.map((key) => `${key}=${String(ctx.headers[key] ?? "")}`).join(",")}`
|
|
33
|
+
: "";
|
|
34
|
+
const parts = [authPart, queryPart, headerPart].filter((p) => p.length > 0);
|
|
35
|
+
return parts.length > 0 ? parts.join("|") : "public";
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=ctx-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ctx-key.js","sourceRoot":"","sources":["../../../src/cache/ctx-key.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAEtD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAC9B,UAA4B,EAC5B,MAAuB,EACvB,GAAe;IAEf,IAAI,GAAG,CAAC,KAAK,KAAK,IAAI;QAAE,OAAO,OAAO,CAAA;IAEtC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7D,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,QAAQ,CAAA;IAEtC,MAAM,IAAI,GAAG,cAAc,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;IAEzC,MAAM,QAAQ,GACZ,IAAI,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC;QACxB,CAAC,CAAC,QAAQ,IAAI,CAAC,UAAU;aACpB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CACb,KAAK,KAAK,YAAY;YACpB,CAAC,CAAC,cAAc,GAAG,CAAC,IAAI,EAAE,UAAU,IAAI,EAAE,EAAE;YAC5C,CAAC,CAAC,GAAG,KAAK,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,CACxD;aACA,IAAI,CAAC,GAAG,CAAC,EAAE;QAChB,CAAC,CAAC,EAAE,CAAA;IAER,MAAM,SAAS,GACb,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC;QACvB,CAAC,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;QACxF,CAAC,CAAC,EAAE,CAAA;IAER,MAAM,UAAU,GACd,IAAI,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC;QACxB,CAAC,CAAC,KAAK,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;QAC3F,CAAC,CAAC,EAAE,CAAA;IAER,MAAM,KAAK,GAAG,CAAC,QAAQ,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAC3E,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;AACtD,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { CollectionCache, makeCollectionCacheLayer } from "./collection-cache.js";
|
|
2
1
|
export { makeCachedCollectionServiceLayer } from "./cached-collection.js";
|
|
2
|
+
export { CollectionCache, makeCollectionCacheLayer } from "./collection-cache.js";
|
|
3
|
+
export { buildCtxCacheTag } from "./ctx-key.js";
|
|
3
4
|
export type { CacheEntry, CollectionCacheConfigValues } from "./types.js";
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cache/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AACjF,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cache/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gCAAgC,EAAE,MAAM,wBAAwB,CAAA;AACzE,OAAO,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AACjF,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAC/C,YAAY,EAAE,UAAU,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAA"}
|
package/dist/src/cache/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { CollectionCache, makeCollectionCacheLayer } from "./collection-cache.js";
|
|
2
1
|
export { makeCachedCollectionServiceLayer } from "./cached-collection.js";
|
|
2
|
+
export { CollectionCache, makeCollectionCacheLayer } from "./collection-cache.js";
|
|
3
|
+
export { buildCtxCacheTag } from "./ctx-key.js";
|
|
3
4
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/cache/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AACjF,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/cache/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gCAAgC,EAAE,MAAM,wBAAwB,CAAA;AACzE,OAAO,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AACjF,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA"}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { SqlClient } from "@effect/sql";
|
|
2
|
-
import { Context, Effect, Layer } from "effect";
|
|
3
1
|
import type { AnyCollectionDef } from "@gettersethya/mira-client";
|
|
4
2
|
import type { FilterNode } from "@gettersethya/mira-client";
|
|
3
|
+
import { Context, Effect, Layer } from "effect";
|
|
4
|
+
import { SqlClient } from "effect/sql";
|
|
5
|
+
import { Dialect } from "../dialect/dialect.js";
|
|
6
|
+
import { AuthService } from "../http/auth.js";
|
|
5
7
|
import { Repository } from "../repository/repository.js";
|
|
6
8
|
import type { RepoRecord, SortOrder } from "../repository/types.js";
|
|
7
9
|
import { FileStorage } from "../storage/storage.js";
|
|
8
10
|
import type { CursorPage, RequestCtx } from "./context.js";
|
|
9
11
|
import type { CollectionError } from "./errors.js";
|
|
10
|
-
declare const CollectionService_base: Context.
|
|
12
|
+
declare const CollectionService_base: Context.ServiceClass<CollectionService, "CollectionService", {
|
|
11
13
|
list(collection: AnyCollectionDef, cursor: number | null, perPage: number, ctx: RequestCtx, filter?: FilterNode, sort?: SortOrder, select?: ReadonlyArray<string> | null, expand?: ReadonlyArray<string> | null): Effect.Effect<CursorPage, CollectionError>;
|
|
12
14
|
view(collection: AnyCollectionDef, id: string, ctx: RequestCtx, select?: ReadonlyArray<string> | null, expand?: ReadonlyArray<string> | null): Effect.Effect<RepoRecord, CollectionError>;
|
|
13
15
|
create(collection: AnyCollectionDef, data: RepoRecord, ctx: RequestCtx): Effect.Effect<RepoRecord, CollectionError>;
|
|
@@ -55,8 +57,8 @@ export declare class CollectionService extends CollectionService_base {
|
|
|
55
57
|
* @see CollectionService — the service tag
|
|
56
58
|
* @see makeCachedCollectionServiceLayer — wrapping layer that adds caching
|
|
57
59
|
*/
|
|
58
|
-
export declare function makeCollectionServiceLayer(allCollections: ReadonlyArray<AnyCollectionDef>): Layer.Layer<CollectionService, never, Repository | SqlClient.SqlClient | FileStorage>;
|
|
60
|
+
export declare function makeCollectionServiceLayer(allCollections: ReadonlyArray<AnyCollectionDef>): Layer.Layer<CollectionService, never, Repository | SqlClient.SqlClient | FileStorage | Dialect | AuthService>;
|
|
59
61
|
/** @deprecated Use makeCollectionServiceLayer(allCollections) */
|
|
60
|
-
export declare const CollectionServiceLive: Layer.Layer<CollectionService, never, FileStorage | SqlClient.SqlClient | Repository>;
|
|
62
|
+
export declare const CollectionServiceLive: Layer.Layer<CollectionService, never, Dialect | AuthService | FileStorage | SqlClient.SqlClient | Repository>;
|
|
61
63
|
export {};
|
|
62
64
|
//# sourceMappingURL=collection-service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection-service.d.ts","sourceRoot":"","sources":["../../../src/collection-service/collection-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"collection-service.d.ts","sourceRoot":"","sources":["../../../src/collection-service/collection-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAoB,MAAM,2BAA2B,CAAA;AACnF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AAE3D,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAkB,MAAM,QAAQ,CAAA;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAGtC,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AACvD,OAAO,KAAK,EAAa,UAAU,EAAE,SAAS,EAAe,MAAM,uBAAuB,CAAA;AAE1F,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAElD,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAG1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;;qBAgChC,gBAAgB,UACpB,MAAM,GAAG,IAAI,WACZ,MAAM,OACV,UAAU,WACN,UAAU,SACZ,SAAS,WACP,aAAa,CAAC,MAAM,CAAC,GAAG,IAAI,WAC5B,aAAa,CAAC,MAAM,CAAC,GAAG,IAAI,GACpC,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC;qBAG/B,gBAAgB,MACxB,MAAM,OACL,UAAU,WACN,aAAa,CAAC,MAAM,CAAC,GAAG,IAAI,WAC5B,aAAa,CAAC,MAAM,CAAC,GAAG,IAAI,GACpC,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC;uBAE1B,gBAAgB,QAAQ,UAAU,OAAO,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC;uBAGrG,gBAAgB,MACxB,MAAM,QACJ,UAAU,OACX,UAAU,GACd,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC;uBAE1B,gBAAgB,MAAM,MAAM,OAAO,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,eAAe,CAAC;;AAtD3G;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,iBAAkB,SAAQ,sBAiCf;CAAG;AAwG3B;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,0BAA0B,CACxC,cAAc,EAAE,aAAa,CAAC,gBAAgB,CAAC,GAC9C,KAAK,CAAC,KAAK,CAAC,iBAAiB,EAAE,KAAK,EAAE,UAAU,GAAG,SAAS,CAAC,SAAS,GAAG,WAAW,GAAG,OAAO,GAAG,WAAW,CAAC,CAub/G;AAED,iEAAiE;AACjE,eAAO,MAAM,qBAAqB,+GAAiC,CAAA"}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { SqlClient } from "@effect/sql";
|
|
2
|
-
import { unsafeFragment } from "@effect/sql/Statement";
|
|
3
|
-
import { Context, Effect, Layer, Option, Schema } from "effect";
|
|
4
1
|
import { filterNodeToWhereClause } from "@gettersethya/mira-client";
|
|
5
|
-
import {
|
|
2
|
+
import { Context, Effect, Layer, Option, Schema } from "effect";
|
|
3
|
+
import { SqlClient } from "effect/sql";
|
|
4
|
+
import { literal } from "effect/sql/Statement";
|
|
5
|
+
import { Dialect } from "../dialect/dialect.js";
|
|
6
|
+
import { AuthService } from "../http/auth.js";
|
|
6
7
|
import { Repository } from "../repository/repository.js";
|
|
8
|
+
import { enforcerForAction } from "../rule/enforcer.js";
|
|
7
9
|
import { FileStorage } from "../storage/storage.js";
|
|
8
|
-
import { makeRowDecoder } from "./decode.js";
|
|
10
|
+
import { makeRowDecoder, makeRowEncoder } from "./decode.js";
|
|
9
11
|
import { ForbiddenError, NotFoundError, ReadOnlyError, ValidationError } from "./errors.js";
|
|
10
12
|
import { buildInputSchemas, parseErrToValidationError } from "./input-schema.js";
|
|
11
13
|
import { andWhere, cursorClause, idClause, resolveCtxPlaceholders, resolveFieldRefs } from "./where.js";
|
|
@@ -32,17 +34,17 @@ import { andWhere, cursorClause, idClause, resolveCtxPlaceholders, resolveFieldR
|
|
|
32
34
|
* @see makeCollectionServiceLayer — factory function
|
|
33
35
|
* @see Repository — the underlying data access layer
|
|
34
36
|
*/
|
|
35
|
-
export class CollectionService extends Context.
|
|
37
|
+
export class CollectionService extends Context.Service()("CollectionService") {
|
|
36
38
|
}
|
|
37
|
-
function resolveFields(schema, userSelect) {
|
|
39
|
+
function resolveFields(schema, userSelect, admin = false) {
|
|
38
40
|
const nonHidden = Object.entries(schema.properties)
|
|
39
|
-
.filter(([, p]) => !p["x-hidden"])
|
|
41
|
+
.filter(([, p]) => admin || !p["x-hidden"])
|
|
40
42
|
.map(([k]) => k);
|
|
41
43
|
if (userSelect === null)
|
|
42
44
|
return nonHidden;
|
|
43
|
-
// Intersect with non-hidden fields
|
|
45
|
+
// Intersect with non-hidden fields (admin bypasses hidden filter)
|
|
44
46
|
const nonHiddenSet = new Set(nonHidden);
|
|
45
|
-
const selected = userSelect.filter((f) => nonHiddenSet.has(f));
|
|
47
|
+
const selected = userSelect.filter((f) => admin || nonHiddenSet.has(f));
|
|
46
48
|
// Always include core system fields needed for the response
|
|
47
49
|
const always = new Set(["id", "created", "updated"]);
|
|
48
50
|
for (const a of always) {
|
|
@@ -71,8 +73,11 @@ function resolveExpand(schema, expandFieldNames, allCollections) {
|
|
|
71
73
|
}
|
|
72
74
|
return result;
|
|
73
75
|
}
|
|
76
|
+
// These fields are auto-generated by the system and must never be accepted from user input.
|
|
77
|
+
// Detected via the `x-generated` schema annotation rather than a hardcoded name list,
|
|
78
|
+
// so auth fields (email/password) and future additions stay user-writable.
|
|
74
79
|
function rejectSystemFields(data, schema, collectionName) {
|
|
75
|
-
const sysKeys = Object.keys(data).filter((k) => schema.properties[k]?.["x-
|
|
80
|
+
const sysKeys = Object.keys(data).filter((k) => schema.properties[k]?.["x-generated"] === true);
|
|
76
81
|
if (sysKeys.length === 0)
|
|
77
82
|
return Effect.void;
|
|
78
83
|
return Effect.fail(new ValidationError({
|
|
@@ -126,26 +131,148 @@ export function makeCollectionServiceLayer(allCollections) {
|
|
|
126
131
|
const repo = yield* Repository;
|
|
127
132
|
const sql = yield* SqlClient.SqlClient;
|
|
128
133
|
const fileStorage = yield* FileStorage;
|
|
129
|
-
const
|
|
134
|
+
const dialect = yield* Dialect;
|
|
135
|
+
const authService = yield* AuthService;
|
|
136
|
+
// Passwords on auth collections are hashed here, at the single write
|
|
137
|
+
// chokepoint, so every caller (HTTP router, dashboard, plugins) stores a
|
|
138
|
+
// hash rather than a plaintext value. On update, an empty password is
|
|
139
|
+
// dropped from the patch entirely so an edit form that leaves the field
|
|
140
|
+
// blank does not overwrite the stored hash.
|
|
141
|
+
const hashAuthPassword = (collection, data, dropEmpty) => Effect.gen(function* () {
|
|
142
|
+
if (collection.schema["x-collection-kind"] !== "auth")
|
|
143
|
+
return data;
|
|
144
|
+
if (!("password" in data))
|
|
145
|
+
return data;
|
|
146
|
+
const raw = data["password"];
|
|
147
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
148
|
+
if (!dropEmpty)
|
|
149
|
+
return data;
|
|
150
|
+
const withoutPassword = { ...data };
|
|
151
|
+
delete withoutPassword["password"];
|
|
152
|
+
return withoutPassword;
|
|
153
|
+
}
|
|
154
|
+
const hash = yield* authService.hashPassword(raw);
|
|
155
|
+
return { ...data, password: hash };
|
|
156
|
+
});
|
|
157
|
+
const decoderMap = new Map(allCollections.map((c) => [c.name, makeRowDecoder(c.schema, dialect.storesBooleanAsInteger)]));
|
|
158
|
+
const encoderMap = new Map(allCollections.map((c) => [c.name, makeRowEncoder(c.schema, dialect.storesBooleanAsInteger)]));
|
|
130
159
|
const inputSchemaMap = new Map(allCollections.map((c) => [c.name, buildInputSchemas(c)]));
|
|
131
160
|
const getInputSchemas = (col) => inputSchemaMap.get(col.name) ?? buildInputSchemas(col);
|
|
132
161
|
const decodeRow = (collection, record) => (decoderMap.get(collection.name) ?? Effect.succeed)(record);
|
|
162
|
+
const getEncoder = (collection) => encoderMap.get(collection.name) ?? ((r) => r);
|
|
163
|
+
// create/update return the stored row directly (no `resolveFields` pass), so
|
|
164
|
+
// hidden fields (auth password, seqId) must be stripped here for non-admin
|
|
165
|
+
// callers — matching the list/view response contract.
|
|
166
|
+
const stripHidden = (collection, record, admin) => {
|
|
167
|
+
if (admin)
|
|
168
|
+
return record;
|
|
169
|
+
const out = {};
|
|
170
|
+
for (const [k, v] of Object.entries(record)) {
|
|
171
|
+
if (collection.schema.properties[k]?.["x-hidden"] === true)
|
|
172
|
+
continue;
|
|
173
|
+
out[k] = v;
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
};
|
|
177
|
+
const isAuthCollection = (collection) => collection.schema["x-collection-kind"] === "auth";
|
|
178
|
+
const asString = (value) => (typeof value === "string" ? value : undefined);
|
|
179
|
+
// canManage = admin OR (auth + authenticated + manage rule matches). Evaluated
|
|
180
|
+
// against the incoming payload for create (emailVerified forced false so the
|
|
181
|
+
// rule can't be gamed by the field it guards) and against the stored record for
|
|
182
|
+
// update. Additive: the base create/update rule still gates access — manage only
|
|
183
|
+
// escalates privileges (email, emailVerified, password without oldPassword).
|
|
184
|
+
const canManagePayload = (collection, payload, ctx) => Effect.gen(function* () {
|
|
185
|
+
if (ctx.admin === true)
|
|
186
|
+
return true;
|
|
187
|
+
if (!isAuthCollection(collection) || ctx.auth === undefined)
|
|
188
|
+
return false;
|
|
189
|
+
const rule = enforcerForAction(collection.schema, "manage");
|
|
190
|
+
if (rule === null)
|
|
191
|
+
return false;
|
|
192
|
+
const resolved = yield* resolveCtxPlaceholders(rule, ctx, collection.name, "manage");
|
|
193
|
+
const bound = resolveFieldRefs(resolved, { ...payload, emailVerified: false });
|
|
194
|
+
const where = literal(bound.sql, bound.params);
|
|
195
|
+
const check = yield* sql `SELECT 1 AS ok WHERE ${where}`;
|
|
196
|
+
return check.length > 0;
|
|
197
|
+
});
|
|
198
|
+
const canManageRecord = (collection, id, ctx) => Effect.gen(function* () {
|
|
199
|
+
if (ctx.admin === true)
|
|
200
|
+
return true;
|
|
201
|
+
if (!isAuthCollection(collection) || ctx.auth === undefined)
|
|
202
|
+
return false;
|
|
203
|
+
const rule = enforcerForAction(collection.schema, "manage");
|
|
204
|
+
if (rule === null)
|
|
205
|
+
return false;
|
|
206
|
+
const resolved = yield* resolveCtxPlaceholders(rule, ctx, collection.name, "manage");
|
|
207
|
+
const allowed = yield* repo.viewFilter(collection.name, {
|
|
208
|
+
where: andWhere(resolved, idClause(id))
|
|
209
|
+
});
|
|
210
|
+
return allowed.length > 0;
|
|
211
|
+
});
|
|
212
|
+
const credentialError = (collection, issue) => new ValidationError({ collection: collection.name, issues: [issue] });
|
|
213
|
+
const verifyOldPassword = (collection, raw, existing) => Effect.gen(function* () {
|
|
214
|
+
const oldPassword = asString(raw["oldPassword"]);
|
|
215
|
+
if (oldPassword === undefined || oldPassword.length === 0) {
|
|
216
|
+
return yield* Effect.fail(credentialError(collection, "oldPassword is required"));
|
|
217
|
+
}
|
|
218
|
+
const stored = asString(existing["password"]);
|
|
219
|
+
const ok = stored !== undefined
|
|
220
|
+
? yield* authService.verifyPassword(oldPassword, stored).pipe(Effect.orElseSucceed(() => false))
|
|
221
|
+
: false;
|
|
222
|
+
if (!ok) {
|
|
223
|
+
return yield* Effect.fail(credentialError(collection, "Missing or invalid old password"));
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
const enforceCreateCredentials = (collection, raw, cleaned, canManage, ctx) => Effect.gen(function* () {
|
|
227
|
+
if (!isAuthCollection(collection) || ctx.admin === true)
|
|
228
|
+
return;
|
|
229
|
+
const password = asString(cleaned["password"]) ?? asString(raw["password"]) ?? "";
|
|
230
|
+
if (password.length > 0) {
|
|
231
|
+
const confirm = asString(raw["passwordConfirm"]);
|
|
232
|
+
if (confirm === undefined || confirm !== password) {
|
|
233
|
+
return yield* Effect.fail(credentialError(collection, "passwordConfirm must match password"));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (!canManage && cleaned["emailVerified"] === true) {
|
|
237
|
+
return yield* Effect.fail(credentialError(collection, "emailVerified cannot be set"));
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
const enforceUpdateCredentials = (collection, raw, cleaned, existing, canManage, ctx) => Effect.gen(function* () {
|
|
241
|
+
if (!isAuthCollection(collection) || ctx.admin === true)
|
|
242
|
+
return;
|
|
243
|
+
const newPassword = asString(cleaned["password"]);
|
|
244
|
+
const passwordChanging = newPassword !== undefined && newPassword.length > 0;
|
|
245
|
+
const emailChanging = "email" in cleaned && cleaned["email"] !== existing["email"];
|
|
246
|
+
const verifiedChanging = "emailVerified" in cleaned && Boolean(cleaned["emailVerified"]) !== Boolean(existing["emailVerified"]);
|
|
247
|
+
if (passwordChanging) {
|
|
248
|
+
const confirm = asString(raw["passwordConfirm"]);
|
|
249
|
+
if (confirm === undefined || confirm !== newPassword) {
|
|
250
|
+
return yield* Effect.fail(credentialError(collection, "passwordConfirm must match password"));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (!canManage && (passwordChanging || emailChanging)) {
|
|
254
|
+
yield* verifyOldPassword(collection, raw, existing);
|
|
255
|
+
}
|
|
256
|
+
if (!canManage && verifiedChanging) {
|
|
257
|
+
return yield* Effect.fail(credentialError(collection, "emailVerified cannot be changed"));
|
|
258
|
+
}
|
|
259
|
+
});
|
|
133
260
|
const list = (collection, cursor, perPage, ctx, filter, sort, select, expand) => Effect.gen(function* () {
|
|
134
|
-
const ruleWhere = yield* compileRule(collection, "list", ctx);
|
|
261
|
+
const ruleWhere = ctx.admin ? null : yield* compileRule(collection, "list", ctx);
|
|
135
262
|
const userFilter = filter !== undefined ? yield* filterNodeToWhereClause(filter, collection.schema, collection.name) : null;
|
|
136
|
-
const where = userFilter !== null ? andWhere(ruleWhere, userFilter) : ruleWhere;
|
|
137
|
-
const combined = cursor !== null ? andWhere(where, cursorClause(cursor)) : where;
|
|
138
|
-
const fields = resolveFields(collection.schema, select ?? null);
|
|
263
|
+
const where = ruleWhere !== null && userFilter !== null ? andWhere(ruleWhere, userFilter) : (userFilter ?? ruleWhere);
|
|
264
|
+
const combined = cursor !== null && where !== null ? andWhere(where, cursorClause(cursor)) : where;
|
|
265
|
+
const fields = resolveFields(collection.schema, select ?? null, ctx.admin === true);
|
|
139
266
|
const expandDefs = resolveExpand(collection.schema, expand ?? null, allCollections);
|
|
140
267
|
const fieldsWithExpand = [...new Set([...fields, ...expandDefs.map((e) => e.localField)])];
|
|
141
268
|
// seqId is x-hidden but must be fetched for cursor-based pagination
|
|
142
269
|
const fieldsForQuery = fieldsWithExpand.includes("seqId") ? fieldsWithExpand : [...fieldsWithExpand, "seqId"];
|
|
143
|
-
const
|
|
144
|
-
where: combined,
|
|
270
|
+
const listOpts = {
|
|
145
271
|
sort: sort ?? { field: "seqId", direction: "asc" },
|
|
146
272
|
fields: fieldsForQuery,
|
|
147
273
|
expand: expandDefs
|
|
148
|
-
}
|
|
274
|
+
};
|
|
275
|
+
const rows = yield* repo.list(collection.name, perPage, combined !== null ? { ...listOpts, where: combined } : listOpts);
|
|
149
276
|
const lastRow = rows.items.at(-1);
|
|
150
277
|
const rawSeqId = lastRow?.["seqId"];
|
|
151
278
|
const lastSeqId = typeof rawSeqId === "number" ? rawSeqId : null;
|
|
@@ -162,9 +289,9 @@ export function makeCollectionServiceLayer(allCollections) {
|
|
|
162
289
|
attributes: { collection: collection.name }
|
|
163
290
|
}));
|
|
164
291
|
const view = (collection, id, ctx, select, expand) => Effect.gen(function* () {
|
|
165
|
-
const ruleWhere = yield* compileRule(collection, "view", ctx);
|
|
166
|
-
const combined = andWhere(ruleWhere, idClause(id));
|
|
167
|
-
const fields = resolveFields(collection.schema, select ?? null);
|
|
292
|
+
const ruleWhere = ctx.admin ? null : yield* compileRule(collection, "view", ctx);
|
|
293
|
+
const combined = ruleWhere !== null ? andWhere(ruleWhere, idClause(id)) : idClause(id);
|
|
294
|
+
const fields = resolveFields(collection.schema, select ?? null, ctx.admin === true);
|
|
168
295
|
const expandDefs = resolveExpand(collection.schema, expand ?? null, allCollections);
|
|
169
296
|
const fieldsWithExpand = [...new Set([...fields, ...expandDefs.map((e) => e.localField)])];
|
|
170
297
|
const rows = yield* repo.viewFilter(collection.name, {
|
|
@@ -186,17 +313,21 @@ export function makeCollectionServiceLayer(allCollections) {
|
|
|
186
313
|
}
|
|
187
314
|
yield* rejectSystemFields(data, collection.schema, collection.name);
|
|
188
315
|
const schemas = getInputSchemas(collection);
|
|
189
|
-
const cleaned = yield* Schema.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
316
|
+
const cleaned = yield* Schema.decodeUnknownEffect(schemas.create)(data).pipe(Effect.mapError(parseErrToValidationError(collection.name)));
|
|
317
|
+
if (!ctx.admin) {
|
|
318
|
+
const ruleWhere = yield* compileRule(collection, "create", ctx);
|
|
319
|
+
// Pre-check: evaluate the create rule against the payload (no table needed).
|
|
320
|
+
const payloadBound = resolveFieldRefs(ruleWhere, cleaned);
|
|
321
|
+
const where = literal(payloadBound.sql, payloadBound.params);
|
|
322
|
+
const check = yield* sql `SELECT 1 AS ok WHERE ${where}`;
|
|
323
|
+
if (check.length === 0) {
|
|
324
|
+
return yield* new ForbiddenError({ collection: collection.name, action: "create" });
|
|
325
|
+
}
|
|
197
326
|
}
|
|
198
|
-
const
|
|
199
|
-
|
|
327
|
+
const canManage = yield* canManagePayload(collection, cleaned, ctx);
|
|
328
|
+
yield* enforceCreateCredentials(collection, data, cleaned, canManage, ctx);
|
|
329
|
+
const row = yield* repo.create(collection.name, getEncoder(collection)(yield* hashAuthPassword(collection, cleaned, false)));
|
|
330
|
+
return stripHidden(collection, yield* decodeRow(collection, row), ctx.admin === true);
|
|
200
331
|
}).pipe(Effect.withSpan("collection.create", {
|
|
201
332
|
kind: "internal",
|
|
202
333
|
attributes: { collection: collection.name }
|
|
@@ -207,28 +338,38 @@ export function makeCollectionServiceLayer(allCollections) {
|
|
|
207
338
|
}
|
|
208
339
|
yield* rejectSystemFields(data, collection.schema, collection.name);
|
|
209
340
|
const schemas = getInputSchemas(collection);
|
|
210
|
-
const cleaned = yield* Schema.
|
|
211
|
-
|
|
212
|
-
|
|
341
|
+
const cleaned = yield* Schema.decodeUnknownEffect(schemas.update)(data).pipe(Effect.mapError(parseErrToValidationError(collection.name)));
|
|
342
|
+
// If the collection has no rules at all, deny immediately (don't leak existence)
|
|
343
|
+
if (!ctx.admin) {
|
|
344
|
+
const enforcerResult = enforcerForAction(collection.schema, "update");
|
|
345
|
+
if (enforcerResult === null) {
|
|
346
|
+
return yield* new ForbiddenError({ collection: collection.name, action: "update" });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
213
349
|
const existing = yield* repo.view(collection.name, id);
|
|
214
350
|
if (Option.isNone(existing)) {
|
|
215
351
|
return yield* new NotFoundError({ collection: collection.name, id });
|
|
216
352
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
353
|
+
if (!ctx.admin) {
|
|
354
|
+
const ruleWhere = yield* compileRule(collection, "update", ctx);
|
|
355
|
+
// Rule check against this specific row
|
|
356
|
+
const combined = andWhere(ruleWhere, idClause(id));
|
|
357
|
+
const allowed = yield* repo.viewFilter(collection.name, { where: combined });
|
|
358
|
+
if (allowed.length === 0) {
|
|
359
|
+
return yield* new ForbiddenError({ collection: collection.name, action: "update" });
|
|
360
|
+
}
|
|
222
361
|
}
|
|
223
|
-
const
|
|
362
|
+
const canManage = yield* canManageRecord(collection, id, ctx);
|
|
363
|
+
yield* enforceUpdateCredentials(collection, data, cleaned, existing.value, canManage, ctx);
|
|
364
|
+
const updated = yield* repo.update(collection.name, id, getEncoder(collection)(yield* hashAuthPassword(collection, cleaned, true)));
|
|
224
365
|
// Best-effort: delete old files and their thumbnails for any replaced file fields.
|
|
225
366
|
const oldKeys = extractReplacedFileKeys(existing.value, cleaned, collection.schema);
|
|
226
367
|
yield* Effect.forEach(oldKeys, (key) => Effect.gen(function* () {
|
|
227
|
-
yield* fileStorage.delete(key).pipe(Effect.
|
|
368
|
+
yield* fileStorage.delete(key).pipe(Effect.catch(() => Effect.void));
|
|
228
369
|
const thumbKeys = yield* fileStorage.list(`_thumbs/${key}/`).pipe(Effect.orElseSucceed(() => []));
|
|
229
|
-
yield* Effect.forEach(thumbKeys, (tk) => fileStorage.delete(tk).pipe(Effect.
|
|
370
|
+
yield* Effect.forEach(thumbKeys, (tk) => fileStorage.delete(tk).pipe(Effect.catch(() => Effect.void)), { concurrency: "unbounded" });
|
|
230
371
|
}), { concurrency: "unbounded" });
|
|
231
|
-
return yield* decodeRow(collection, Option.getOrElse(updated, () => existing.value));
|
|
372
|
+
return stripHidden(collection, yield* decodeRow(collection, Option.getOrElse(updated, () => existing.value)), ctx.admin === true);
|
|
232
373
|
}).pipe(Effect.withSpan("collection.update", {
|
|
233
374
|
kind: "internal",
|
|
234
375
|
attributes: { collection: collection.name }
|
|
@@ -237,23 +378,32 @@ export function makeCollectionServiceLayer(allCollections) {
|
|
|
237
378
|
if (collection.schema["x-collection-kind"] === "view") {
|
|
238
379
|
return yield* new ReadOnlyError({ collection: collection.name });
|
|
239
380
|
}
|
|
240
|
-
|
|
381
|
+
// If the collection has no rules at all, deny immediately (don't leak existence)
|
|
382
|
+
if (!ctx.admin) {
|
|
383
|
+
const enforcerResult = enforcerForAction(collection.schema, "delete");
|
|
384
|
+
if (enforcerResult === null) {
|
|
385
|
+
return yield* new ForbiddenError({ collection: collection.name, action: "delete" });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
241
388
|
const existing = yield* repo.view(collection.name, id);
|
|
242
389
|
if (Option.isNone(existing)) {
|
|
243
390
|
return yield* new NotFoundError({ collection: collection.name, id });
|
|
244
391
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
392
|
+
if (!ctx.admin) {
|
|
393
|
+
const ruleWhere = yield* compileRule(collection, "delete", ctx);
|
|
394
|
+
const combined = andWhere(ruleWhere, idClause(id));
|
|
395
|
+
const allowed = yield* repo.viewFilter(collection.name, { where: combined });
|
|
396
|
+
if (allowed.length === 0) {
|
|
397
|
+
return yield* new ForbiddenError({ collection: collection.name, action: "delete" });
|
|
398
|
+
}
|
|
249
399
|
}
|
|
250
400
|
yield* repo.delete(collection.name, id);
|
|
251
401
|
// Best-effort: delete files and their thumbnails after the record is gone.
|
|
252
402
|
const keys = extractFileKeys(existing.value, collection.schema);
|
|
253
403
|
yield* Effect.forEach(keys, (key) => Effect.gen(function* () {
|
|
254
|
-
yield* fileStorage.delete(key).pipe(Effect.
|
|
404
|
+
yield* fileStorage.delete(key).pipe(Effect.catch(() => Effect.void));
|
|
255
405
|
const thumbKeys = yield* fileStorage.list(`_thumbs/${key}/`).pipe(Effect.orElseSucceed(() => []));
|
|
256
|
-
yield* Effect.forEach(thumbKeys, (tk) => fileStorage.delete(tk).pipe(Effect.
|
|
406
|
+
yield* Effect.forEach(thumbKeys, (tk) => fileStorage.delete(tk).pipe(Effect.catch(() => Effect.void)), { concurrency: "unbounded" });
|
|
257
407
|
}), { concurrency: "unbounded" });
|
|
258
408
|
}).pipe(Effect.withSpan("collection.delete", {
|
|
259
409
|
kind: "internal",
|