@warlock.js/core 5.20.0 → 5.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1124 -1086
- package/esm/benchmark/benchmark.mjs +34 -25
- package/esm/benchmark/benchmark.mjs.map +1 -1
- package/esm/benchmark/profiler.d.mts.map +1 -1
- package/esm/benchmark/profiler.mjs +8 -3
- package/esm/benchmark/profiler.mjs.map +1 -1
- package/esm/bootstrap.d.mts.map +1 -1
- package/esm/bootstrap.mjs +3 -0
- package/esm/bootstrap.mjs.map +1 -1
- package/esm/cache/database-cache-driver.d.mts +2 -2
- package/esm/cache/database-cache-driver.d.mts.map +1 -1
- package/esm/cache/database-cache-driver.mjs +3 -2
- package/esm/cache/database-cache-driver.mjs.map +1 -1
- package/esm/cli/cli-commands.manager.mjs +16 -14
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/add.command.mjs +1 -1
- package/esm/cli/commands/add.command.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs +3 -2
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
- package/esm/cli/commands/generate/generate.command.mjs +20 -0
- package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/column-dsl-parser.mjs +4 -2
- package/esm/cli/commands/generate/generators/column-dsl-parser.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/controller.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/controller.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/migration.generator.mjs +22 -6
- package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/model.generator.mjs +22 -7
- package/esm/cli/commands/generate/generators/model.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/module.generator.mjs +8 -6
- package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/repository.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/repository.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/resource.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/resource.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/service.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/service.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/templates/stubs.mjs +7 -7
- package/esm/cli/commands/generate/templates/stubs.mjs.map +1 -1
- package/esm/cli/commands/generate/utils/writer.mjs +25 -1
- package/esm/cli/commands/generate/utils/writer.mjs.map +1 -1
- package/esm/cli/commands/routes/routes-command.action.mjs +5 -1
- package/esm/cli/commands/routes/routes-command.action.mjs.map +1 -1
- package/esm/cli/commands/routes/with-console-on-stderr.mjs +24 -0
- package/esm/cli/commands/routes/with-console-on-stderr.mjs.map +1 -0
- package/esm/cli/commands/routes.command.mjs +3 -4
- package/esm/cli/commands/routes.command.mjs.map +1 -1
- package/esm/cli/commands/start-production.command.mjs +5 -5
- package/esm/cli/commands/start-production.command.mjs.map +1 -1
- package/esm/cli/commands/storage-put.action.mjs +2 -1
- package/esm/cli/commands/storage-put.action.mjs.map +1 -1
- package/esm/cli/commands/update.command.mjs +1 -0
- package/esm/cli/commands/update.command.mjs.map +1 -1
- package/esm/cli/parse-cli-args.mjs +13 -1
- package/esm/cli/parse-cli-args.mjs.map +1 -1
- package/esm/commands/cli-command.d.mts.map +1 -1
- package/esm/commands/cli-command.mjs +4 -2
- package/esm/commands/cli-command.mjs.map +1 -1
- package/esm/commands/index.mjs +3 -0
- package/esm/config/config-key-from-path.mjs +20 -0
- package/esm/config/config-key-from-path.mjs.map +1 -0
- package/esm/config/config-loader.d.mts.map +1 -1
- package/esm/config/config-loader.mjs +2 -3
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/connectors/connectors-manager.d.mts +4 -0
- package/esm/connectors/connectors-manager.d.mts.map +1 -1
- package/esm/connectors/connectors-manager.mjs +30 -2
- package/esm/connectors/connectors-manager.mjs.map +1 -1
- package/esm/connectors/socket-connector.d.mts +29 -0
- package/esm/connectors/socket-connector.d.mts.map +1 -1
- package/esm/connectors/socket-connector.mjs +73 -9
- package/esm/connectors/socket-connector.mjs.map +1 -1
- package/esm/connectors/types.d.mts +1 -1
- package/esm/connectors/types.mjs.map +1 -1
- package/esm/database/models/database-log/database-log.d.mts +47 -24
- package/esm/database/models/database-log/database-log.d.mts.map +1 -1
- package/esm/database/models/database-log/database-log.mjs +36 -4
- package/esm/database/models/database-log/database-log.mjs.map +1 -1
- package/esm/database/seed-command-action.mjs +10 -2
- package/esm/database/seed-command-action.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +11 -4
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/file-event-handler.mjs +26 -6
- package/esm/dev-server/file-event-handler.mjs.map +1 -1
- package/esm/dev-server/file-manager.d.mts.map +1 -1
- package/esm/dev-server/file-manager.mjs +2 -2
- package/esm/dev-server/file-manager.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +3 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/files-watcher.mjs +64 -5
- package/esm/dev-server/files-watcher.mjs.map +1 -1
- package/esm/dev-server/health-checker/files-healthcare.manager.mjs +31 -9
- package/esm/dev-server/health-checker/files-healthcare.manager.mjs.map +1 -1
- package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs +11 -7
- package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs.map +1 -1
- package/esm/dev-server/loader/load-hook.mjs +1 -1
- package/esm/dev-server/loader/register-loader.mjs +1 -1
- package/esm/dev-server/manifest-manager.d.mts.map +1 -1
- package/esm/dev-server/manifest-manager.mjs +12 -6
- package/esm/dev-server/manifest-manager.mjs.map +1 -1
- package/esm/dev-server/parse-imports.mjs +9 -0
- package/esm/dev-server/parse-imports.mjs.map +1 -1
- package/esm/dev-server/register-dev-shortcuts.mjs +2 -1
- package/esm/dev-server/register-dev-shortcuts.mjs.map +1 -1
- package/esm/dev-server/shortcuts.mjs +2 -2
- package/esm/dev-server/shortcuts.mjs.map +1 -1
- package/esm/dev-server/special-file-patterns.mjs +18 -0
- package/esm/dev-server/special-file-patterns.mjs.map +1 -0
- package/esm/dev-server/special-files-collector.d.mts.map +1 -1
- package/esm/dev-server/special-files-collector.mjs +2 -5
- package/esm/dev-server/special-files-collector.mjs.map +1 -1
- package/esm/dev-server/start-development-server.mjs +1 -2
- package/esm/dev-server/start-development-server.mjs.map +1 -1
- package/esm/dev-server/supervisor.mjs +23 -3
- package/esm/dev-server/supervisor.mjs.map +1 -1
- package/esm/dev-server/tsconfig-manager.mjs +22 -12
- package/esm/dev-server/tsconfig-manager.mjs.map +1 -1
- package/esm/dev-server/type-generator.mjs +10 -9
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/encryption/encrypt.d.mts.map +1 -1
- package/esm/encryption/encrypt.mjs +16 -5
- package/esm/encryption/encrypt.mjs.map +1 -1
- package/esm/encryption/hash.d.mts.map +1 -1
- package/esm/encryption/hash.mjs +2 -0
- package/esm/encryption/hash.mjs.map +1 -1
- package/esm/encryption/password.d.mts.map +1 -1
- package/esm/encryption/password.mjs +3 -1
- package/esm/encryption/password.mjs.map +1 -1
- package/esm/generations/features/index.mjs +1 -1
- package/esm/generations/features/mysql.feature.mjs +9 -2
- package/esm/generations/features/mysql.feature.mjs.map +1 -1
- package/esm/generations/features/test.feature.mjs +2 -1
- package/esm/generations/features/test.feature.mjs.map +1 -1
- package/esm/generations/features/web.feature.mjs +4 -2
- package/esm/generations/features/web.feature.mjs.map +1 -1
- package/esm/http/csrf-origin-policy.d.mts.map +1 -1
- package/esm/http/csrf-origin-policy.mjs +5 -2
- package/esm/http/csrf-origin-policy.mjs.map +1 -1
- package/esm/http/flush-pending-cookies.mjs +52 -0
- package/esm/http/flush-pending-cookies.mjs.map +1 -0
- package/esm/http/middleware/cache-response-middleware.d.mts.map +1 -1
- package/esm/http/middleware/cache-response-middleware.mjs +4 -2
- package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +14 -3
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/utils/cidr-match.d.mts +1 -1
- package/esm/http/middleware/utils/cidr-match.d.mts.map +1 -1
- package/esm/http/middleware/utils/cidr-match.mjs +11 -2
- package/esm/http/middleware/utils/cidr-match.mjs.map +1 -1
- package/esm/http/middleware/utils/idempotency-key.mjs +1 -1
- package/esm/http/middleware/utils/idempotency-key.mjs.map +1 -1
- package/esm/http/plugins.d.mts.map +1 -1
- package/esm/http/plugins.mjs +10 -1
- package/esm/http/plugins.mjs.map +1 -1
- package/esm/http/request.d.mts +12 -1
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +35 -7
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts +16 -0
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +75 -37
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/types.d.mts +24 -0
- package/esm/http/types.d.mts.map +1 -1
- package/esm/http/uploads/uploaded-file.controller.mjs +1 -1
- package/esm/http/uploads-config.mjs +2 -1
- package/esm/http/uploads-config.mjs.map +1 -1
- package/esm/http/uploads-types.d.mts +3 -3
- package/esm/image/image.d.mts +1 -1
- package/esm/image/image.d.mts.map +1 -1
- package/esm/image/image.mjs +25 -15
- package/esm/image/image.mjs.map +1 -1
- package/esm/index.d.mts +4 -3
- package/esm/index.mjs +5 -3
- package/esm/logger/logger.mjs +6 -1
- package/esm/logger/logger.mjs.map +1 -1
- package/esm/mail/config.d.mts.map +1 -1
- package/esm/mail/config.mjs +31 -21
- package/esm/mail/config.mjs.map +1 -1
- package/esm/mail/react-mail.mjs +6 -3
- package/esm/mail/react-mail.mjs.map +1 -1
- package/esm/mail/send-mail.d.mts.map +1 -1
- package/esm/mail/send-mail.mjs +40 -19
- package/esm/mail/send-mail.mjs.map +1 -1
- package/esm/production/build-contributions.mjs +3 -0
- package/esm/production/build-contributions.mjs.map +1 -1
- package/esm/production/esbuild-plugins.mjs +3 -2
- package/esm/production/esbuild-plugins.mjs.map +1 -1
- package/esm/production/production-builder.mjs +32 -36
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/production-supervisor.mjs +12 -5
- package/esm/production/production-supervisor.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-adapter.d.mts +3 -1
- package/esm/repositories/adapters/cascade/cascade-adapter.d.mts.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-adapter.mjs +17 -5
- package/esm/repositories/adapters/cascade/cascade-adapter.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-query-builder.d.mts +3 -3
- package/esm/repositories/adapters/cascade/cascade-query-builder.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/filter-applicator.mjs +88 -61
- package/esm/repositories/adapters/cascade/filter-applicator.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/index.d.mts +1 -1
- package/esm/repositories/adapters/cascade/index.mjs +1 -1
- package/esm/repositories/contracts/query-builder.contract.d.mts +3 -3
- package/esm/repositories/contracts/types.d.mts +5 -0
- package/esm/repositories/contracts/types.d.mts.map +1 -1
- package/esm/repositories/index.d.mts +1 -1
- package/esm/repositories/index.mjs +1 -1
- package/esm/repositories/repository.manager.d.mts +65 -3
- package/esm/repositories/repository.manager.d.mts.map +1 -1
- package/esm/repositories/repository.manager.mjs +146 -57
- package/esm/repositories/repository.manager.mjs.map +1 -1
- package/esm/resource/resource-field-builder.d.mts.map +1 -1
- package/esm/resource/resource-field-builder.mjs +1 -0
- package/esm/resource/resource-field-builder.mjs.map +1 -1
- package/esm/resource/resource.d.mts +10 -3
- package/esm/resource/resource.d.mts.map +1 -1
- package/esm/resource/resource.mjs +19 -5
- package/esm/resource/resource.mjs.map +1 -1
- package/esm/restful/restful.d.mts +12 -5
- package/esm/restful/restful.d.mts.map +1 -1
- package/esm/restful/restful.mjs +47 -16
- package/esm/restful/restful.mjs.map +1 -1
- package/esm/router/build-route-rate-limit.mjs +22 -0
- package/esm/router/build-route-rate-limit.mjs.map +1 -0
- package/esm/router/dev-dispatch.mjs +60 -0
- package/esm/router/dev-dispatch.mjs.map +1 -0
- package/esm/router/route-builder.mjs +4 -1
- package/esm/router/route-builder.mjs.map +1 -1
- package/esm/router/route-registry.d.mts.map +1 -1
- package/esm/router/route-registry.mjs +2 -2
- package/esm/router/route-registry.mjs.map +1 -1
- package/esm/router/router.d.mts +1 -1
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +57 -39
- package/esm/router/router.mjs.map +1 -1
- package/esm/socket/types.d.mts +9 -2
- package/esm/socket/types.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.d.mts +17 -0
- package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.mjs +53 -17
- package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
- package/esm/storage/drivers/local-driver.d.mts +7 -5
- package/esm/storage/drivers/local-driver.d.mts.map +1 -1
- package/esm/storage/drivers/local-driver.mjs +31 -26
- package/esm/storage/drivers/local-driver.mjs.map +1 -1
- package/esm/storage/index.d.mts +2 -1
- package/esm/storage/index.mjs +2 -1
- package/esm/storage/scoped-storage.d.mts +13 -1
- package/esm/storage/scoped-storage.d.mts.map +1 -1
- package/esm/storage/scoped-storage.mjs +18 -8
- package/esm/storage/scoped-storage.mjs.map +1 -1
- package/esm/storage/storage.d.mts +0 -5
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/storage.mjs +7 -16
- package/esm/storage/storage.mjs.map +1 -1
- package/esm/storage/types.d.mts +22 -4
- package/esm/storage/types.d.mts.map +1 -1
- package/esm/storage/utils/classify-ip-address.d.mts +13 -0
- package/esm/storage/utils/classify-ip-address.d.mts.map +1 -0
- package/esm/storage/utils/classify-ip-address.mjs +98 -0
- package/esm/storage/utils/classify-ip-address.mjs.map +1 -0
- package/esm/storage/utils/safe-fetch.d.mts +3 -12
- package/esm/storage/utils/safe-fetch.d.mts.map +1 -1
- package/esm/storage/utils/safe-fetch.mjs +3 -48
- package/esm/storage/utils/safe-fetch.mjs.map +1 -1
- package/esm/updater/update-warlock-packages.mjs +11 -1
- package/esm/updater/update-warlock-packages.mjs.map +1 -1
- package/esm/use-cases/use-case-broadcast.mjs +7 -1
- package/esm/use-cases/use-case-broadcast.mjs.map +1 -1
- package/esm/use-cases/use-cases-registry.mjs +21 -2
- package/esm/use-cases/use-cases-registry.mjs.map +1 -1
- package/esm/utils/database-log.mjs +0 -3
- package/esm/utils/database-log.mjs.map +1 -1
- package/esm/utils/queue.d.mts +6 -4
- package/esm/utils/queue.d.mts.map +1 -1
- package/esm/utils/queue.mjs +20 -9
- package/esm/utils/queue.mjs.map +1 -1
- package/esm/validation/database/exists-except-current-id.mjs +6 -2
- package/esm/validation/database/exists-except-current-id.mjs.map +1 -1
- package/esm/validation/database/unique-except-current-id.mjs +6 -2
- package/esm/validation/database/unique-except-current-id.mjs.map +1 -1
- package/esm/validation/init.mjs +6 -2
- package/esm/validation/init.mjs.map +1 -1
- package/esm/validation/validateAll.mjs +3 -2
- package/esm/validation/validateAll.mjs.map +1 -1
- package/esm/warlock-config/types.d.mts +14 -1
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/esm/warlock-config/warlock-config.manager.d.mts.map +1 -1
- package/esm/warlock-config/warlock-config.manager.mjs +3 -1
- package/esm/warlock-config/warlock-config.manager.mjs.map +1 -1
- package/esm/warlock-config/warn-deprecated-config.mjs +25 -0
- package/esm/warlock-config/warn-deprecated-config.mjs.map +1 -0
- package/package.json +11 -11
- package/esm/_virtual/_rolldown/runtime.mjs +0 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-fetch.mjs","names":[],"sources":["../../../../../../../../core/src/storage/utils/safe-fetch.ts"],"sourcesContent":["import { lookup } from \"node:dns/promises\";\nimport { isIP } from \"@warlock.js/seal\";\nimport { StorageError } from \"./storage-error\";\n\n/**\n * SSRF / resource-exhaustion guard for storage's outbound downloads\n * (`putFromUrl`).\n *\n * NOTE: this MIRRORS `@warlock.js/ai`'s `src/security/private-ip.ts` and\n * `src/security/outbound-policy.ts`. It is duplicated here intentionally\n * because `@warlock.js/core` cannot depend on `@warlock.js/ai`. Pending\n * extraction of a shared security package, keep the two copies in sync.\n */\n\n/** 50 MiB — default cap on a downloaded body (storage files run larger than AI payloads). */\nconst DEFAULT_MAX_BYTES = 50 * 1024 * 1024;\n/** 30s — default per-request timeout. */\nconst DEFAULT_TIMEOUT_MS = 30_000;\n/**\n * Cap on the number of re-validated redirect hops. Mirrors\n * `@warlock.js/ai`'s `DEFAULT_MAX_REDIRECTS`\n * (ai/src/security/outbound-policy.ts:15).\n */\nconst DEFAULT_MAX_REDIRECTS = 5;\n\n/** 3xx statuses whose `Location` a follow re-issues — mirrors ai/src/security/outbound-policy.ts:18. */\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\n\n/**\n * Options controlling a guarded outbound fetch. Every field is optional;\n * {@link resolveFetchPolicy} fills safe defaults. The defaults are\n * deliberately strict (https+http, private-IP deny ON, 30s timeout,\n * 50 MiB cap) so a caller that does not tune them still gets a hardened\n * request.\n */\nexport type SafeFetchOptions = {\n /**\n * Whether to ALLOW private / loopback / link-local / metadata hosts.\n * Default `false` — the SSRF guard is on (private hosts are denied).\n */\n allowPrivateHosts?: boolean;\n /**\n * Maximum response body size in bytes. A declared `content-length` over\n * this fails fast; otherwise the body is read with a running cap and the\n * stream is aborted on overflow. Default `52_428_800` (50 MiB).\n */\n maxBytes?: number;\n /** Per-request timeout in milliseconds. Default `30_000`. */\n timeoutMs?: number;\n /**\n * URL schemes permitted for the request, compared case-insensitively.\n * Default `[\"https\", \"http\"]`.\n */\n allowedSchemes?: string[];\n /**\n * Injected `fetch` implementation (for tests or proxies). Defaults to the\n * global `fetch`.\n */\n fetch?: typeof fetch;\n};\n\n/** {@link SafeFetchOptions} with every default resolved — never partial. */\ntype ResolvedFetchPolicy = {\n allowPrivateHosts: boolean;\n maxBytes: number;\n timeoutMs: number;\n allowedSchemes: string[];\n fetch: typeof fetch;\n};\n\n/**\n * Fill a {@link SafeFetchOptions} with strict defaults: https+http,\n * private-IP deny on, 30s timeout, 50 MiB cap, global `fetch`.\n */\nfunction resolveFetchPolicy(options: SafeFetchOptions = {}): ResolvedFetchPolicy {\n return {\n allowPrivateHosts: options.allowPrivateHosts ?? false,\n maxBytes: options.maxBytes ?? DEFAULT_MAX_BYTES,\n timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n allowedSchemes: options.allowedSchemes ?? [\"https\", \"http\"],\n fetch: options.fetch ?? globalThis.fetch,\n };\n}\n\n/** Parse a dotted-quad into four octets, or `null` if malformed. */\nfunction parseIpv4(ip: string): [number, number, number, number] | null {\n const parts = ip.split(\".\");\n if (parts.length !== 4) {\n return null;\n }\n\n const octets = parts.map((part) => Number(part));\n if (octets.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) {\n return null;\n }\n\n return octets as [number, number, number, number];\n}\n\nfunction isPrivateIpv4(ip: string): boolean {\n const octets = parseIpv4(ip);\n if (!octets) {\n return true; // unparseable → refuse, fail closed\n }\n\n const [a, b] = octets;\n\n return (\n a === 0 || // 0.0.0.0/8 \"this network\"\n a === 10 || // 10.0.0.0/8 private\n a === 127 || // 127.0.0.0/8 loopback\n (a === 100 && b >= 64 && b <= 127) || // 100.64.0.0/10 CGNAT\n (a === 169 && b === 254) || // 169.254.0.0/16 link-local + metadata\n (a === 172 && b >= 16 && b <= 31) || // 172.16.0.0/12 private\n (a === 192 && b === 168) || // 192.168.0.0/16 private\n (a === 192 && b === 0) || // 192.0.0.0/24 + 192.0.2.0/24 (IETF/test)\n (a === 198 && (b === 18 || b === 19)) || // 198.18.0.0/15 benchmarking\n (a === 198 && b === 51) || // 198.51.100.0/24 test-net-2\n (a === 203 && b === 0) || // 203.0.113.0/24 test-net-3\n a >= 224 // 224.0.0.0/4 multicast + 240.0.0.0/4 reserved + 255.255.255.255\n );\n}\n\nfunction isPrivateIpv6(ip: string): boolean {\n const normalized = ip.toLowerCase().split(\"%\")[0]; // drop zone id\n\n // EVERY fallback in this function returns `true`, and that direction is the\n // whole point: this is an SSRF guard, and `true` means \"private — refuse\".\n //\n // `split` on a non-empty string always yields a first element, so none of\n // these is reachable today. But the safe answer for an address we could not\n // classify is to REFUSE it, not to let it through. A `?? \"\"` here would fall\n // past every check below and return `false` — reporting an unparseable\n // address as public, which is a fail-OPEN in the one function whose job is\n // to keep requests off the internal network.\n if (normalized === undefined) {\n return true;\n }\n\n // IPv4-mapped / -embedded (::ffff:a.b.c.d, ::a.b.c.d) — defer to the v4\n // check on the trailing dotted-quad so an inward-mapped address is caught.\n const v4 = normalized.match(/(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})$/);\n if (v4) {\n const dottedQuad = v4[1];\n\n // Capture group 1 exists whenever this pattern matched. Refusing rather\n // than defaulting, for the reason above.\n return dottedQuad === undefined ? true : isPrivateIpv4(dottedQuad);\n }\n\n if (normalized === \"::1\" || normalized === \"::\") {\n return true; // loopback / unspecified\n }\n\n // Expand only the leading group enough to classify the reserved blocks.\n const firstGroup = normalized.split(\":\")[0];\n\n if (firstGroup === undefined) {\n return true;\n }\n\n const head = firstGroup === \"\" ? 0 : Number.parseInt(firstGroup, 16);\n\n // fc00::/7 unique-local (fc.. / fd..)\n if ((head & 0xfe00) === 0xfc00) {\n return true;\n }\n // fe80::/10 link-local\n if ((head & 0xffc0) === 0xfe80) {\n return true;\n }\n\n return false;\n}\n\n/**\n * True when `ip` is a private, loopback, link-local, unique-local,\n * carrier-grade-NAT, unspecified, or otherwise non-public address — the set\n * an SSRF guard must refuse. Accepts IPv4 and IPv6 literals (including\n * IPv4-mapped IPv6 like `::ffff:169.254.169.254`). A non-IP string returns\n * `false` (the caller resolves hostnames via DNS first).\n *\n * The cloud-metadata endpoint `169.254.169.254` is covered by the IPv4\n * link-local range `169.254.0.0/16`.\n */\nexport function isPrivateOrReservedIp(ip: string): boolean {\n const family = isIP(ip);\n if (family === 4) {\n return isPrivateIpv4(ip);\n }\n if (family === 6) {\n return isPrivateIpv6(ip);\n }\n return false;\n}\n\n/** Strip the `[ ]` IPv6 brackets `URL.hostname` keeps. */\nfunction stripBrackets(host: string): string {\n return host.startsWith(\"[\") && host.endsWith(\"]\") ? host.slice(1, -1) : host;\n}\n\n/**\n * Reject when `host` is — or resolves to — a private / reserved address.\n * IP literals are checked directly; hostnames are resolved via DNS and every\n * returned address is checked (a public name pointing inward is caught). A\n * resolution failure fails closed.\n */\nasync function assertHostNotPrivate(host: string, rawUrl: string): Promise<void> {\n if (isIP(host) !== 0) {\n if (isPrivateOrReservedIp(host)) {\n throw new StorageError(`outbound request blocked — \"${host}\" is a private/reserved address`, {\n context: { url: rawUrl, address: host },\n });\n }\n return;\n }\n\n let addresses: Array<{ address: string }>;\n try {\n addresses = await lookup(host, { all: true });\n } catch (cause) {\n throw new StorageError(\n `outbound request blocked — could not resolve host \"${host}\" to verify it is public`,\n { cause, context: { url: rawUrl, host } },\n );\n }\n\n for (const { address } of addresses) {\n if (isPrivateOrReservedIp(address)) {\n throw new StorageError(\n `outbound request blocked — host \"${host}\" resolves to a private/reserved address (${address})`,\n { context: { url: rawUrl, host, address } },\n );\n }\n }\n}\n\n/**\n * Validate a URL against the policy BEFORE any network call: scheme\n * allowlist and (when private hosts are not allowed) a DNS resolution that\n * rejects private / loopback / link-local / metadata addresses — the SSRF\n * guard. Returns the parsed `URL` on success; throws {@link StorageError}\n * otherwise.\n */\nasync function assertUrlAllowed(rawUrl: string, policy: ResolvedFetchPolicy): Promise<URL> {\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n throw new StorageError(`outbound request blocked — invalid URL: ${rawUrl}`, {\n context: { url: rawUrl },\n });\n }\n\n const scheme = url.protocol.replace(/:$/, \"\").toLowerCase();\n if (!policy.allowedSchemes.some((s) => s.toLowerCase() === scheme)) {\n throw new StorageError(\n `outbound request blocked — scheme \"${scheme}\" is not allowed (allowed: ${policy.allowedSchemes.join(\", \")})`,\n { context: { url: rawUrl, scheme } },\n );\n }\n\n if (!policy.allowPrivateHosts) {\n const host = stripBrackets(url.hostname);\n await assertHostNotPrivate(host, rawUrl);\n }\n\n return url;\n}\n\n/**\n * Result of {@link safeFetchToBuffer}: the (capped) body plus the response's\n * content-type, so callers can derive a MIME type and enforce a non-empty\n * content-type check.\n */\nexport type SafeFetchResult = {\n buffer: Buffer;\n contentType: string | null;\n status: number;\n statusText: string;\n ok: boolean;\n};\n\n/**\n * SSRF-guarded download: validates the URL (scheme allowlist + post-DNS\n * private-IP deny), performs the request with a timeout via\n * `AbortController`, then streams the body with a hard byte cap — aborting\n * the moment the running total exceeds `maxBytes`. A declared\n * `content-length` over the cap fails fast.\n *\n * Redirects are NEVER delegated to the platform: every hop is issued with\n * `redirect: \"manual\"` and its `Location` is re-run through\n * {@link assertUrlAllowed} — including the DNS-resolution private-IP check\n * — before being followed (capped at {@link DEFAULT_MAX_REDIRECTS}), so a\n * 3xx from an allowed host cannot smuggle the request to a private /\n * metadata target. Mirrors `@warlock.js/ai`'s `guardedFetch`\n * (ai/src/security/outbound-policy.ts:188-284); duplicated per this\n * module's file-header note since core cannot depend on ai.\n *\n * @throws {StorageError} On a blocked URL (initial or redirect target),\n * a redirect-hop bound overflow, timeout, or oversized body.\n */\nexport async function safeFetchToBuffer(\n rawUrl: string,\n options: SafeFetchOptions = {},\n): Promise<SafeFetchResult> {\n const policy = resolveFetchPolicy(options);\n let url = await assertUrlAllowed(rawUrl, policy);\n\n const controller = new AbortController();\n const timer = setTimeout(() => {\n controller.abort(\n new StorageError(`outbound request timed out after ${policy.timeoutMs}ms`, {\n context: { url: rawUrl, timeoutMs: policy.timeoutMs },\n }),\n );\n }, policy.timeoutMs);\n\n try {\n let response: Response;\n\n for (let hop = 0; ; hop++) {\n response = await policy.fetch(url, { signal: controller.signal, redirect: \"manual\" });\n\n const location = response.headers.get(\"location\");\n if (!REDIRECT_STATUSES.has(response.status) || location === null) {\n break;\n }\n\n if (hop >= DEFAULT_MAX_REDIRECTS) {\n throw new StorageError(\n `outbound request blocked — more than ${DEFAULT_MAX_REDIRECTS} redirects`,\n { context: { url: rawUrl, maxRedirects: DEFAULT_MAX_REDIRECTS } },\n );\n }\n\n let target: URL;\n try {\n target = new URL(location, url);\n } catch {\n throw new StorageError(\n `outbound request blocked — invalid redirect Location: ${location}`,\n {\n context: { url: url.toString(), location },\n },\n );\n }\n\n // Discard the interim body so the connection can be reused.\n if (response.body) {\n await response.body.cancel().catch(() => undefined);\n }\n\n // The redirect target gets the SAME scheme + private-IP validation\n // (incl. DNS resolution) as the initial URL — this is what closes\n // the redirect-based SSRF bypass: a 302 from an allowed host to\n // 169.254.169.254 is re-validated here, not blindly followed.\n url = await assertUrlAllowed(target.toString(), policy);\n }\n\n const declared = Number(response.headers.get(\"content-length\"));\n if (Number.isFinite(declared) && declared > policy.maxBytes) {\n throw new StorageError(\n `outbound response body too large — declared ${declared} bytes exceeds the ${policy.maxBytes}-byte cap`,\n { context: { url: rawUrl, declared, maxBytes: policy.maxBytes } },\n );\n }\n\n const buffer = await readBodyCapped(response, policy.maxBytes, rawUrl);\n\n return {\n buffer,\n contentType: response.headers.get(\"content-type\"),\n status: response.status,\n statusText: response.statusText,\n ok: response.ok,\n };\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Read a response body into a Buffer with a hard byte cap. The stream is\n * read chunk-by-chunk and cancelled the moment the running total exceeds\n * `maxBytes`. Throws {@link StorageError} on overflow.\n */\nasync function readBodyCapped(\n response: Response,\n maxBytes: number,\n rawUrl: string,\n): Promise<Buffer> {\n if (!response.body) {\n const arrayBuffer = await response.arrayBuffer();\n if (arrayBuffer.byteLength > maxBytes) {\n throw new StorageError(`outbound response body exceeded the ${maxBytes}-byte cap`, {\n context: { url: rawUrl, maxBytes },\n });\n }\n return Buffer.from(arrayBuffer);\n }\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n if (!value) {\n continue;\n }\n\n total += value.byteLength;\n if (total > maxBytes) {\n await reader.cancel();\n throw new StorageError(`outbound response body exceeded the ${maxBytes}-byte cap`, {\n context: { url: rawUrl, maxBytes },\n });\n }\n chunks.push(value);\n }\n\n return Buffer.concat(chunks);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"safe-fetch.mjs","names":[],"sources":["../../../../../../../../core/src/storage/utils/safe-fetch.ts"],"sourcesContent":["import { lookup } from \"node:dns/promises\";\nimport { isIP } from \"node:net\";\nimport { isPrivateOrReservedIp } from \"./classify-ip-address\";\nimport { StorageError } from \"./storage-error\";\n\n/**\n * SSRF / resource-exhaustion guard for storage's outbound downloads\n * (`putFromUrl`).\n *\n * NOTE: this MIRRORS `@warlock.js/ai`'s `src/security/private-ip.ts` and\n * `src/security/outbound-policy.ts`. It is duplicated here intentionally\n * because `@warlock.js/core` cannot depend on `@warlock.js/ai`. Pending\n * extraction of a shared security package, keep the two copies in sync.\n */\n\n/** 50 MiB — default cap on a downloaded body (storage files run larger than AI payloads). */\nconst DEFAULT_MAX_BYTES = 50 * 1024 * 1024;\n/** 30s — default per-request timeout. */\nconst DEFAULT_TIMEOUT_MS = 30_000;\n/**\n * Cap on the number of re-validated redirect hops. Mirrors\n * `@warlock.js/ai`'s `DEFAULT_MAX_REDIRECTS`\n * (ai/src/security/outbound-policy.ts:15).\n */\nconst DEFAULT_MAX_REDIRECTS = 5;\n\n/** 3xx statuses whose `Location` a follow re-issues — mirrors ai/src/security/outbound-policy.ts:18. */\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\n\n/**\n * Options controlling a guarded outbound fetch. Every field is optional;\n * {@link resolveFetchPolicy} fills safe defaults. The defaults are\n * deliberately strict (https+http, private-IP deny ON, 30s timeout,\n * 50 MiB cap) so a caller that does not tune them still gets a hardened\n * request.\n */\nexport type SafeFetchOptions = {\n /**\n * Whether to ALLOW private / loopback / link-local / metadata hosts.\n * Default `false` — the SSRF guard is on (private hosts are denied).\n */\n allowPrivateHosts?: boolean;\n /**\n * Maximum response body size in bytes. A declared `content-length` over\n * this fails fast; otherwise the body is read with a running cap and the\n * stream is aborted on overflow. Default `52_428_800` (50 MiB).\n */\n maxBytes?: number;\n /** Per-request timeout in milliseconds. Default `30_000`. */\n timeoutMs?: number;\n /**\n * URL schemes permitted for the request, compared case-insensitively.\n * Default `[\"https\", \"http\"]`.\n */\n allowedSchemes?: string[];\n /**\n * Injected `fetch` implementation (for tests or proxies). Defaults to the\n * global `fetch`.\n */\n fetch?: typeof fetch;\n};\n\n/** {@link SafeFetchOptions} with every default resolved — never partial. */\ntype ResolvedFetchPolicy = {\n allowPrivateHosts: boolean;\n maxBytes: number;\n timeoutMs: number;\n allowedSchemes: string[];\n fetch: typeof fetch;\n};\n\n/**\n * Fill a {@link SafeFetchOptions} with strict defaults: https+http,\n * private-IP deny on, 30s timeout, 50 MiB cap, global `fetch`.\n */\nfunction resolveFetchPolicy(options: SafeFetchOptions = {}): ResolvedFetchPolicy {\n return {\n allowPrivateHosts: options.allowPrivateHosts ?? false,\n maxBytes: options.maxBytes ?? DEFAULT_MAX_BYTES,\n timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n allowedSchemes: options.allowedSchemes ?? [\"https\", \"http\"],\n fetch: options.fetch ?? globalThis.fetch,\n };\n}\n\nexport { isPrivateOrReservedIp };\n\n/** Strip the `[ ]` IPv6 brackets `URL.hostname` keeps. */\nfunction stripBrackets(host: string): string {\n return host.startsWith(\"[\") && host.endsWith(\"]\") ? host.slice(1, -1) : host;\n}\n\n/**\n * Reject when `host` is — or resolves to — a private / reserved address.\n * IP literals are checked directly; hostnames are resolved via DNS and every\n * returned address is checked (a public name pointing inward is caught). A\n * resolution failure fails closed.\n */\nasync function assertHostNotPrivate(host: string, rawUrl: string): Promise<void> {\n if (isIP(host) !== 0) {\n if (isPrivateOrReservedIp(host)) {\n throw new StorageError(`outbound request blocked — \"${host}\" is a private/reserved address`, {\n context: { url: rawUrl, address: host },\n });\n }\n return;\n }\n\n let addresses: Array<{ address: string }>;\n try {\n addresses = await lookup(host, { all: true });\n } catch (cause) {\n throw new StorageError(\n `outbound request blocked — could not resolve host \"${host}\" to verify it is public`,\n { cause, context: { url: rawUrl, host } },\n );\n }\n\n for (const { address } of addresses) {\n if (isPrivateOrReservedIp(address)) {\n throw new StorageError(\n `outbound request blocked — host \"${host}\" resolves to a private/reserved address (${address})`,\n { context: { url: rawUrl, host, address } },\n );\n }\n }\n}\n\n/**\n * Validate a URL against the policy BEFORE any network call: scheme\n * allowlist and (when private hosts are not allowed) a DNS resolution that\n * rejects private / loopback / link-local / metadata addresses — the SSRF\n * guard. Returns the parsed `URL` on success; throws {@link StorageError}\n * otherwise.\n */\nasync function assertUrlAllowed(rawUrl: string, policy: ResolvedFetchPolicy): Promise<URL> {\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n throw new StorageError(`outbound request blocked — invalid URL: ${rawUrl}`, {\n context: { url: rawUrl },\n });\n }\n\n const scheme = url.protocol.replace(/:$/, \"\").toLowerCase();\n if (!policy.allowedSchemes.some((s) => s.toLowerCase() === scheme)) {\n throw new StorageError(\n `outbound request blocked — scheme \"${scheme}\" is not allowed (allowed: ${policy.allowedSchemes.join(\", \")})`,\n { context: { url: rawUrl, scheme } },\n );\n }\n\n if (!policy.allowPrivateHosts) {\n const host = stripBrackets(url.hostname);\n await assertHostNotPrivate(host, rawUrl);\n }\n\n return url;\n}\n\n/**\n * Result of {@link safeFetchToBuffer}: the (capped) body plus the response's\n * content-type, so callers can derive a MIME type and enforce a non-empty\n * content-type check.\n */\nexport type SafeFetchResult = {\n buffer: Buffer;\n contentType: string | null;\n status: number;\n statusText: string;\n ok: boolean;\n};\n\n/**\n * SSRF-guarded download: validates the URL (scheme allowlist + post-DNS\n * private-IP deny), performs the request with a timeout via\n * `AbortController`, then streams the body with a hard byte cap — aborting\n * the moment the running total exceeds `maxBytes`. A declared\n * `content-length` over the cap fails fast.\n *\n * Redirects are NEVER delegated to the platform: every hop is issued with\n * `redirect: \"manual\"` and its `Location` is re-run through\n * {@link assertUrlAllowed} — including the DNS-resolution private-IP check\n * — before being followed (capped at {@link DEFAULT_MAX_REDIRECTS}), so a\n * 3xx from an allowed host cannot smuggle the request to a private /\n * metadata target. Mirrors `@warlock.js/ai`'s `guardedFetch`\n * (ai/src/security/outbound-policy.ts:188-284); duplicated per this\n * module's file-header note since core cannot depend on ai.\n *\n * @throws {StorageError} On a blocked URL (initial or redirect target),\n * a redirect-hop bound overflow, timeout, or oversized body.\n */\nexport async function safeFetchToBuffer(\n rawUrl: string,\n options: SafeFetchOptions = {},\n): Promise<SafeFetchResult> {\n const policy = resolveFetchPolicy(options);\n let url = await assertUrlAllowed(rawUrl, policy);\n\n const controller = new AbortController();\n const timer = setTimeout(() => {\n controller.abort(\n new StorageError(`outbound request timed out after ${policy.timeoutMs}ms`, {\n context: { url: rawUrl, timeoutMs: policy.timeoutMs },\n }),\n );\n }, policy.timeoutMs);\n\n try {\n let response: Response;\n\n for (let hop = 0; ; hop++) {\n response = await policy.fetch(url, { signal: controller.signal, redirect: \"manual\" });\n\n const location = response.headers.get(\"location\");\n if (!REDIRECT_STATUSES.has(response.status) || location === null) {\n break;\n }\n\n if (hop >= DEFAULT_MAX_REDIRECTS) {\n throw new StorageError(\n `outbound request blocked — more than ${DEFAULT_MAX_REDIRECTS} redirects`,\n { context: { url: rawUrl, maxRedirects: DEFAULT_MAX_REDIRECTS } },\n );\n }\n\n let target: URL;\n try {\n target = new URL(location, url);\n } catch {\n throw new StorageError(\n `outbound request blocked — invalid redirect Location: ${location}`,\n {\n context: { url: url.toString(), location },\n },\n );\n }\n\n // Discard the interim body so the connection can be reused.\n if (response.body) {\n await response.body.cancel().catch(() => undefined);\n }\n\n // The redirect target gets the SAME scheme + private-IP validation\n // (incl. DNS resolution) as the initial URL — this is what closes\n // the redirect-based SSRF bypass: a 302 from an allowed host to\n // 169.254.169.254 is re-validated here, not blindly followed.\n url = await assertUrlAllowed(target.toString(), policy);\n }\n\n const declared = Number(response.headers.get(\"content-length\"));\n if (Number.isFinite(declared) && declared > policy.maxBytes) {\n throw new StorageError(\n `outbound response body too large — declared ${declared} bytes exceeds the ${policy.maxBytes}-byte cap`,\n { context: { url: rawUrl, declared, maxBytes: policy.maxBytes } },\n );\n }\n\n const buffer = await readBodyCapped(response, policy.maxBytes, rawUrl);\n\n return {\n buffer,\n contentType: response.headers.get(\"content-type\"),\n status: response.status,\n statusText: response.statusText,\n ok: response.ok,\n };\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Read a response body into a Buffer with a hard byte cap. The stream is\n * read chunk-by-chunk and cancelled the moment the running total exceeds\n * `maxBytes`. Throws {@link StorageError} on overflow.\n */\nasync function readBodyCapped(\n response: Response,\n maxBytes: number,\n rawUrl: string,\n): Promise<Buffer> {\n if (!response.body) {\n const arrayBuffer = await response.arrayBuffer();\n if (arrayBuffer.byteLength > maxBytes) {\n throw new StorageError(`outbound response body exceeded the ${maxBytes}-byte cap`, {\n context: { url: rawUrl, maxBytes },\n });\n }\n return Buffer.from(arrayBuffer);\n }\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n if (!value) {\n continue;\n }\n\n total += value.byteLength;\n if (total > maxBytes) {\n await reader.cancel();\n throw new StorageError(`outbound response body exceeded the ${maxBytes}-byte cap`, {\n context: { url: rawUrl, maxBytes },\n });\n }\n chunks.push(value);\n }\n\n return Buffer.concat(chunks);\n}\n"],"mappings":"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"}
|
|
@@ -54,6 +54,14 @@ async function updateWarlockPackages(options = {}) {
|
|
|
54
54
|
updates: []
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
+
const failedLookups = resolved.filter((dependency) => !dependency.latest);
|
|
58
|
+
if (failedLookups.length > 0) {
|
|
59
|
+
console.log(`${colors.yellow("⚠")} Could not look up ${failedLookups.map((dependency) => dependency.name).join(", ")} — no versions were changed, to keep @warlock.js packages in lockstep. Try again.`);
|
|
60
|
+
return {
|
|
61
|
+
outcome: "registry-unreachable",
|
|
62
|
+
updates: []
|
|
63
|
+
};
|
|
64
|
+
}
|
|
57
65
|
const updates = resolvePackageUpdates(resolved);
|
|
58
66
|
if (updates.length === 0) {
|
|
59
67
|
console.log(`${colors.green("✓")} All @warlock.js packages are already up to date.`);
|
|
@@ -154,9 +162,11 @@ function resolvePackageUpdates(resolved) {
|
|
|
154
162
|
function parseSpec(spec) {
|
|
155
163
|
const match = spec.trim().match(/^(\^|~)?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/);
|
|
156
164
|
if (!match) return;
|
|
165
|
+
const version = match[2];
|
|
166
|
+
if (version === void 0) return;
|
|
157
167
|
return {
|
|
158
168
|
operator: match[1] ?? "",
|
|
159
|
-
version
|
|
169
|
+
version
|
|
160
170
|
};
|
|
161
171
|
}
|
|
162
172
|
/** Write the resolved bumps back into the package.json object in place. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update-warlock-packages.mjs","names":[],"sources":["../../../../../../../core/src/updater/update-warlock-packages.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, getJsonFileAsync, putJsonFileAsync } from \"@warlock.js/fs\";\nimport { execSync } from \"node:child_process\";\nimport { rootPath } from \"../utils\";\nimport { fetchLatestVersion } from \"../utils/npm-registry\";\nimport { isNewerVersion } from \"../utils/version-compare\";\nimport { detectPackageManager, getInstallCommand } from \"./package-manager\";\n\n/** Scope prefix that identifies a Warlock framework package. */\nconst WARLOCK_SCOPE = \"@warlock.js/\";\n\n/** The dependency maps in package.json we update, in display order. */\nconst DEPENDENCY_SECTIONS = [\"dependencies\", \"devDependencies\"] as const;\n\ntype DependencySection = (typeof DEPENDENCY_SECTIONS)[number];\n\ntype DependencyMap = Record<string, string>;\n\ntype PackageJson = Record<string, unknown> & {\n dependencies?: DependencyMap;\n devDependencies?: DependencyMap;\n};\n\n/** A `@warlock.js/*` dependency found in package.json. */\ntype WarlockDependency = {\n name: string;\n section: DependencySection;\n /** The current spec exactly as written, e.g. `^4.2.0`. */\n current: string;\n};\n\n/** A dependency whose latest version has been resolved from the registry. */\nexport type ResolvedDependency = WarlockDependency & {\n /** Latest version from npm, or `undefined` when the lookup failed. */\n latest: string | undefined;\n};\n\n/** A concrete version bump to write back into package.json. */\nexport type PackageUpdate = {\n name: string;\n section: DependencySection;\n /** Existing spec, e.g. `^4.2.0`. */\n from: string;\n /** New spec with the original range operator preserved, e.g. `^4.3.0`. */\n to: string;\n};\n\n/** Options accepted by {@link updateWarlockPackages}. */\nexport type UpdateWarlockPackagesOptions = {\n /** Run the package manager install after rewriting versions (default true). */\n install?: boolean;\n /**\n * Report what would change without touching package.json or node_modules.\n * Resolves with the `outdated` outcome instead of `updated`.\n */\n dryRun?: boolean;\n};\n\n/**\n * How an update run ended. Callers branch on this instead of re-deriving the\n * situation from the printed output — the dev-server `u` shortcut needs to\n * know whether restarting is warranted, and whether it may offer a retry.\n */\nexport type UpdateOutcome =\n /** No package.json at the project root — nothing to do. */\n | \"no-package-json\"\n /** package.json carries no `@warlock.js/*` dependency. */\n | \"no-packages\"\n /** Every registry lookup failed — offline, DNS down, or npm unreachable. */\n | \"registry-unreachable\"\n /** Every package is already at (or ahead of) its latest published version. */\n | \"up-to-date\"\n /** Newer versions exist, and a dry run left everything untouched. */\n | \"outdated\"\n /** Versions were rewritten (and installed, unless `install: false`). */\n | \"updated\"\n /** Versions were rewritten but the package manager install failed. */\n | \"install-failed\";\n\n/** The result of an update run. */\nexport type UpdateWarlockPackagesResult = {\n outcome: UpdateOutcome;\n /** The bumps written to package.json — empty unless `outcome` wrote any. */\n updates: PackageUpdate[];\n /** The install failure, present only for `install-failed`. */\n error?: Error;\n};\n\n/**\n * Update every `@warlock.js/*` package listed in the project's root\n * package.json to its latest published version, then reconcile `node_modules`\n * via the detected package manager.\n *\n * The original range operator on each dependency (`^`, `~`, or an exact pin)\n * is preserved; specs that aren't a plain semver — `workspace:*`, `*`,\n * `latest`, git/file URLs — are left untouched. Only genuine upgrades are\n * written, so re-running on an already-current project is a no-op.\n *\n * Resolves rather than throws on the two failures a caller is expected to\n * survive — an unreachable registry and a failed install — so the dev-server\n * shortcut can report them and keep serving.\n */\nexport async function updateWarlockPackages(\n options: UpdateWarlockPackagesOptions = {},\n): Promise<UpdateWarlockPackagesResult> {\n const runInstall = options.install ?? true;\n const packageJsonPath = rootPath(\"package.json\");\n\n if (!(await fileExistsAsync(packageJsonPath))) {\n console.log(`${colors.red(\"✖\")} No package.json found at the project root.`);\n return { outcome: \"no-package-json\", updates: [] };\n }\n\n const packageJson = (await getJsonFileAsync(packageJsonPath)) as PackageJson;\n const dependencies = collectWarlockDependencies(packageJson);\n\n if (dependencies.length === 0) {\n console.log(`${colors.yellow(\"⚠\")} No @warlock.js packages found in package.json.`);\n return { outcome: \"no-packages\", updates: [] };\n }\n\n console.log(\n `${colors.cyan(\"›\")} Checking ${colors.bold(String(dependencies.length))} ` +\n `@warlock.js package(s) for updates…`,\n );\n\n const resolved = await resolveLatestVersions(dependencies);\n\n // Distinguish \"nothing newer\" from \"we never got an answer\". Without this,\n // an offline run reports every package as up to date, which is a lie.\n if (isRegistryUnreachable(resolved)) {\n console.log(\n `${colors.yellow(\"⚠\")} Could not reach the npm registry — ` +\n `no versions were changed. Check your connection and try again.`,\n );\n return { outcome: \"registry-unreachable\", updates: [] };\n }\n\n const updates = resolvePackageUpdates(resolved);\n\n if (updates.length === 0) {\n console.log(`${colors.green(\"✓\")} All @warlock.js packages are already up to date.`);\n return { outcome: \"up-to-date\", updates: [] };\n }\n\n if (options.dryRun) {\n printUpdates(updates, { pending: true });\n return { outcome: \"outdated\", updates };\n }\n\n applyUpdates(packageJson, updates);\n await putJsonFileAsync(packageJsonPath, packageJson);\n\n printUpdates(updates);\n\n if (!runInstall) {\n console.log(\n colors.dim(\"Skipped install (--no-install). Run your package manager to apply the changes.\"),\n );\n return { outcome: \"updated\", updates };\n }\n\n try {\n await installDependencies();\n } catch (error) {\n console.log(\n `${colors.red(\"✖\")} The install failed. package.json is already updated — ` +\n `re-run your package manager install to finish.`,\n );\n return { outcome: \"install-failed\", updates, error: error as Error };\n }\n\n return { outcome: \"updated\", updates };\n}\n\n/**\n * Whether *every* lookup came back empty, which in practice means the\n * registry was never reached. A single failed package (unpublished, renamed,\n * private) leaves the rest resolvable and is not treated as an outage.\n */\nexport function isRegistryUnreachable(resolved: ResolvedDependency[]): boolean {\n return resolved.length > 0 && resolved.every((dependency) => !dependency.latest);\n}\n\n/** Collect every `@warlock.js/*` dependency across the relevant sections. */\nfunction collectWarlockDependencies(packageJson: PackageJson): WarlockDependency[] {\n const dependencies: WarlockDependency[] = [];\n\n for (const section of DEPENDENCY_SECTIONS) {\n const map = packageJson[section];\n\n if (!map) {\n continue;\n }\n\n for (const [name, current] of Object.entries(map)) {\n if (name.startsWith(WARLOCK_SCOPE)) {\n dependencies.push({ name, section, current });\n }\n }\n }\n\n return dependencies;\n}\n\n/** Resolve each dependency's latest version from the registry in parallel. */\nasync function resolveLatestVersions(\n dependencies: WarlockDependency[],\n): Promise<ResolvedDependency[]> {\n return Promise.all(\n dependencies.map(async (dependency) => ({\n ...dependency,\n latest: await fetchLatestVersion(dependency.name),\n })),\n );\n}\n\n/**\n * Turn resolved dependencies into the concrete set of version bumps to write.\n * Pure (no I/O) so it can be unit-tested directly: skips lookups that failed,\n * non-semver specs, and anything already at (or ahead of) the latest version,\n * and preserves the original range operator on everything it does rewrite.\n */\nexport function resolvePackageUpdates(resolved: ResolvedDependency[]): PackageUpdate[] {\n const updates: PackageUpdate[] = [];\n\n for (const dependency of resolved) {\n if (!dependency.latest) {\n continue;\n }\n\n const parsed = parseSpec(dependency.current);\n\n if (!parsed) {\n continue;\n }\n\n if (!isNewerVersion(dependency.latest, parsed.version)) {\n continue;\n }\n\n updates.push({\n name: dependency.name,\n section: dependency.section,\n from: dependency.current,\n to: `${parsed.operator}${dependency.latest}`,\n });\n }\n\n return updates;\n}\n\n/**\n * Split a dependency spec into its range operator and concrete version.\n * Returns `undefined` for specs we won't touch — `workspace:*`, `*`,\n * `latest`, multi-part ranges, or git/file URLs — leaving them exactly as\n * the author wrote them.\n */\nfunction parseSpec(spec: string): { operator: string; version: string } | undefined {\n const match = spec.trim().match(/^(\\^|~)?(\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?)$/);\n\n if (!match) {\n return undefined;\n }\n\n return { operator: match[1] ?? \"\", version: match[2] };\n}\n\n/** Write the resolved bumps back into the package.json object in place. */\nfunction applyUpdates(packageJson: PackageJson, updates: PackageUpdate[]): void {\n for (const update of updates) {\n const map = packageJson[update.section];\n\n if (map) {\n map[update.name] = update.to;\n }\n }\n}\n\n/** Print the version bumps — applied, or pending on a dry run. */\nfunction printUpdates(updates: PackageUpdate[], { pending = false } = {}): void {\n const headline = pending\n ? `${colors.yellow(\"↑\")} ${colors.bold(String(updates.length))} package(s) can be updated:`\n : `${colors.green(\"✓\")} Updated ${colors.bold(String(updates.length))} package(s):`;\n\n console.log(headline);\n\n for (const update of updates) {\n console.log(\n ` ${colors.cyan(update.name)} ` +\n `${colors.dim(update.from)} ${colors.dim(\"→\")} ${colors.greenBright(update.to)}`,\n );\n }\n}\n\n/** Reconcile node_modules to the rewritten package.json via the project PM. */\nasync function installDependencies(): Promise<void> {\n const packageManager = await detectPackageManager();\n const installCommand = getInstallCommand(packageManager);\n\n console.log(`${colors.cyan(\"›\")} Running ${colors.bold(installCommand)}…`);\n\n execSync(installCommand, { cwd: process.cwd(), stdio: \"inherit\" });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"update-warlock-packages.mjs","names":[],"sources":["../../../../../../../core/src/updater/update-warlock-packages.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, getJsonFileAsync, putJsonFileAsync } from \"@warlock.js/fs\";\nimport { execSync } from \"node:child_process\";\nimport { rootPath } from \"../utils\";\nimport { fetchLatestVersion } from \"../utils/npm-registry\";\nimport { isNewerVersion } from \"../utils/version-compare\";\nimport { detectPackageManager, getInstallCommand } from \"./package-manager\";\n\n/** Scope prefix that identifies a Warlock framework package. */\nconst WARLOCK_SCOPE = \"@warlock.js/\";\n\n/** The dependency maps in package.json we update, in display order. */\nconst DEPENDENCY_SECTIONS = [\"dependencies\", \"devDependencies\"] as const;\n\ntype DependencySection = (typeof DEPENDENCY_SECTIONS)[number];\n\ntype DependencyMap = Record<string, string>;\n\ntype PackageJson = Record<string, unknown> & {\n dependencies?: DependencyMap;\n devDependencies?: DependencyMap;\n};\n\n/** A `@warlock.js/*` dependency found in package.json. */\ntype WarlockDependency = {\n name: string;\n section: DependencySection;\n /** The current spec exactly as written, e.g. `^4.2.0`. */\n current: string;\n};\n\n/** A dependency whose latest version has been resolved from the registry. */\nexport type ResolvedDependency = WarlockDependency & {\n /** Latest version from npm, or `undefined` when the lookup failed. */\n latest: string | undefined;\n};\n\n/** A concrete version bump to write back into package.json. */\nexport type PackageUpdate = {\n name: string;\n section: DependencySection;\n /** Existing spec, e.g. `^4.2.0`. */\n from: string;\n /** New spec with the original range operator preserved, e.g. `^4.3.0`. */\n to: string;\n};\n\n/** Options accepted by {@link updateWarlockPackages}. */\nexport type UpdateWarlockPackagesOptions = {\n /** Run the package manager install after rewriting versions (default true). */\n install?: boolean;\n /**\n * Report what would change without touching package.json or node_modules.\n * Resolves with the `outdated` outcome instead of `updated`.\n */\n dryRun?: boolean;\n};\n\n/**\n * How an update run ended. Callers branch on this instead of re-deriving the\n * situation from the printed output — the dev-server `u` shortcut needs to\n * know whether restarting is warranted, and whether it may offer a retry.\n */\nexport type UpdateOutcome =\n /** No package.json at the project root — nothing to do. */\n | \"no-package-json\"\n /** package.json carries no `@warlock.js/*` dependency. */\n | \"no-packages\"\n /** Every registry lookup failed — offline, DNS down, or npm unreachable. */\n | \"registry-unreachable\"\n /** Every package is already at (or ahead of) its latest published version. */\n | \"up-to-date\"\n /** Newer versions exist, and a dry run left everything untouched. */\n | \"outdated\"\n /** Versions were rewritten (and installed, unless `install: false`). */\n | \"updated\"\n /** Versions were rewritten but the package manager install failed. */\n | \"install-failed\";\n\n/** The result of an update run. */\nexport type UpdateWarlockPackagesResult = {\n outcome: UpdateOutcome;\n /** The bumps written to package.json — empty unless `outcome` wrote any. */\n updates: PackageUpdate[];\n /** The install failure, present only for `install-failed`. */\n error?: Error;\n};\n\n/**\n * Update every `@warlock.js/*` package listed in the project's root\n * package.json to its latest published version, then reconcile `node_modules`\n * via the detected package manager.\n *\n * The original range operator on each dependency (`^`, `~`, or an exact pin)\n * is preserved; specs that aren't a plain semver — `workspace:*`, `*`,\n * `latest`, git/file URLs — are left untouched. Only genuine upgrades are\n * written, so re-running on an already-current project is a no-op.\n *\n * Resolves rather than throws on the two failures a caller is expected to\n * survive — an unreachable registry and a failed install — so the dev-server\n * shortcut can report them and keep serving.\n */\nexport async function updateWarlockPackages(\n options: UpdateWarlockPackagesOptions = {},\n): Promise<UpdateWarlockPackagesResult> {\n const runInstall = options.install ?? true;\n const packageJsonPath = rootPath(\"package.json\");\n\n if (!(await fileExistsAsync(packageJsonPath))) {\n console.log(`${colors.red(\"✖\")} No package.json found at the project root.`);\n return { outcome: \"no-package-json\", updates: [] };\n }\n\n const packageJson = (await getJsonFileAsync(packageJsonPath)) as PackageJson;\n const dependencies = collectWarlockDependencies(packageJson);\n\n if (dependencies.length === 0) {\n console.log(`${colors.yellow(\"⚠\")} No @warlock.js packages found in package.json.`);\n return { outcome: \"no-packages\", updates: [] };\n }\n\n console.log(\n `${colors.cyan(\"›\")} Checking ${colors.bold(String(dependencies.length))} ` +\n `@warlock.js package(s) for updates…`,\n );\n\n const resolved = await resolveLatestVersions(dependencies);\n\n // Distinguish \"nothing newer\" from \"we never got an answer\". Without this,\n // an offline run reports every package as up to date, which is a lie.\n if (isRegistryUnreachable(resolved)) {\n console.log(\n `${colors.yellow(\"⚠\")} Could not reach the npm registry — ` +\n `no versions were changed. Check your connection and try again.`,\n );\n return { outcome: \"registry-unreachable\", updates: [] };\n }\n\n // A partial failure would bump some family members and leave others behind,\n // a lockstep drift reported as success. Refuse the whole run instead.\n const failedLookups = resolved.filter((dependency) => !dependency.latest);\n\n if (failedLookups.length > 0) {\n console.log(\n `${colors.yellow(\"⚠\")} Could not look up ${failedLookups.map((dependency) => dependency.name).join(\", \")} — ` +\n `no versions were changed, to keep @warlock.js packages in lockstep. Try again.`,\n );\n return { outcome: \"registry-unreachable\", updates: [] };\n }\n\n const updates = resolvePackageUpdates(resolved);\n\n if (updates.length === 0) {\n console.log(`${colors.green(\"✓\")} All @warlock.js packages are already up to date.`);\n return { outcome: \"up-to-date\", updates: [] };\n }\n\n if (options.dryRun) {\n printUpdates(updates, { pending: true });\n return { outcome: \"outdated\", updates };\n }\n\n applyUpdates(packageJson, updates);\n await putJsonFileAsync(packageJsonPath, packageJson);\n\n printUpdates(updates);\n\n if (!runInstall) {\n console.log(\n colors.dim(\"Skipped install (--no-install). Run your package manager to apply the changes.\"),\n );\n return { outcome: \"updated\", updates };\n }\n\n try {\n await installDependencies();\n } catch (error) {\n console.log(\n `${colors.red(\"✖\")} The install failed. package.json is already updated — ` +\n `re-run your package manager install to finish.`,\n );\n return { outcome: \"install-failed\", updates, error: error as Error };\n }\n\n return { outcome: \"updated\", updates };\n}\n\n/**\n * Whether *every* lookup came back empty, which in practice means the\n * registry was never reached. A single failed package (unpublished, renamed,\n * private) leaves the rest resolvable and is not treated as an outage.\n */\nexport function isRegistryUnreachable(resolved: ResolvedDependency[]): boolean {\n return resolved.length > 0 && resolved.every((dependency) => !dependency.latest);\n}\n\n/** Collect every `@warlock.js/*` dependency across the relevant sections. */\nfunction collectWarlockDependencies(packageJson: PackageJson): WarlockDependency[] {\n const dependencies: WarlockDependency[] = [];\n\n for (const section of DEPENDENCY_SECTIONS) {\n const map = packageJson[section];\n\n if (!map) {\n continue;\n }\n\n for (const [name, current] of Object.entries(map)) {\n if (name.startsWith(WARLOCK_SCOPE)) {\n dependencies.push({ name, section, current });\n }\n }\n }\n\n return dependencies;\n}\n\n/** Resolve each dependency's latest version from the registry in parallel. */\nasync function resolveLatestVersions(\n dependencies: WarlockDependency[],\n): Promise<ResolvedDependency[]> {\n return Promise.all(\n dependencies.map(async (dependency) => ({\n ...dependency,\n latest: await fetchLatestVersion(dependency.name),\n })),\n );\n}\n\n/**\n * Turn resolved dependencies into the concrete set of version bumps to write.\n * Pure (no I/O) so it can be unit-tested directly: skips lookups that failed,\n * non-semver specs, and anything already at (or ahead of) the latest version,\n * and preserves the original range operator on everything it does rewrite.\n */\nexport function resolvePackageUpdates(resolved: ResolvedDependency[]): PackageUpdate[] {\n const updates: PackageUpdate[] = [];\n\n for (const dependency of resolved) {\n if (!dependency.latest) {\n continue;\n }\n\n const parsed = parseSpec(dependency.current);\n\n if (!parsed) {\n continue;\n }\n\n if (!isNewerVersion(dependency.latest, parsed.version)) {\n continue;\n }\n\n updates.push({\n name: dependency.name,\n section: dependency.section,\n from: dependency.current,\n to: `${parsed.operator}${dependency.latest}`,\n });\n }\n\n return updates;\n}\n\n/**\n * Split a dependency spec into its range operator and concrete version.\n * Returns `undefined` for specs we won't touch — `workspace:*`, `*`,\n * `latest`, multi-part ranges, or git/file URLs — leaving them exactly as\n * the author wrote them.\n */\nfunction parseSpec(spec: string): { operator: string; version: string } | undefined {\n const match = spec.trim().match(/^(\\^|~)?(\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?)$/);\n\n if (!match) {\n return undefined;\n }\n\n const version = match[2];\n\n if (version === undefined) {\n return undefined;\n }\n\n return { operator: match[1] ?? \"\", version };\n}\n\n/** Write the resolved bumps back into the package.json object in place. */\nfunction applyUpdates(packageJson: PackageJson, updates: PackageUpdate[]): void {\n for (const update of updates) {\n const map = packageJson[update.section];\n\n if (map) {\n map[update.name] = update.to;\n }\n }\n}\n\n/** Print the version bumps — applied, or pending on a dry run. */\nfunction printUpdates(updates: PackageUpdate[], { pending = false } = {}): void {\n const headline = pending\n ? `${colors.yellow(\"↑\")} ${colors.bold(String(updates.length))} package(s) can be updated:`\n : `${colors.green(\"✓\")} Updated ${colors.bold(String(updates.length))} package(s):`;\n\n console.log(headline);\n\n for (const update of updates) {\n console.log(\n ` ${colors.cyan(update.name)} ` +\n `${colors.dim(update.from)} ${colors.dim(\"→\")} ${colors.greenBright(update.to)}`,\n );\n }\n}\n\n/** Reconcile node_modules to the rewritten package.json via the project PM. */\nasync function installDependencies(): Promise<void> {\n const packageManager = await detectPackageManager();\n const installCommand = getInstallCommand(packageManager);\n\n console.log(`${colors.cyan(\"›\")} Running ${colors.bold(installCommand)}…`);\n\n execSync(installCommand, { cwd: process.cwd(), stdio: \"inherit\" });\n}\n"],"mappings":"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"}
|
|
@@ -19,7 +19,13 @@ async function broadcastUseCaseResult(params) {
|
|
|
19
19
|
if (!channels || channels.length === 0) return;
|
|
20
20
|
const projector = typeof broadcast === "object" ? broadcast.output : void 0;
|
|
21
21
|
const event = typeof broadcast === "object" && broadcast.event ? broadcast.event : name;
|
|
22
|
-
|
|
22
|
+
let payload = output;
|
|
23
|
+
if (projector) try {
|
|
24
|
+
payload = projector(output, result);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
log.error("use-cases", name, "broadcast projector failed", { error });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
23
29
|
const broadcastEvent = {
|
|
24
30
|
useCase: name,
|
|
25
31
|
event,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-case-broadcast.mjs","names":[],"sources":["../../../../../../../core/src/use-cases/use-case-broadcast.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\nimport type {\n UseCaseBroadcastEvent,\n UseCaseBroadcastOption,\n UseCaseConfigurations,\n UseCaseResult,\n} from \"./types\";\n\n/**\n * Broadcast a successful use-case result through the globally-configured channels.\n *\n * Fires only when the use case opted in (`broadcast`) and the global transport is\n * enabled with at least one channel. Fan-out is isolated via `Promise.allSettled` —\n * a failing channel is logged and never affects the use case or sibling channels.\n *\n * @example\n * await broadcastUseCaseResult({ name, id, output, result, broadcast, config });\n */\nexport async function broadcastUseCaseResult<Output>(params: {\n name: string;\n id: string;\n output: Output;\n result: UseCaseResult<Output>;\n broadcast?: UseCaseBroadcastOption<Output>;\n config?: UseCaseConfigurations[\"broadcast\"];\n}): Promise<void> {\n const { name, id, output, result, broadcast, config } = params;\n\n if (!broadcast) {\n return;\n }\n\n if (config?.enabled === false) {\n return;\n }\n\n const channels = config?.channels;\n\n if (!channels || channels.length === 0) {\n return;\n }\n\n const projector = typeof broadcast === \"object\" ? broadcast.output : undefined;\n const event = typeof broadcast === \"object\" && broadcast.event ? broadcast.event : name;\n
|
|
1
|
+
{"version":3,"file":"use-case-broadcast.mjs","names":[],"sources":["../../../../../../../core/src/use-cases/use-case-broadcast.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\nimport type {\n UseCaseBroadcastEvent,\n UseCaseBroadcastOption,\n UseCaseConfigurations,\n UseCaseResult,\n} from \"./types\";\n\n/**\n * Broadcast a successful use-case result through the globally-configured channels.\n *\n * Fires only when the use case opted in (`broadcast`) and the global transport is\n * enabled with at least one channel. Fan-out is isolated via `Promise.allSettled` —\n * a failing channel is logged and never affects the use case or sibling channels.\n *\n * @example\n * await broadcastUseCaseResult({ name, id, output, result, broadcast, config });\n */\nexport async function broadcastUseCaseResult<Output>(params: {\n name: string;\n id: string;\n output: Output;\n result: UseCaseResult<Output>;\n broadcast?: UseCaseBroadcastOption<Output>;\n config?: UseCaseConfigurations[\"broadcast\"];\n}): Promise<void> {\n const { name, id, output, result, broadcast, config } = params;\n\n if (!broadcast) {\n return;\n }\n\n if (config?.enabled === false) {\n return;\n }\n\n const channels = config?.channels;\n\n if (!channels || channels.length === 0) {\n return;\n }\n\n const projector = typeof broadcast === \"object\" ? broadcast.output : undefined;\n const event = typeof broadcast === \"object\" && broadcast.event ? broadcast.event : name;\n let payload: unknown = output;\n\n if (projector) {\n try {\n payload = projector(output, result);\n } catch (error) {\n log.error(\"use-cases\", name, \"broadcast projector failed\", { error });\n\n return;\n }\n }\n\n const broadcastEvent: UseCaseBroadcastEvent = {\n useCase: name,\n event,\n id,\n at: new Date(),\n payload,\n };\n\n const outcomes = await Promise.allSettled(\n channels.map((channel) => channel.broadcast(broadcastEvent)),\n );\n\n for (const outcome of outcomes) {\n if (outcome.status === \"rejected\") {\n log.error(\"use-cases\", name, \"broadcast channel failed\", { error: outcome.reason });\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAkBA,eAAsB,uBAA+B,QAOnC;CAChB,MAAM,EAAE,MAAM,IAAI,QAAQ,QAAQ,WAAW,WAAW;CAExD,IAAI,CAAC,WACH;CAGF,IAAI,QAAQ,YAAY,OACtB;CAGF,MAAM,WAAW,QAAQ;CAEzB,IAAI,CAAC,YAAY,SAAS,WAAW,GACnC;CAGF,MAAM,YAAY,OAAO,cAAc,WAAW,UAAU,SAAS;CACrE,MAAM,QAAQ,OAAO,cAAc,YAAY,UAAU,QAAQ,UAAU,QAAQ;CACnF,IAAI,UAAmB;CAEvB,IAAI,WACF,IAAI;EACF,UAAU,UAAU,QAAQ,MAAM;CACpC,SAAS,OAAO;EACd,IAAI,MAAM,aAAa,MAAM,8BAA8B,EAAE,MAAM,CAAC;EAEpE;CACF;CAGF,MAAM,iBAAwC;EAC5C,SAAS;EACT;EACA;EACA,oBAAI,IAAI,KAAK;EACb;CACF;CAEA,MAAM,WAAW,MAAM,QAAQ,WAC7B,SAAS,KAAK,YAAY,QAAQ,UAAU,cAAc,CAAC,CAC7D;CAEA,KAAK,MAAM,WAAW,UACpB,IAAI,QAAQ,WAAW,YACrB,IAAI,MAAM,aAAa,MAAM,4BAA4B,EAAE,OAAO,QAAQ,OAAO,CAAC;AAGxF"}
|
|
@@ -99,13 +99,32 @@ async function getUseCaseHistory(name) {
|
|
|
99
99
|
* unbounded — oldest entries (and their ids) are evicted past MAX_HISTORY_ENTRIES.
|
|
100
100
|
*/
|
|
101
101
|
async function addUseCaseHistory(name, result) {
|
|
102
|
+
if (!(config.get("use-cases")?.history?.enabled ?? process.env.NODE_ENV !== "production")) return;
|
|
103
|
+
const current = (historyWrites.get(name) ?? Promise.resolve()).catch(() => void 0).then(() => writeHistory(name, result));
|
|
104
|
+
historyWrites.set(name, current);
|
|
105
|
+
try {
|
|
106
|
+
await current;
|
|
107
|
+
} finally {
|
|
108
|
+
if (historyWrites.get(name) === current) historyWrites.delete(name);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Per-use-case write chain so the list read-modify-write never interleaves
|
|
113
|
+
* within a process (keeps the cap enforced under concurrency).
|
|
114
|
+
*/
|
|
115
|
+
const historyWrites = /* @__PURE__ */ new Map();
|
|
116
|
+
async function writeHistory(name, result) {
|
|
102
117
|
const useCaseConfig = config.get("use-cases");
|
|
103
|
-
if (useCaseConfig?.history?.enabled === false) return;
|
|
104
118
|
const ttl = resolveHistoryTtl(useCaseConfig);
|
|
105
119
|
const maxEntries = useCaseConfig?.history?.maxEntries ?? 100;
|
|
106
120
|
const key = `use-case:history:${name}:${result.id}`;
|
|
107
121
|
const listKey = `use-case:history:${name}:list`;
|
|
108
|
-
|
|
122
|
+
const projection = {
|
|
123
|
+
...result,
|
|
124
|
+
output: void 0,
|
|
125
|
+
ctx: void 0
|
|
126
|
+
};
|
|
127
|
+
await cache.set(key, projection, ttl);
|
|
109
128
|
const list = await cache.get(listKey) || [];
|
|
110
129
|
list.push(result.id);
|
|
111
130
|
while (list.length > maxEntries) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-cases-registry.mjs","names":[],"sources":["../../../../../../../core/src/use-cases/use-cases-registry.ts"],"sourcesContent":["import { cache } from \"@warlock.js/cache\";\nimport { log } from \"@warlock.js/logger\";\nimport { config } from \"../config\";\nimport { type RegisteredUseCase, type UseCaseConfigurations, type UseCaseResult } from \"./types\";\n\n/**\n * Store registered use cases\n */\nconst useCaseRegister = new Map<string, RegisteredUseCase>();\n\n/**\n * Register a use case\n */\nexport function $registerUseCase<Output, Input>(\n name: string,\n useCase: RegisteredUseCase<Output, Input>,\n) {\n if (useCaseRegister.has(name) && process.env.NODE_ENV !== \"production\") {\n log.warn(\"use-cases\", \"registering\", `Use case \"${name}\" is already registered. Overwriting.`);\n }\n\n useCaseRegister.set(name, useCase);\n}\n\n/**\n * Unregister a use case.\n *\n * Dropping the history namespace is best-effort: `$cleanup` is synchronous, so the\n * async cache removal is fire-and-forget with a `.catch` guard — a rejecting cache\n * driver must never turn cleanup into an unhandled rejection.\n */\nexport function $unregisterUseCase(name: string) {\n useCaseRegister.delete(name);\n\n Promise.resolve(cache.removeNamespace(`use-case:history:${name}`)).catch((error) => {\n log.error(\"use-cases\", name, \"history namespace removal failed\", { error });\n });\n}\n\n/**\n * Get a use case.\n *\n * Returns the `RegisteredUseCase` shape — including the runtime `calls` counters\n * the registry actually stores — so consumers can read call stats without an\n * ad-hoc cast.\n */\nexport function getUseCase<Output, Input>(name: string) {\n return useCaseRegister.get(name) as RegisteredUseCase<Output, Input> | undefined;\n}\n\n/**\n * Get all use cases\n */\nexport function getUseCases() {\n return useCaseRegister;\n}\n\n/**\n * Increase use case success calls\n */\nexport function increaseUseCaseSuccessCalls(name: string): number {\n const useCase = useCaseRegister.get(name);\n if (useCase) {\n useCase.calls.success++;\n useCase.calls.total++;\n\n return useCase.calls.success;\n }\n\n return 0;\n}\n\n/**\n * Increase use case failed calls\n */\nexport function increaseUseCaseFailedCalls(name: string): number {\n const useCase = useCaseRegister.get(name);\n if (useCase) {\n useCase.calls.failed++;\n useCase.calls.total++;\n\n return useCase.calls.failed;\n }\n\n return 0;\n}\n\n/**\n * Resolve the history cache TTL (seconds) from config.\n * `false` means \"use the cache driver's default\", represented as `undefined`.\n */\nfunction resolveHistoryTtl(useCaseConfig?: UseCaseConfigurations): number | undefined {\n const ttlConfig = useCaseConfig?.history?.ttl ?? 3600; // 1 hour default\n\n return ttlConfig === false ? undefined : ttlConfig;\n}\n\n/**\n * Get use case history from cache.\n *\n * Prunes ids whose entries have expired so the list can't accumulate dead refs.\n */\nexport async function getUseCaseHistory(name: string): Promise<UseCaseResult<any>[]> {\n const listKey = `use-case:history:${name}:list`;\n const ids = (await cache.get<string[]>(listKey)) || [];\n\n const entries = await Promise.all(\n ids.map(async (id: string) => ({\n id,\n result: await cache.get<UseCaseResult<any>>(`use-case:history:${name}:${id}`),\n })),\n );\n\n const liveEntries = entries.filter((entry) => Boolean(entry.result));\n\n if (liveEntries.length !== ids.length) {\n const useCaseConfig = config.get<UseCaseConfigurations>(\"use-cases\");\n\n await cache.set(\n listKey,\n liveEntries.map((entry) => entry.id),\n resolveHistoryTtl(useCaseConfig),\n );\n }\n\n return liveEntries.map((entry) => entry.result) as UseCaseResult<any>[];\n}\n\n/**\n * Add use case history to cache, capping the per-use-case list so it can't grow\n * unbounded — oldest entries (and their ids) are evicted past MAX_HISTORY_ENTRIES.\n */\nexport async function addUseCaseHistory(name: string, result: UseCaseResult<any>) {\n const useCaseConfig = config.get<UseCaseConfigurations>(\"use-cases\");\n\n
|
|
1
|
+
{"version":3,"file":"use-cases-registry.mjs","names":[],"sources":["../../../../../../../core/src/use-cases/use-cases-registry.ts"],"sourcesContent":["import { cache } from \"@warlock.js/cache\";\nimport { log } from \"@warlock.js/logger\";\nimport { config } from \"../config\";\nimport { type RegisteredUseCase, type UseCaseConfigurations, type UseCaseResult } from \"./types\";\n\n/**\n * Store registered use cases\n */\nconst useCaseRegister = new Map<string, RegisteredUseCase>();\n\n/**\n * Register a use case\n */\nexport function $registerUseCase<Output, Input>(\n name: string,\n useCase: RegisteredUseCase<Output, Input>,\n) {\n if (useCaseRegister.has(name) && process.env.NODE_ENV !== \"production\") {\n log.warn(\"use-cases\", \"registering\", `Use case \"${name}\" is already registered. Overwriting.`);\n }\n\n useCaseRegister.set(name, useCase);\n}\n\n/**\n * Unregister a use case.\n *\n * Dropping the history namespace is best-effort: `$cleanup` is synchronous, so the\n * async cache removal is fire-and-forget with a `.catch` guard — a rejecting cache\n * driver must never turn cleanup into an unhandled rejection.\n */\nexport function $unregisterUseCase(name: string) {\n useCaseRegister.delete(name);\n\n Promise.resolve(cache.removeNamespace(`use-case:history:${name}`)).catch((error) => {\n log.error(\"use-cases\", name, \"history namespace removal failed\", { error });\n });\n}\n\n/**\n * Get a use case.\n *\n * Returns the `RegisteredUseCase` shape — including the runtime `calls` counters\n * the registry actually stores — so consumers can read call stats without an\n * ad-hoc cast.\n */\nexport function getUseCase<Output, Input>(name: string) {\n return useCaseRegister.get(name) as RegisteredUseCase<Output, Input> | undefined;\n}\n\n/**\n * Get all use cases\n */\nexport function getUseCases() {\n return useCaseRegister;\n}\n\n/**\n * Increase use case success calls\n */\nexport function increaseUseCaseSuccessCalls(name: string): number {\n const useCase = useCaseRegister.get(name);\n if (useCase) {\n useCase.calls.success++;\n useCase.calls.total++;\n\n return useCase.calls.success;\n }\n\n return 0;\n}\n\n/**\n * Increase use case failed calls\n */\nexport function increaseUseCaseFailedCalls(name: string): number {\n const useCase = useCaseRegister.get(name);\n if (useCase) {\n useCase.calls.failed++;\n useCase.calls.total++;\n\n return useCase.calls.failed;\n }\n\n return 0;\n}\n\n/**\n * Resolve the history cache TTL (seconds) from config.\n * `false` means \"use the cache driver's default\", represented as `undefined`.\n */\nfunction resolveHistoryTtl(useCaseConfig?: UseCaseConfigurations): number | undefined {\n const ttlConfig = useCaseConfig?.history?.ttl ?? 3600; // 1 hour default\n\n return ttlConfig === false ? undefined : ttlConfig;\n}\n\n/**\n * Get use case history from cache.\n *\n * Prunes ids whose entries have expired so the list can't accumulate dead refs.\n */\nexport async function getUseCaseHistory(name: string): Promise<UseCaseResult<any>[]> {\n const listKey = `use-case:history:${name}:list`;\n const ids = (await cache.get<string[]>(listKey)) || [];\n\n const entries = await Promise.all(\n ids.map(async (id: string) => ({\n id,\n result: await cache.get<UseCaseResult<any>>(`use-case:history:${name}:${id}`),\n })),\n );\n\n const liveEntries = entries.filter((entry) => Boolean(entry.result));\n\n if (liveEntries.length !== ids.length) {\n const useCaseConfig = config.get<UseCaseConfigurations>(\"use-cases\");\n\n await cache.set(\n listKey,\n liveEntries.map((entry) => entry.id),\n resolveHistoryTtl(useCaseConfig),\n );\n }\n\n return liveEntries.map((entry) => entry.result) as UseCaseResult<any>[];\n}\n\n/**\n * Add use case history to cache, capping the per-use-case list so it can't grow\n * unbounded — oldest entries (and their ids) are evicted past MAX_HISTORY_ENTRIES.\n */\nexport async function addUseCaseHistory(name: string, result: UseCaseResult<any>) {\n const useCaseConfig = config.get<UseCaseConfigurations>(\"use-cases\");\n\n // On by default outside production; opt in to production with `history.enabled: true`.\n const enabled = useCaseConfig?.history?.enabled ?? process.env.NODE_ENV !== \"production\";\n\n if (!enabled) return;\n\n const previous = historyWrites.get(name) ?? Promise.resolve();\n const current = previous.catch(() => undefined).then(() => writeHistory(name, result));\n\n historyWrites.set(name, current);\n\n try {\n await current;\n } finally {\n if (historyWrites.get(name) === current) {\n historyWrites.delete(name);\n }\n }\n}\n\n/**\n * Per-use-case write chain so the list read-modify-write never interleaves\n * within a process (keeps the cap enforced under concurrency).\n */\nconst historyWrites = new Map<string, Promise<unknown>>();\n\nasync function writeHistory(name: string, result: UseCaseResult<any>) {\n const useCaseConfig = config.get<UseCaseConfigurations>(\"use-cases\");\n const ttl = resolveHistoryTtl(useCaseConfig);\n const maxEntries = useCaseConfig?.history?.maxEntries ?? 100;\n const key = `use-case:history:${name}:${result.id}`;\n const listKey = `use-case:history:${name}:list`;\n\n // Store a projection only: `output` and `ctx` (tokens, payloads) never reach the cache.\n const projection = { ...result, output: undefined, ctx: undefined } as unknown as UseCaseResult<any>;\n\n await cache.set(key, projection, ttl);\n\n const list = (await cache.get<string[]>(listKey)) || [];\n\n list.push(result.id);\n\n while (list.length > maxEntries) {\n const evictedId = list.shift();\n\n if (evictedId) {\n await cache.remove(`use-case:history:${name}:${evictedId}`);\n }\n }\n\n await cache.set(listKey, list, ttl);\n}\n"],"mappings":"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"}
|
|
@@ -31,9 +31,6 @@ var DatabaseLog = class extends LogChannel {
|
|
|
31
31
|
if (message instanceof Error) {
|
|
32
32
|
data.stack = message.stack;
|
|
33
33
|
data.content = message.message;
|
|
34
|
-
} else {
|
|
35
|
-
data.content = message;
|
|
36
|
-
data.stack = (/* @__PURE__ */ new Error()).stack;
|
|
37
34
|
}
|
|
38
35
|
try {
|
|
39
36
|
await this.model.create(data);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-log.mjs","names":[],"sources":["../../../../../../../core/src/utils/database-log.ts"],"sourcesContent":["import type { ChildModel, Model } from \"@warlock.js/cascade\";\nimport type { BasicLogConfigurations, LoggingData } from \"@warlock.js/logger\";\nimport { LogChannel, type LogContract, type LogMessage } from \"@warlock.js/logger\";\nimport { DatabaseLogModel } from \"../database/models/database-log\";\n\nexport type DatabaseLogOptions = BasicLogConfigurations & {\n /**\n * Model to use for logging\n */\n model?: typeof DatabaseLogModel;\n};\n\nexport class DatabaseLog extends LogChannel<DatabaseLogOptions> implements LogContract {\n /**\n * {@inheritdoc}\n */\n public name = \"database\";\n\n /**\n * Database model\n */\n public get model(): ChildModel<Model> {\n return this.config(\"model\") ?? DatabaseLogModel;\n }\n\n /**\n * {@inheritdoc}\n */\n public async log(log: LoggingData) {\n const { module, action, message, type: level } = log;\n\n if (!this.model.getDataSource().driver?.isConnected) return;\n\n if (!this.shouldBeLogged(log)) return;\n\n const data: LogMessage = {\n module,\n action,\n content: message,\n level,\n date: new Date().toISOString(),\n };\n\n if (message instanceof Error) {\n data.stack = message.stack;\n data.content = message.message;\n }
|
|
1
|
+
{"version":3,"file":"database-log.mjs","names":[],"sources":["../../../../../../../core/src/utils/database-log.ts"],"sourcesContent":["import type { ChildModel, Model } from \"@warlock.js/cascade\";\nimport type { BasicLogConfigurations, LoggingData } from \"@warlock.js/logger\";\nimport { LogChannel, type LogContract, type LogMessage } from \"@warlock.js/logger\";\nimport { DatabaseLogModel } from \"../database/models/database-log\";\n\nexport type DatabaseLogOptions = BasicLogConfigurations & {\n /**\n * Model to use for logging\n */\n model?: typeof DatabaseLogModel;\n};\n\nexport class DatabaseLog extends LogChannel<DatabaseLogOptions> implements LogContract {\n /**\n * {@inheritdoc}\n */\n public name = \"database\";\n\n /**\n * Database model\n */\n public get model(): ChildModel<Model> {\n return this.config(\"model\") ?? DatabaseLogModel;\n }\n\n /**\n * {@inheritdoc}\n */\n public async log(log: LoggingData) {\n const { module, action, message, type: level } = log;\n\n if (!this.model.getDataSource().driver?.isConnected) return;\n\n if (!this.shouldBeLogged(log)) return;\n\n const data: LogMessage = {\n module,\n action,\n content: message,\n level,\n date: new Date().toISOString(),\n };\n\n if (message instanceof Error) {\n data.stack = message.stack;\n data.content = message.message;\n }\n\n try {\n await this.model.create(data);\n } catch (error) {\n console.log(\"Error\", error);\n }\n }\n}\n"],"mappings":";;;;;AAYA,IAAa,cAAb,cAAiC,WAAsD;;;cAIvE;;;;;CAKd,IAAW,QAA2B;EACpC,OAAO,KAAK,OAAO,OAAO,KAAK;CACjC;;;;CAKA,MAAa,IAAI,KAAkB;EACjC,MAAM,EAAE,QAAQ,QAAQ,SAAS,MAAM,UAAU;EAEjD,IAAI,CAAC,KAAK,MAAM,cAAc,CAAC,CAAC,QAAQ,aAAa;EAErD,IAAI,CAAC,KAAK,eAAe,GAAG,GAAG;EAE/B,MAAM,OAAmB;GACvB;GACA;GACA,SAAS;GACT;GACA,uBAAM,IAAI,KAAK,EAAC,CAAC,YAAY;EAC/B;EAEA,IAAI,mBAAmB,OAAO;GAC5B,KAAK,QAAQ,QAAQ;GACrB,KAAK,UAAU,QAAQ;EACzB;EAEA,IAAI;GACF,MAAM,KAAK,MAAM,OAAO,IAAI;EAC9B,SAAS,OAAO;GACd,QAAQ,IAAI,SAAS,KAAK;EAC5B;CACF;AACF"}
|
package/esm/utils/queue.d.mts
CHANGED
|
@@ -18,7 +18,9 @@ declare class Queue<T> {
|
|
|
18
18
|
/** Flag to determine if execution should be parallel or sequential. */
|
|
19
19
|
private readonly executeInParallel;
|
|
20
20
|
/** The batch size for processing items. */
|
|
21
|
-
private readonly batchSize
|
|
21
|
+
private readonly batchSize?;
|
|
22
|
+
/** Called when the execute function rejects. */
|
|
23
|
+
private readonly onError;
|
|
22
24
|
/** Whether the current queue is busy executing */
|
|
23
25
|
private isExecuting;
|
|
24
26
|
/**
|
|
@@ -29,7 +31,7 @@ declare class Queue<T> {
|
|
|
29
31
|
* @param executeInParallel - Whether to execute the function in parallel or sequentially.
|
|
30
32
|
* @param batchSize - The number of items to process in each batch.
|
|
31
33
|
*/
|
|
32
|
-
constructor(executeFn: (items: T[]) => Promise<void>, executeInParallel
|
|
34
|
+
constructor(executeFn: (items: T[]) => Promise<void>, executeInParallel?: boolean, executeEvery?: number, batchSize?: number, maxSize?: number, onError?: (error: unknown) => void);
|
|
33
35
|
/**
|
|
34
36
|
* Adds an item to the queue.
|
|
35
37
|
* Triggers execution if the queue reaches the maximum size.
|
|
@@ -42,8 +44,8 @@ declare class Queue<T> {
|
|
|
42
44
|
*/
|
|
43
45
|
private startTimer;
|
|
44
46
|
/**
|
|
45
|
-
* Executes the function with the
|
|
46
|
-
*
|
|
47
|
+
* Executes the function with the queued items until the queue is drained.
|
|
48
|
+
* Rejections from the execute function are passed to `onError`.
|
|
47
49
|
*/
|
|
48
50
|
private execute;
|
|
49
51
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queue.d.mts","names":[],"sources":["../../../../../../../core/src/utils/queue.ts"],"mappings":";;AAKA;;;;cAAa,KAAA;
|
|
1
|
+
{"version":3,"file":"queue.d.mts","names":[],"sources":["../../../../../../../core/src/utils/queue.ts"],"mappings":";;AAKA;;;;cAAa,KAAA;EA0DU;EAAA,QAxDb,KAAA;EAwDc;EAAA,iBArDL,OAAA;EAHT;EAAA,iBAMS,QAAA;EAAA;EAAA,iBAGA,SAAA;EAGT;EAAA,QAAA,KAAA;EAMS;EAAA,iBAHA,iBAAA;EAST;EAAA,iBANS,SAAA;EAiBI;EAAA,iBAdJ,OAAA;EAcY;EAAA,QAXrB,WAAA;EAYN;;;;;;;;cADA,SAAA,GAAY,KAAA,EAAO,CAAA,OAAQ,OAAA,QAC3B,iBAAA,YACA,YAAA,WACA,SAAA,WACA,OAAA,WACA,OAAA,IAAU,KAAA;EA8BJ;;;AAaa;;;EA3Bd,OAAA,CAAQ,IAAA,EAAM,CAAA;;;;UAcb,UAAA;;;;;UAaM,OAAA;AAAA"}
|
package/esm/utils/queue.mjs
CHANGED
|
@@ -13,10 +13,11 @@ var Queue = class {
|
|
|
13
13
|
* @param executeInParallel - Whether to execute the function in parallel or sequentially.
|
|
14
14
|
* @param batchSize - The number of items to process in each batch.
|
|
15
15
|
*/
|
|
16
|
-
constructor(executeFn, executeInParallel = true, executeEvery = 5e3, batchSize, maxSize) {
|
|
16
|
+
constructor(executeFn, executeInParallel = true, executeEvery = 5e3, batchSize, maxSize, onError = (error) => console.error("Queue execution failed", error)) {
|
|
17
17
|
this.items = [];
|
|
18
18
|
this.timer = null;
|
|
19
19
|
this.isExecuting = false;
|
|
20
|
+
this.onError = onError;
|
|
20
21
|
this.executeFn = executeFn;
|
|
21
22
|
this.maxSize = maxSize;
|
|
22
23
|
this.interval = executeEvery;
|
|
@@ -32,7 +33,7 @@ var Queue = class {
|
|
|
32
33
|
enqueue(item) {
|
|
33
34
|
this.items.push(item);
|
|
34
35
|
if (this.maxSize && this.items.length >= this.maxSize) this.execute();
|
|
35
|
-
if (!this.timer) this.startTimer();
|
|
36
|
+
if (!this.timer && !this.isExecuting) this.startTimer();
|
|
36
37
|
}
|
|
37
38
|
/**
|
|
38
39
|
* Starts a timer to execute the queue after the specified interval.
|
|
@@ -41,23 +42,33 @@ var Queue = class {
|
|
|
41
42
|
this.timer = setInterval(() => {
|
|
42
43
|
if (this.items.length > 0) this.execute();
|
|
43
44
|
}, this.interval);
|
|
45
|
+
this.timer.unref?.();
|
|
44
46
|
}
|
|
45
47
|
/**
|
|
46
|
-
* Executes the function with the
|
|
47
|
-
*
|
|
48
|
+
* Executes the function with the queued items until the queue is drained.
|
|
49
|
+
* Rejections from the execute function are passed to `onError`.
|
|
48
50
|
*/
|
|
49
51
|
async execute() {
|
|
50
52
|
if (this.timer) {
|
|
51
53
|
clearInterval(this.timer);
|
|
52
54
|
this.timer = null;
|
|
53
55
|
}
|
|
56
|
+
if (this.isExecuting) return;
|
|
54
57
|
this.isExecuting = true;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
try {
|
|
59
|
+
while (this.items.length > 0) {
|
|
60
|
+
const batch = this.items.splice(0, this.batchSize || this.items.length);
|
|
61
|
+
try {
|
|
62
|
+
if (!this.batchSize) await this.executeFn(batch);
|
|
63
|
+
else if (this.executeInParallel) await Promise.all(batch.map((item) => this.executeFn([item])));
|
|
64
|
+
else for (const item of batch) await this.executeFn([item]);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
this.onError(error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} finally {
|
|
70
|
+
this.isExecuting = false;
|
|
59
71
|
}
|
|
60
|
-
this.isExecuting = false;
|
|
61
72
|
}
|
|
62
73
|
};
|
|
63
74
|
|
package/esm/utils/queue.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queue.mjs","names":[],"sources":["../../../../../../../core/src/utils/queue.ts"],"sourcesContent":["/**\n * A utility class for managing a queue of operations.\n * Allows enqueuing values and executing a function when the queue reaches a certain size or after a specified interval.\n * Supports both parallel and sequential execution of the queued operations.\n */\nexport class Queue<T> {\n /** The items currently in the queue. */\n private items: T[] = [];\n\n /** The maximum size of the queue before triggering execution. */\n private readonly maxSize?: number;\n\n /** The interval in milliseconds after which the queue will be executed if not already triggered by size. */\n private readonly interval: number;\n\n /** The function to execute with the items in the queue. */\n private readonly executeFn: (items: T[]) => Promise<void>;\n\n /** Timer for managing the interval-based execution. */\n private timer: NodeJS.Timeout | null = null;\n\n /** Flag to determine if execution should be parallel or sequential. */\n private readonly executeInParallel: boolean;\n\n /** The batch size for processing items. */\n private readonly batchSize
|
|
1
|
+
{"version":3,"file":"queue.mjs","names":[],"sources":["../../../../../../../core/src/utils/queue.ts"],"sourcesContent":["/**\n * A utility class for managing a queue of operations.\n * Allows enqueuing values and executing a function when the queue reaches a certain size or after a specified interval.\n * Supports both parallel and sequential execution of the queued operations.\n */\nexport class Queue<T> {\n /** The items currently in the queue. */\n private items: T[] = [];\n\n /** The maximum size of the queue before triggering execution. */\n private readonly maxSize?: number;\n\n /** The interval in milliseconds after which the queue will be executed if not already triggered by size. */\n private readonly interval: number;\n\n /** The function to execute with the items in the queue. */\n private readonly executeFn: (items: T[]) => Promise<void>;\n\n /** Timer for managing the interval-based execution. */\n private timer: NodeJS.Timeout | null = null;\n\n /** Flag to determine if execution should be parallel or sequential. */\n private readonly executeInParallel: boolean;\n\n /** The batch size for processing items. */\n private readonly batchSize?: number;\n\n /** Called when the execute function rejects. */\n private readonly onError: (error: unknown) => void;\n\n /** Whether the current queue is busy executing */\n private isExecuting = false;\n\n /**\n * Constructs a new Queue instance.\n * @param executeFn - The function to execute with the items in the queue.\n * @param maxSize - The maximum number of items before the queue is executed.\n * @param executeEvery - The time in milliseconds after which the queue is executed if not already triggered.\n * @param executeInParallel - Whether to execute the function in parallel or sequentially.\n * @param batchSize - The number of items to process in each batch.\n */\n public constructor(\n executeFn: (items: T[]) => Promise<void>,\n executeInParallel: boolean = true,\n executeEvery: number = 5000,\n batchSize?: number,\n maxSize?: number,\n onError: (error: unknown) => void = (error) => console.error(\"Queue execution failed\", error),\n ) {\n this.onError = onError;\n this.executeFn = executeFn;\n this.maxSize = maxSize;\n this.interval = executeEvery;\n this.executeInParallel = executeInParallel;\n this.batchSize = batchSize;\n }\n\n /**\n * Adds an item to the queue.\n * Triggers execution if the queue reaches the maximum size.\n * Starts a timer if not already running.\n * @param item - The item to add to the queue.\n */\n public enqueue(item: T): void {\n this.items.push(item);\n if (this.maxSize && this.items.length >= this.maxSize) {\n void this.execute();\n }\n\n if (!this.timer && !this.isExecuting) {\n this.startTimer();\n }\n }\n\n /**\n * Starts a timer to execute the queue after the specified interval.\n */\n private startTimer(): void {\n this.timer = setInterval(() => {\n if (this.items.length > 0) {\n void this.execute();\n }\n }, this.interval);\n this.timer.unref?.();\n }\n\n /**\n * Executes the function with the queued items until the queue is drained.\n * Rejections from the execute function are passed to `onError`.\n */\n private async execute(): Promise<void> {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n\n // the running drain loop will pick up newly enqueued items\n if (this.isExecuting) return;\n\n this.isExecuting = true;\n\n try {\n while (this.items.length > 0) {\n // Without a batch size, all items go in a single call\n const batch = this.items.splice(0, this.batchSize || this.items.length);\n\n try {\n if (!this.batchSize) {\n await this.executeFn(batch);\n } else if (this.executeInParallel) {\n await Promise.all(batch.map((item) => this.executeFn([item])));\n } else {\n for (const item of batch) {\n await this.executeFn([item]);\n }\n }\n } catch (error) {\n this.onError(error);\n }\n }\n } finally {\n this.isExecuting = false;\n }\n }\n}\n"],"mappings":";;;;;;AAKA,IAAa,QAAb,MAAsB;;;;;;;;;CAoCpB,AAAO,YACL,WACA,oBAA6B,MAC7B,eAAuB,KACvB,WACA,SACA,WAAqC,UAAU,QAAQ,MAAM,0BAA0B,KAAK,GAC5F;eAzCmB,CAAC;eAYiB;qBAYjB;EAkBpB,KAAK,UAAU;EACf,KAAK,YAAY;EACjB,KAAK,UAAU;EACf,KAAK,WAAW;EAChB,KAAK,oBAAoB;EACzB,KAAK,YAAY;CACnB;;;;;;;CAQA,AAAO,QAAQ,MAAe;EAC5B,KAAK,MAAM,KAAK,IAAI;EACpB,IAAI,KAAK,WAAW,KAAK,MAAM,UAAU,KAAK,SAC5C,AAAK,KAAK,QAAQ;EAGpB,IAAI,CAAC,KAAK,SAAS,CAAC,KAAK,aACvB,KAAK,WAAW;CAEpB;;;;CAKA,AAAQ,aAAmB;EACzB,KAAK,QAAQ,kBAAkB;GAC7B,IAAI,KAAK,MAAM,SAAS,GACtB,AAAK,KAAK,QAAQ;EAEtB,GAAG,KAAK,QAAQ;EAChB,KAAK,MAAM,QAAQ;CACrB;;;;;CAMA,MAAc,UAAyB;EACrC,IAAI,KAAK,OAAO;GACd,cAAc,KAAK,KAAK;GACxB,KAAK,QAAQ;EACf;EAGA,IAAI,KAAK,aAAa;EAEtB,KAAK,cAAc;EAEnB,IAAI;GACF,OAAO,KAAK,MAAM,SAAS,GAAG;IAE5B,MAAM,QAAQ,KAAK,MAAM,OAAO,GAAG,KAAK,aAAa,KAAK,MAAM,MAAM;IAEtE,IAAI;KACF,IAAI,CAAC,KAAK,WACR,MAAM,KAAK,UAAU,KAAK;UACrB,IAAI,KAAK,mBACd,MAAM,QAAQ,IAAI,MAAM,KAAK,SAAS,KAAK,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;UAE7D,KAAK,MAAM,QAAQ,OACjB,MAAM,KAAK,UAAU,CAAC,IAAI,CAAC;IAGjC,SAAS,OAAO;KACd,KAAK,QAAQ,KAAK;IACpB;GACF;EACF,UAAU;GACR,KAAK,cAAc;EACrB;CACF;AACF"}
|
|
@@ -11,11 +11,15 @@ const existsExceptCurrentIdRule = {
|
|
|
11
11
|
name: "existsExceptCurrentId",
|
|
12
12
|
defaultErrorMessage: "The :input must exist",
|
|
13
13
|
async validate(value, context) {
|
|
14
|
-
const { Model, query, column = context.key, exceptCurrentIdColumn = "id" } = this.context.options;
|
|
14
|
+
const { Model, query, column = context.key, exceptCurrentIdColumn = "id", param = "id" } = this.context.options;
|
|
15
15
|
const { request } = useRequestStore();
|
|
16
16
|
const dbQuery = resolveModelClass(Model).query();
|
|
17
17
|
dbQuery.where(column, value);
|
|
18
|
-
|
|
18
|
+
const rawId = request.input(param);
|
|
19
|
+
if (rawId !== void 0 && rawId !== null && rawId !== "") {
|
|
20
|
+
const currentId = typeof rawId === "string" && /^\d{1,15}$/.test(rawId) ? Number(rawId) : rawId;
|
|
21
|
+
dbQuery.where(exceptCurrentIdColumn, "!=", currentId);
|
|
22
|
+
}
|
|
19
23
|
if (query) await query({
|
|
20
24
|
query: dbQuery,
|
|
21
25
|
value,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exists-except-current-id.mjs","names":[],"sources":["../../../../../../../../core/src/validation/database/exists-except-current-id.ts"],"sourcesContent":["import { resolveModelClass } from \"@warlock.js/cascade\";\nimport { invalidRule, VALID_RULE, type SchemaRule } from \"@warlock.js/seal\";\nimport { useRequestStore } from \"../../http\";\nimport type { ExistsExceptCurrentIdRuleOptions } from \"../types\";\n\n/**\n * Exists except current ID rule\n */\nexport const existsExceptCurrentIdRule: SchemaRule<ExistsExceptCurrentIdRuleOptions> = {\n name: \"existsExceptCurrentId\",\n defaultErrorMessage: \"The :input must exist\",\n async validate(value: any, context) {\n const {\n Model,\n query,\n column = context.key,\n exceptCurrentIdColumn = \"id\",\n } = this.context.options;\n\n const { request } = useRequestStore();\n\n const ResolvedModelClass = resolveModelClass(Model);\n\n const dbQuery = ResolvedModelClass.query();\n\n dbQuery.where(column, value);\n dbQuery.where(exceptCurrentIdColumn, \"!=\",
|
|
1
|
+
{"version":3,"file":"exists-except-current-id.mjs","names":[],"sources":["../../../../../../../../core/src/validation/database/exists-except-current-id.ts"],"sourcesContent":["import { resolveModelClass } from \"@warlock.js/cascade\";\nimport { invalidRule, VALID_RULE, type SchemaRule } from \"@warlock.js/seal\";\nimport { useRequestStore } from \"../../http\";\nimport type { ExistsExceptCurrentIdRuleOptions } from \"../types\";\n\n/**\n * Exists except current ID rule\n */\nexport const existsExceptCurrentIdRule: SchemaRule<ExistsExceptCurrentIdRuleOptions> = {\n name: \"existsExceptCurrentId\",\n defaultErrorMessage: \"The :input must exist\",\n async validate(value: any, context) {\n const {\n Model,\n query,\n column = context.key,\n exceptCurrentIdColumn = \"id\",\n param = \"id\",\n } = this.context.options as ExistsExceptCurrentIdRuleOptions & { param?: string };\n\n const { request } = useRequestStore();\n\n const ResolvedModelClass = resolveModelClass(Model);\n\n const dbQuery = ResolvedModelClass.query();\n\n dbQuery.where(column, value);\n // Read the current id from the configured route param, keeping ObjectId/UUID strings intact\n const rawId = request.input(param);\n\n if (rawId !== undefined && rawId !== null && rawId !== \"\") {\n const currentId =\n typeof rawId === \"string\" && /^\\d{1,15}$/.test(rawId) ? Number(rawId) : rawId;\n\n dbQuery.where(exceptCurrentIdColumn, \"!=\", currentId);\n }\n\n if (query) {\n await query({\n query: dbQuery,\n value,\n allValues: context.allValues,\n });\n }\n\n const document = await dbQuery.first();\n return document ? VALID_RULE : invalidRule(this, context);\n },\n};\n"],"mappings":";;;;;;;;;AAQA,MAAa,4BAA0E;CACrF,MAAM;CACN,qBAAqB;CACrB,MAAM,SAAS,OAAY,SAAS;EAClC,MAAM,EACJ,OACA,OACA,SAAS,QAAQ,KACjB,wBAAwB,MACxB,QAAQ,SACN,KAAK,QAAQ;EAEjB,MAAM,EAAE,YAAY,gBAAgB;EAIpC,MAAM,UAFqB,kBAAkB,KAEZ,CAAC,CAAC,MAAM;EAEzC,QAAQ,MAAM,QAAQ,KAAK;EAE3B,MAAM,QAAQ,QAAQ,MAAM,KAAK;EAEjC,IAAI,UAAU,UAAa,UAAU,QAAQ,UAAU,IAAI;GACzD,MAAM,YACJ,OAAO,UAAU,YAAY,aAAa,KAAK,KAAK,IAAI,OAAO,KAAK,IAAI;GAE1E,QAAQ,MAAM,uBAAuB,MAAM,SAAS;EACtD;EAEA,IAAI,OACF,MAAM,MAAM;GACV,OAAO;GACP;GACA,WAAW,QAAQ;EACrB,CAAC;EAIH,OAAO,MADgB,QAAQ,MAAM,IACnB,aAAa,YAAY,MAAM,OAAO;CAC1D;AACF"}
|
|
@@ -11,11 +11,15 @@ const uniqueExceptCurrentIdRule = {
|
|
|
11
11
|
name: "uniqueExceptCurrentId",
|
|
12
12
|
defaultErrorMessage: "The :input must be unique",
|
|
13
13
|
async validate(value, context) {
|
|
14
|
-
const { Model, column = context.key, exceptCurrentIdColumn = "id", query } = this.context.options;
|
|
14
|
+
const { Model, column = context.key, exceptCurrentIdColumn = "id", query, param = "id" } = this.context.options;
|
|
15
15
|
const { request } = useRequestStore();
|
|
16
16
|
const dbQuery = resolveModelClass(Model).query();
|
|
17
17
|
dbQuery.where(column, value);
|
|
18
|
-
|
|
18
|
+
const rawId = request.input(param);
|
|
19
|
+
if (rawId !== void 0 && rawId !== null && rawId !== "") {
|
|
20
|
+
const currentId = typeof rawId === "string" && /^\d{1,15}$/.test(rawId) ? Number(rawId) : rawId;
|
|
21
|
+
dbQuery.where(exceptCurrentIdColumn, "!=", currentId);
|
|
22
|
+
}
|
|
19
23
|
if (query) await query({
|
|
20
24
|
query: dbQuery,
|
|
21
25
|
value,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unique-except-current-id.mjs","names":[],"sources":["../../../../../../../../core/src/validation/database/unique-except-current-id.ts"],"sourcesContent":["import { resolveModelClass } from \"@warlock.js/cascade\";\nimport { invalidRule, VALID_RULE, type SchemaRule } from \"@warlock.js/seal\";\nimport { useRequestStore } from \"../../http\";\nimport type { UniqueExceptCurrentIdRuleOptions } from \"../types\";\n/**\n * Unique except current ID rule\n */\nexport const uniqueExceptCurrentIdRule: SchemaRule<UniqueExceptCurrentIdRuleOptions> = {\n name: \"uniqueExceptCurrentId\",\n defaultErrorMessage: \"The :input must be unique\",\n async validate(value: any, context) {\n const {\n Model,\n column = context.key,\n exceptCurrentIdColumn = \"id\",\n query,\n } = this.context.options;\n\n const { request } = useRequestStore();\n\n const ResolvedModelClass = resolveModelClass(Model);\n\n const dbQuery = ResolvedModelClass.query();\n\n dbQuery.where(column, value);\n
|
|
1
|
+
{"version":3,"file":"unique-except-current-id.mjs","names":[],"sources":["../../../../../../../../core/src/validation/database/unique-except-current-id.ts"],"sourcesContent":["import { resolveModelClass } from \"@warlock.js/cascade\";\nimport { invalidRule, VALID_RULE, type SchemaRule } from \"@warlock.js/seal\";\nimport { useRequestStore } from \"../../http\";\nimport type { UniqueExceptCurrentIdRuleOptions } from \"../types\";\n/**\n * Unique except current ID rule\n */\nexport const uniqueExceptCurrentIdRule: SchemaRule<UniqueExceptCurrentIdRuleOptions> = {\n name: \"uniqueExceptCurrentId\",\n defaultErrorMessage: \"The :input must be unique\",\n async validate(value: any, context) {\n const {\n Model,\n column = context.key,\n exceptCurrentIdColumn = \"id\",\n query,\n param = \"id\",\n } = this.context.options as UniqueExceptCurrentIdRuleOptions & { param?: string };\n\n const { request } = useRequestStore();\n\n const ResolvedModelClass = resolveModelClass(Model);\n\n const dbQuery = ResolvedModelClass.query();\n\n dbQuery.where(column, value);\n // Read the current id from the configured route param, keeping ObjectId/UUID strings intact\n const rawId = request.input(param);\n\n if (rawId !== undefined && rawId !== null && rawId !== \"\") {\n const currentId =\n typeof rawId === \"string\" && /^\\d{1,15}$/.test(rawId) ? Number(rawId) : rawId;\n\n dbQuery.where(exceptCurrentIdColumn, \"!=\", currentId);\n }\n\n if (query) {\n await query({\n query: dbQuery,\n value,\n allValues: context.allValues,\n });\n }\n\n const document = await dbQuery.first();\n return document ? invalidRule(this, context) : VALID_RULE;\n },\n};\n"],"mappings":";;;;;;;;;AAOA,MAAa,4BAA0E;CACrF,MAAM;CACN,qBAAqB;CACrB,MAAM,SAAS,OAAY,SAAS;EAClC,MAAM,EACJ,OACA,SAAS,QAAQ,KACjB,wBAAwB,MACxB,OACA,QAAQ,SACN,KAAK,QAAQ;EAEjB,MAAM,EAAE,YAAY,gBAAgB;EAIpC,MAAM,UAFqB,kBAAkB,KAEZ,CAAC,CAAC,MAAM;EAEzC,QAAQ,MAAM,QAAQ,KAAK;EAE3B,MAAM,QAAQ,QAAQ,MAAM,KAAK;EAEjC,IAAI,UAAU,UAAa,UAAU,QAAQ,UAAU,IAAI;GACzD,MAAM,YACJ,OAAO,UAAU,YAAY,aAAa,KAAK,KAAK,IAAI,OAAO,KAAK,IAAI;GAE1E,QAAQ,MAAM,uBAAuB,MAAM,SAAS;EACtD;EAEA,IAAI,OACF,MAAM,MAAM;GACV,OAAO;GACP;GACA,WAAW,QAAQ;EACrB,CAAC;EAIH,OAAO,MADgB,QAAQ,MAAM,IACnB,YAAY,MAAM,OAAO,IAAI;CACjD;AACF"}
|
package/esm/validation/init.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { databasePlugin } from "./plugins/database-plugin.mjs";
|
|
|
5
5
|
import { filePlugin } from "./plugins/file-plugin.mjs";
|
|
6
6
|
import { localizedPlugin } from "./plugins/localized-plugin.mjs";
|
|
7
7
|
import "./plugins/index.mjs";
|
|
8
|
-
import { configureSeal, registerPlugin } from "@warlock.js/seal";
|
|
8
|
+
import { configureSeal, getSealConfig, registerPlugin } from "@warlock.js/seal";
|
|
9
9
|
|
|
10
10
|
//#region ../core/src/validation/init.ts
|
|
11
11
|
/**
|
|
@@ -14,7 +14,6 @@ import { configureSeal, registerPlugin } from "@warlock.js/seal";
|
|
|
14
14
|
* This file configures Seal to use Warlock's localization system
|
|
15
15
|
*/
|
|
16
16
|
configureSeal({
|
|
17
|
-
firstErrorOnly: config.key("validation.firstErrorOnly", true),
|
|
18
17
|
translateRule({ rule, attributes }) {
|
|
19
18
|
const translateRule = config.key("validation.translateRule");
|
|
20
19
|
if (translateRule) return translateRule({
|
|
@@ -38,6 +37,11 @@ configureSeal({
|
|
|
38
37
|
return output === translationKey ? attribute : output;
|
|
39
38
|
}
|
|
40
39
|
});
|
|
40
|
+
Object.defineProperty(getSealConfig(), "firstErrorOnly", {
|
|
41
|
+
configurable: true,
|
|
42
|
+
enumerable: true,
|
|
43
|
+
get: () => config.key("validation.firstErrorOnly", true)
|
|
44
|
+
});
|
|
41
45
|
registerPlugin(databasePlugin);
|
|
42
46
|
registerPlugin(filePlugin);
|
|
43
47
|
registerPlugin(localizedPlugin);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.mjs","names":[],"sources":["../../../../../../../core/src/validation/init.ts"],"sourcesContent":["/**\n * Initialize Seal with Warlock Framework Settings\n *\n * This file configures Seal to use Warlock's localization system\n */\n\n// Auto-register framework plugins\nimport { configureSeal, registerPlugin } from \"@warlock.js/seal\";\nimport { config } from \"../config\";\nimport { t } from \"../http/middleware/inject-request-context\";\nimport { databasePlugin, filePlugin, localizedPlugin } from \"./plugins\";\n\n// Configure Seal to use Warlock's localization\nconfigureSeal({\n
|
|
1
|
+
{"version":3,"file":"init.mjs","names":[],"sources":["../../../../../../../core/src/validation/init.ts"],"sourcesContent":["/**\n * Initialize Seal with Warlock Framework Settings\n *\n * This file configures Seal to use Warlock's localization system\n */\n\n// Auto-register framework plugins\nimport { configureSeal, getSealConfig, registerPlugin } from \"@warlock.js/seal\";\nimport { config } from \"../config\";\nimport { t } from \"../http/middleware/inject-request-context\";\nimport { databasePlugin, filePlugin, localizedPlugin } from \"./plugins\";\n\n// Configure Seal to use Warlock's localization\nconfigureSeal({\n translateRule({ rule, attributes }) {\n const translateRule = config.key(\"validation.translateRule\");\n\n if (translateRule) {\n return translateRule({ rule, attributes });\n }\n\n const translationGroup = config.key(\"validation.translationGroup\", \"validation\");\n\n const translationKey = `${translationGroup}.${rule.name}`;\n const translation = t(translationKey, attributes);\n\n return translation === translationKey\n ? rule.errorMessage || rule.defaultErrorMessage\n : translation;\n },\n\n translateAttribute({ attribute, context, rule }) {\n const translateAttribute = config.key(\"validation.translateAttribute\");\n\n if (translateAttribute) {\n return translateAttribute({ attribute, context, rule });\n }\n\n const attributeGroup = config.key(\"validation.attributeGroup\") ?? \"attributes\";\n\n const translationKey = `${attributeGroup ? attributeGroup + \".\" : \"\"}${attribute}`;\n\n const output = t(translationKey, context.allValues);\n return output === translationKey ? attribute : output;\n },\n});\n\n// App config is not loaded yet when this module is imported, so resolve\n// firstErrorOnly lazily on every read instead of capturing it once.\nObject.defineProperty(getSealConfig(), \"firstErrorOnly\", {\n configurable: true,\n enumerable: true,\n get: () => config.key(\"validation.firstErrorOnly\", true),\n});\n\n// Register plugins to inject methods\nregisterPlugin(databasePlugin);\nregisterPlugin(filePlugin);\nregisterPlugin(localizedPlugin);\n"],"mappings":";;;;;;;;;;;;;;;AAaA,cAAc;CACZ,cAAc,EAAE,MAAM,cAAc;EAClC,MAAM,gBAAgB,OAAO,IAAI,0BAA0B;EAE3D,IAAI,eACF,OAAO,cAAc;GAAE;GAAM;EAAW,CAAC;EAK3C,MAAM,iBAAiB,GAFE,OAAO,IAAI,+BAA+B,YAE1B,EAAE,GAAG,KAAK;EACnD,MAAM,cAAc,EAAE,gBAAgB,UAAU;EAEhD,OAAO,gBAAgB,iBACnB,KAAK,gBAAgB,KAAK,sBAC1B;CACN;CAEA,mBAAmB,EAAE,WAAW,SAAS,QAAQ;EAC/C,MAAM,qBAAqB,OAAO,IAAI,+BAA+B;EAErE,IAAI,oBACF,OAAO,mBAAmB;GAAE;GAAW;GAAS;EAAK,CAAC;EAGxD,MAAM,iBAAiB,OAAO,IAAI,2BAA2B,KAAK;EAElE,MAAM,iBAAiB,GAAG,iBAAiB,iBAAiB,MAAM,KAAK;EAEvE,MAAM,SAAS,EAAE,gBAAgB,QAAQ,SAAS;EAClD,OAAO,WAAW,iBAAiB,YAAY;CACjD;AACF,CAAC;AAID,OAAO,eAAe,cAAc,GAAG,kBAAkB;CACvD,cAAc;CACd,YAAY;CACZ,WAAW,OAAO,IAAI,6BAA6B,IAAI;AACzD,CAAC;AAGD,eAAe,cAAc;AAC7B,eAAe,UAAU;AACzB,eAAe,eAAe"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Response } from "../http/response.mjs";
|
|
1
2
|
import { v } from "@warlock.js/seal";
|
|
2
3
|
import config from "@mongez/config";
|
|
3
4
|
import { log } from "@warlock.js/logger";
|
|
@@ -43,9 +44,9 @@ async function validateAll(validation, request, response) {
|
|
|
43
44
|
response
|
|
44
45
|
});
|
|
45
46
|
if (result) {
|
|
46
|
-
if (!response.statusCode) response.setStatusCode(config.get("validation.responseStatus", 400));
|
|
47
47
|
log.info("validation", "failed", "Validation failed");
|
|
48
|
-
return result;
|
|
48
|
+
if (result instanceof Response) return result;
|
|
49
|
+
return response.send(result, config.get("validation.responseStatus", 400));
|
|
49
50
|
}
|
|
50
51
|
log.info("validation", "passed", "Validation passed");
|
|
51
52
|
}
|