diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1793dd1..993c334 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,14 +85,26 @@ jobs: with: task: test command: pnpm test:coverage $TURBO_FLAGS - - name: Upload coverage report - # Code Quality requires the org on GitHub Team/Enterprise Cloud, which ExaDev is not yet on, so the upload call itself will fail until that changes -- fail-on-error: false keeps that failure a log annotation instead of gating the release job below on a feature we can't turn on yet. Also guarded against fork PRs, which never hold the code-quality: write permission to upload. - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + # One upload per package, each labelled by the package whose coverage it is, since the action takes a single report file rather than a set. + # + # Both are conditional on their own report existing, which is not defensive: `pnpm test:coverage` runs through turbo with $TURBO_FLAGS, which carries --affected on a pull request, so a package no commit in the PR touched legitimately does not run and legitimately writes no report. `fail-on-error` does not cover that case -- it governs the upload call, while a missing input file fails the action before it gets that far -- so without the hashFiles guard a PR confined to one package fails this job on the other package's absent report. + # + # Code Quality requires the org on GitHub Team/Enterprise Cloud, which ExaDev is not yet on, so the upload call itself will fail until that changes -- fail-on-error: false keeps that failure a log annotation instead of gating the release job below on a feature we can't turn on yet. Also guarded against fork PRs, which never hold the code-quality: write permission to upload. + - name: Upload coverage report (trilean) + if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && hashFiles('packages/trilean/coverage/cobertura-coverage.xml') != '' uses: actions/upload-code-coverage@v1 with: file: packages/trilean/coverage/cobertura-coverage.xml language: typescript - label: unit + label: unit-trilean + fail-on-error: false + - name: Upload coverage report (trilean-sql) + if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && hashFiles('packages/trilean-sql/coverage/cobertura-coverage.xml') != '' + uses: actions/upload-code-coverage@v1 + with: + file: packages/trilean-sql/coverage/cobertura-coverage.xml + language: typescript + label: unit-trilean-sql fail-on-error: false test-integration: diff --git a/README.md b/README.md index e88865d..8b4771f 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ The pnpm workspace holding the trilean packages. This file describes the reposit | Package | Directory | Published as | | --- | --- | --- | | [trilean](packages/trilean/README.md) | `packages/trilean` | [`trilean`](https://www.npmjs.com/package/trilean) on npm, [`@exadev/trilean`](https://github.com/ExaDev/trilean/pkgs/npm/trilean) on GitHub Packages | +| [trilean-sql](packages/trilean-sql/README.md) | `packages/trilean-sql` | [`trilean-sql`](https://www.npmjs.com/package/trilean-sql) on npm | Each package is versioned, released, and published independently of every other, from its own commit history. There is no lockstep version shared across the workspace. @@ -35,7 +36,8 @@ The `pnpm ` scripts are thin wrappers over `turbo run _`; the unders ``` . workspace root: tooling config, the task pipeline, release orchestration ├── packages/ -│ └── trilean/ the published library, with its own README, CHANGELOG, and configs +│ ├── trilean/ the evaluation library, with its own README, CHANGELOG, and configs +│ └── trilean-sql/ the SQL compiler over trilean's predicate trees, likewise self-contained ├── pnpm-workspace.yaml package globs and pnpm's install-time settings ├── turbo.json the task pipeline every package's tasks are ordered and cached by ├── tsconfig.base.json the compiler options every package extends diff --git a/packages/trilean-sql/LICENSE b/packages/trilean-sql/LICENSE new file mode 100644 index 0000000..e6cf1d1 --- /dev/null +++ b/packages/trilean-sql/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Joseph Mearman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/trilean-sql/README.md b/packages/trilean-sql/README.md new file mode 100644 index 0000000..b21c8d0 --- /dev/null +++ b/packages/trilean-sql/README.md @@ -0,0 +1,147 @@ +# trilean-sql + +Compiles a [trilean](https://www.npmjs.com/package/trilean) predicate tree into a parameterised SQL `WHERE` fragment, so a rule stored as data can be evaluated by the database over a whole table instead of in process, one subject at a time. + +```sh +pnpm add trilean-sql +``` + +`trilean` is a peer concern rather than an implementation detail: this package takes its `PredicateNode` type as input and declares it as a dependency, so the tree you compile is the same tree you evaluate. + +## The idea + +trilean evaluates a predicate to `definite(true)`, `definite(false)`, or `indeterminate` — the third value meaning the tree could not be judged, usually because a reference resolved to nothing. Given a table of subjects and a rule to apply to all of them, the obvious approach is to fetch every row and evaluate the tree once per row. That reads the whole table to discard most of it. + +The alternative is to let the database apply the rule. It already has a three-valued logic: `TRUE`, `FALSE`, and `NULL` meaning unknown, with `AND`, `OR` and `NOT` following Kleene's strong tables — the same tables trilean's own connectives implement — and a comparison against `NULL` yielding `NULL` rather than a verdict. A `WHERE` clause keeps only the rows whose condition came out `TRUE`, so a row whose condition was unknown is dropped exactly as a subject the evaluator declines to judge is not accepted. + +So the third value is not reimplemented on top of SQL. It is the same third value, and the compiler's job is to translate the tree faithfully enough that it stays that way. + +```ts +import { compilePredicateNode } from "trilean-sql"; +import type { PredicateNode } from "trilean"; + +const rule: PredicateNode = { + kind: "allOf", + operands: [ + { + kind: "compare", + op: "gte", + left: { kind: "reference", key: "age" }, + right: { kind: "numberLiteral", value: 18 }, + }, + { + kind: "textCompare", + op: "matches", + left: { kind: "reference", key: "email" }, + right: { kind: "textLiteral", value: "@example[.]com$" }, + }, + ], +}; + +const columns = { + age: { column: "age", paramType: "number" }, + email: { column: "email", paramType: "text" }, +} as const; + +const { sql, params } = compilePredicateNode(rule, { + dialect: "postgres", + columnFor: (key) => { + const binding = columns[key as keyof typeof columns]; + if (binding === undefined) throw new Error(`no column for ${key}`); + return binding; + }, +}); + +// sql: (("age" >= $1::double precision) AND ("email" ~ $2::text)) +// params: [18, "@example[.]com$"] +await client.query(`SELECT id FROM members WHERE ${sql}`, params); +``` + +The fragment is a self-contained boolean expression with no `WHERE` keyword of its own, so it also drops into a `HAVING`, a `CHECK`, or a partial index predicate. + +## API + +### `compilePredicateNode(node, options)` + +Returns `{ sql, params }`. Throws rather than approximating; see [Refusal](#refusal). + +`options.dialect` is `"postgres"`. It is required rather than defaulted so that a second dialect is a new value here, not a change of behaviour for callers who never said which one they meant. + +`options.columnFor(referenceKey)` maps a `reference` node's key onto `{ column, paramType? }`. Only string keys reach it — trilean permits any JSON value as a key, and a non-string one is refused before the call. Throwing from `columnFor` is how you reject a key you have no column for; the exception propagates out unchanged rather than being wrapped. It is memoised for the duration of one compilation, so it is called once per distinct key however many times that key appears. + +`column` may be qualified with dots (`"members.age"`); each segment is emitted as its own double-quoted identifier. Quoting is unconditional, which is what makes an arbitrary column name safe — an embedded quote is doubled, so a name carrying `"; DROP TABLE ...` becomes one inert identifier that simply does not exist. It also means the name is taken literally rather than case-folded, so return the column's real, case-exact name. + +`paramType` declares the column's value kind: `"text"`, `"number"`, `"boolean"`, or `"timestamp"` (trilean's `instant`). It is optional and worth supplying — it does not change the emitted SQL, but it is the only thing that lets the compiler detect the operand-kind mismatches described under [Refusal](#refusal). Without it, a comparison PostgreSQL would coerce into a definite answer where trilean returns `indeterminate` compiles silently. + +### `findUnpushableNodeKind(node, options?)` + +Returns `{ kind, path, reason }` for the first node the compiler will not translate, or `undefined` if the whole tree is pushable. `compilePredicateNode` runs it first and throws on any result, so call it yourself only to *choose* between pushdown and in-process evaluation without provoking an exception: + +```ts +const blocker = findUnpushableNodeKind(rule, options); +const rows = blocker + ? await evaluateEveryRowInProcess(rule) + : await queryWith(compilePredicateNode(rule, options)); +``` + +Passing `options` widens the check: without them the walk is purely structural; with them it also applies the operand-kind rules that depend on each column's declared `paramType`. + +### Errors + +`UnsupportedNodeError` (carrying `nodeKind`, `path`, `reason`) and `InvalidColumnError` (carrying `referenceKey`, `column`), both extending `TrileanSqlError`. + +## What compiles + +| Node | PostgreSQL | +| --- | --- | +| `and`, `or`, `not` | `AND`, `OR`, `NOT` | +| `allOf`, `anyOf` | n-ary `AND`, `OR`; empty operands become each connective's identity, `TRUE` and `FALSE`, matching the evaluator's own fold | +| `compare` | `>`, `>=`, `<`, `<=`, `=`, `<>` | +| `textCompare` | `=`, `<>`, and `~` / `!~` for `matches` / `notMatches` | +| `memberOf` | `IN` / `NOT IN`, one parameter per candidate | +| `exists` | `IS NOT NULL` | +| `reference` | the mapped column, as a quoted identifier | +| `textLiteral`, `numberLiteral`, `booleanLiteral`, `instantLiteral` | a bind parameter, cast to `text`, `double precision`, `boolean`, `timestamptz` | + +Every literal in the tree becomes a parameter. Nothing but structure, operators and quoted identifiers is ever written into the returned `sql`, so a literal's content cannot alter the statement. + +Placeholders are always cast. That is not decoration: PostgreSQL rejects `$1 < $2` outright because it cannot determine either parameter's type, and casting each placeholder to the type its own literal kind implies is what makes a fragment's meaning independent of how a particular driver decided to infer an untyped parameter. `timestamptz` rather than `timestamp`, because trilean's `instant` is an ISO-8601 string that may carry an offset and parsing one as a naive timestamp would silently discard it. + +An empty `memberOf` candidate list is worth a note, because `IN ()` is a syntax error and the two constants it is tempting to fold to are both wrong. An empty `in` is false and an empty `notIn` is true only once the operand itself is known, and both stay unknown while it is `NULL`. The compiled forms — `(x IS NULL AND NULL::boolean)` and `(x IS NOT NULL OR NULL::boolean)` — reproduce that exactly, which a bare `FALSE`/`TRUE` would not, most visibly under a surrounding `NOT`. + +## Refusal + +The compiler never degrades. There is no best-effort fragment, no silently dropped conjunct, no approximation that answers differently from the evaluator. Either the whole tree compiles to SQL that agrees with `evaluatePredicate` row for row, or `UnsupportedNodeError` is thrown and the caller evaluates in process instead. + +The check is an allow-list walk rather than a deny-list, so a node kind added to trilean after this version was written is refused by default instead of falling through to whatever branch happened to be last. + +That guarantee is about the tree's structure, and it has two limits, both about the *content* of a string operand rather than any node's kind, and neither reachable by a walk over kinds. A `matches`/`notMatches` pattern is matched by the server in PostgreSQL's own regular-expression language, not ECMAScript's — see [Regular expressions](#regular-expressions), which is where the row-for-row claim actually stops. An `instantLiteral` is parsed by PostgreSQL rather than by `Date`, so one carrying no UTC offset is read in the *database session's* time zone where trilean reads it in the Node process's, and one PostgreSQL cannot parse at all raises a query error at execution time rather than this exception (trilean answers `indeterminate` for the same string). Pass instants as offset-bearing ISO-8601, which both read identically. + +**Kinds this version does not translate.** `some`, `every`, `fold`: these range over a collection the caller's resolvers supply, which is not the query's row set. `lookup`, `call`, `delegate`, `treeReference`: each is resolved by something the database has no access to — the caller's resolvers, its function registry, an external system. `conditional`: not implemented here. `accumulator`: only meaningful inside a `reduce` fold. `arithmetic` and `negate`: these carry and combine units, and pushing them down would drop that dimensional analysis without saying so. `durationLiteral`: trilean compares durations by normalising both operands to milliseconds, with no column-level equivalent to normalise against. `complexLiteral`: PostgreSQL has no complex type. + +**Shapes refused despite a supported kind.** A `reference` whose key is not a string, since there is nothing to map. A `reference` or `numberLiteral` carrying a `unit`: a unit on a reference asserts that the resolved value carries the same one, and a column has no unit for that assertion to be checked against. A `numberLiteral` of `NaN`: trilean compares numbers with `===`, under which NaN equals nothing including itself, while PostgreSQL defines NaN as equal to itself and greater than every other double — so `NaN = NaN` selects every row there and none here. Infinities are not refused alongside it; both engines order them identically. + +**Operand pairings the two engines would answer differently.** These are the reason to declare `paramType`, and they are only detectable where it is declared: + +- A `compare` against text. trilean returns `wrong-type` and directs you to `textCompare`; PostgreSQL would order the operand by collation and answer definitely. +- An ordering `compare` (`gt`/`gte`/`lt`/`lte`) against a boolean. trilean has no order for booleans; PostgreSQL orders `false` before `true`. +- A `textCompare` against a non-text operand, which trilean treats as `wrong-type`. +- Any comparison whose operands are of different declared kinds — a number against an instant, say. trilean calls that `wrong-type`; PostgreSQL may coerce one to the other and answer definitely. + +Left undeclared, these compile, and the divergence is real but invisible. That is the whole argument for supplying `paramType`. + +## Regular expressions + +`matches` and `notMatches` compile to PostgreSQL's `~` and `!~`, so the pattern is matched by the server. PostgreSQL's advanced regular expressions and ECMAScript's `RegExp` are close but not the same language: shorthand classes and lookahead exist in both, and much everyday pattern syntax is portable, but they are separate implementations with their own escapes, quantifier subtleties and matching rules. A pattern that relies on ECMAScript-specific behaviour may match differently once pushed down. Keep patterns to portable syntax, or evaluate them in process. + +## Tests + +The unit suite asserts compiled SQL text and parameter arrays per node kind. It cannot, on its own, establish anything about three-valued behaviour: `("age" > $1)` is only indeterminate-preserving because of what PostgreSQL's planner does with a `NULL` age, which is a fact about PostgreSQL rather than about the string. + +So the integration suite (`pnpm test:integration`) starts a real PostgreSQL server in an ephemeral container, seeds a table whose rows carry real `NULL`s, and for every case executes the compiled fragment as a `WHERE` clause *and* evaluates the same tree through trilean's own `evaluatePredicate` once per row, asserting the two agree on which rows match and which do not. Agreement on absence matters as much as on presence: the case that distinguishes three-valued logic from two-valued is the row that appears in neither a predicate nor its negation. + +It needs a working Docker daemon. + +## Licence + +MIT — see [LICENSE](LICENSE). diff --git a/packages/trilean-sql/eslint.config.ts b/packages/trilean-sql/eslint.config.ts new file mode 100644 index 0000000..47b246e --- /dev/null +++ b/packages/trilean-sql/eslint.config.ts @@ -0,0 +1,80 @@ +import { builtinModules } from "node:module"; +import { exadevConfig } from "@exadev/eslint-config"; +import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; +import globals from "globals"; + +const nodeBuiltinBaseModules = [ + ...new Set( + builtinModules + .filter((name) => !name.startsWith("_") && !name.startsWith("node:")) + .map((name) => + name.includes("/") ? name.slice(0, name.indexOf("/")) : name, + ), + ), +].sort(); +const bareNodeBuiltinPattern = `^(${nodeBuiltinBaseModules.join("|")})(/.*)?$`; + +export default exadevConfig( + {}, + { + ignores: ["dist", "coverage", "node_modules", ".turbo"], + }, + { + languageOptions: { + parserOptions: { + project: ["./tsconfig.json", "./tsconfig.node.json"], + tsconfigRootDir: import.meta.dirname, + }, + globals: { ...globals.node }, + }, + }, + { + rules: { + "@typescript-eslint/consistent-type-imports": [ + "error", + { fixStyle: "inline-type-imports" }, + ], + "exadev/barrel-policy": ["error", { mode: "single" }], + }, + }, + { + files: ["src/**/*.ts"], + ignores: ["src/**/*.test.ts", "src/test-support/**"], + rules: { + "no-restricted-imports": [ + "error", + { + patterns: [ + { + group: ["node:*", "node:*/**"], + message: + "This is an isomorphic library: node:* imports are banned in runtime src.", + }, + { + regex: bareNodeBuiltinPattern, + message: + "This is an isomorphic library: bare Node builtin imports are banned in runtime src.", + }, + ], + }, + ], + "no-restricted-globals": [ + "error", + { + name: "Buffer", + message: "Buffer is Node-only; use Uint8Array/plain objects instead.", + }, + ], + }, + }, + { + files: ["**/*.test.ts"], + rules: { + "@typescript-eslint/no-empty-function": [ + "error", + { allow: ["arrowFunctions", "asyncFunctions"] }, + ], + }, + }, + eslintPluginPrettierRecommended, +); diff --git a/packages/trilean-sql/package.json b/packages/trilean-sql/package.json new file mode 100644 index 0000000..e02291c --- /dev/null +++ b/packages/trilean-sql/package.json @@ -0,0 +1,100 @@ +{ + "name": "trilean-sql", + "version": "0.0.0", + "description": "Compiles a trilean predicate tree into a parameterised SQL WHERE fragment, pushing three-valued logic down onto the database's own NULL propagation instead of evaluating it in process.", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/ExaDev/trilean.git", + "directory": "packages/trilean-sql" + }, + "homepage": "https://github.com/ExaDev/trilean/tree/main/packages/trilean-sql", + "bugs": { + "url": "https://github.com/ExaDev/trilean/issues" + }, + "exports": { + ".": { + "types": { + "import": "./dist/index.d.ts", + "require": "./dist/index.d.cts" + }, + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./*": { + "types": { + "import": "./dist/*.d.ts", + "require": "./dist/*.d.cts" + }, + "import": "./dist/*.js", + "require": "./dist/*.cjs" + } + }, + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "registry": "https://registry.npmjs.org/" + }, + "sideEffects": false, + "engines": { + "node": ">=20" + }, + "license": "MIT", + "dependencies": { + "trilean": "workspace:^" + }, + "scripts": { + "build": "turbo run _build", + "_build": "tsdown", + "lint": "turbo run _lint", + "_lint": "eslint . --fix --cache --max-warnings 0", + "typecheck": "turbo run _typecheck _typecheck:attw", + "_typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.node.json", + "_typecheck:attw": "attw --pack", + "test": "turbo run _test", + "_test": "vitest run --project unit", + "test:coverage": "turbo run _test:coverage", + "_test:coverage": "vitest run --project unit --coverage", + "test:integration": "turbo run _test:integration", + "_test:integration": "vitest run --project integration", + "prepush": "turbo run _prepush", + "_prepush": "true", + "prepublishOnly": "pnpm run lint && pnpm run typecheck && pnpm run test && pnpm run test:integration && pnpm run build && publint && attw --pack" + }, + "keywords": [ + "sql", + "postgres", + "predicate", + "three-valued-logic", + "query-builder", + "predicate-pushdown", + "trilean", + "rules-engine" + ], + "devDependencies": { + "@arethetypeswrong/cli": "^0.18.5", + "@exadev/eslint-config": "^2.10.2", + "@testcontainers/postgresql": "^12.1.0", + "@types/node": "^26.4.0", + "@types/pg": "^8.15.6", + "@vitest/coverage-v8": "^4.1.11", + "eslint": "^10.9.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.6", + "globals": "^17.11.0", + "pg": "^8.16.3", + "prettier": "^3.9.6", + "publint": "^0.3.24", + "tsdown": "^0.22.14", + "turbo": "^2.10.12", + "typescript": "^6.0.3", + "typescript-eslint": "^8.68.0", + "vitest": "^4.1.11" + } +} diff --git a/packages/trilean-sql/src/compile.test.ts b/packages/trilean-sql/src/compile.test.ts new file mode 100644 index 0000000..cc63b30 --- /dev/null +++ b/packages/trilean-sql/src/compile.test.ts @@ -0,0 +1,461 @@ +import type { PredicateNode } from "trilean"; +import { describe, expect, it, vi } from "vitest"; +import { compilePredicateNode } from "./compile"; +import { InvalidColumnError, UnsupportedNodeError } from "./errors"; +import type { SqlCompileOptions } from "./options"; +import { subjectOptions } from "./test-support/columns"; + +function compile( + node: PredicateNode, + options: Readonly = subjectOptions, +) { + return compilePredicateNode(node, options); +} + +// Named rather than written twice, so each case's expected `params` is the same value the tree was built from rather than a literal that could drift away from it. +const ADULT_AGE = 18; +const SAMPLE_AGE = 40; +const EXCLUDED_AGE = 7; +const LOWER_BOUND = 1; +const UPPER_BOUND = 4; + +const ageOver: PredicateNode = { + kind: "compare", + op: "gt", + left: { kind: "reference", key: "age" }, + right: { kind: "numberLiteral", value: ADULT_AGE }, +}; + +describe("connectives", () => { + it("compiles and/or/not to their SQL counterparts", () => { + const node: PredicateNode = { + kind: "not", + operand: { + kind: "and", + left: ageOver, + right: { + kind: "or", + left: { kind: "exists", operand: { kind: "reference", key: "note" } }, + right: { + kind: "compare", + op: "eq", + left: { kind: "reference", key: "active" }, + right: { kind: "booleanLiteral", value: true }, + }, + }, + }, + }; + + expect(compile(node)).toEqual({ + sql: '(NOT (("age" > $1::double precision) AND (("note" IS NOT NULL) OR ("active" = $2::boolean))))', + params: [ADULT_AGE, true], + }); + }); + + it("compiles allOf and anyOf to n-ary AND and OR", () => { + const operands: PredicateNode[] = [ + ageOver, + { kind: "exists", operand: { kind: "reference", key: "note" } }, + { + kind: "textCompare", + op: "equals", + left: { kind: "reference", key: "name" }, + right: { kind: "textLiteral", value: "ada" }, + }, + ]; + + expect(compile({ kind: "allOf", operands }).sql).toBe( + '(("age" > $1::double precision) AND ("note" IS NOT NULL) AND ("name" = $2::text))', + ); + expect(compile({ kind: "anyOf", operands }).sql).toBe( + '(("age" > $1::double precision) OR ("note" IS NOT NULL) OR ("name" = $2::text))', + ); + }); + + it("compiles an empty allOf and anyOf to each connective's own identity", () => { + // Matching the evaluator, which folds allOf from definite(true) and anyOf from definite(false). + expect(compile({ kind: "allOf", operands: [] })).toEqual({ + sql: "(TRUE)", + params: [], + }); + expect(compile({ kind: "anyOf", operands: [] })).toEqual({ + sql: "(FALSE)", + params: [], + }); + }); +}); + +describe("compare", () => { + it.each([ + ["gt", ">"], + ["gte", ">="], + ["lt", "<"], + ["lte", "<="], + ["eq", "="], + ["neq", "<>"], + ] as const)("compiles '%s' to '%s'", (op, sqlOperator) => { + expect( + compile({ + kind: "compare", + op, + left: { kind: "reference", key: "age" }, + right: { kind: "numberLiteral", value: SAMPLE_AGE }, + }), + ).toEqual({ + sql: `("age" ${sqlOperator} $1::double precision)`, + params: [SAMPLE_AGE], + }); + }); + + it("casts an instant literal to timestamptz so an offset survives the comparison", () => { + expect( + compile({ + kind: "compare", + op: "gte", + left: { kind: "reference", key: "joined" }, + right: { kind: "instantLiteral", value: "2020-01-01T00:00:00+02:00" }, + }), + ).toEqual({ + sql: '("joined" >= $1::timestamptz)', + params: ["2020-01-01T00:00:00+02:00"], + }); + }); + + it("casts both sides when neither operand is a column", () => { + // PostgreSQL rejects `$1 < $2` outright -- it cannot determine either parameter's type -- so a literal-only comparison is only executable because every placeholder carries the cast its own literal kind implies. + expect( + compile({ + kind: "compare", + op: "lt", + left: { kind: "numberLiteral", value: 1 }, + right: { kind: "numberLiteral", value: 2 }, + }), + ).toEqual({ + sql: "($1::double precision < $2::double precision)", + params: [1, 2], + }); + }); + + it("casts by the literal's own kind, whether or not the column declares one", () => { + // `age` declares number and `note` declares nothing; the placeholder is identical either way, which is why the compiler does not consult the declaration when casting. + expect( + compile({ + kind: "compare", + op: "gt", + left: { kind: "reference", key: "age" }, + right: { kind: "numberLiteral", value: 1 }, + }).sql, + ).toBe('("age" > $1::double precision)'); + + expect( + compile({ + kind: "compare", + op: "gt", + left: { kind: "reference", key: "note" }, + right: { kind: "numberLiteral", value: 1 }, + }).sql, + ).toBe('("note" > $1::double precision)'); + }); +}); + +describe("textCompare", () => { + it.each([ + ["equals", "="], + ["notEquals", "<>"], + ["matches", "~"], + ["notMatches", "!~"], + ] as const)("compiles '%s' to '%s'", (op, sqlOperator) => { + expect( + compile({ + kind: "textCompare", + op, + left: { kind: "reference", key: "name" }, + right: { kind: "textLiteral", value: "^a" }, + }), + ).toEqual({ + sql: `("name" ${sqlOperator} $1::text)`, + params: ["^a"], + }); + }); + + it("compares two columns without producing a parameter", () => { + expect( + compile({ + kind: "textCompare", + op: "equals", + left: { kind: "reference", key: "name" }, + right: { kind: "reference", key: "note" }, + }), + ).toEqual({ sql: '("name" = "note")', params: [] }); + }); +}); + +describe("memberOf", () => { + it("compiles 'in' to IN with one parameter per candidate", () => { + expect( + compile({ + kind: "memberOf", + op: "in", + operand: { kind: "reference", key: "name" }, + candidates: [ + { kind: "textLiteral", value: "ada" }, + { kind: "textLiteral", value: "grace" }, + ], + }), + ).toEqual({ + sql: '("name" IN ($1::text, $2::text))', + params: ["ada", "grace"], + }); + }); + + it("compiles 'notIn' to NOT IN", () => { + expect( + compile({ + kind: "memberOf", + op: "notIn", + operand: { kind: "reference", key: "age" }, + candidates: [{ kind: "numberLiteral", value: EXCLUDED_AGE }], + }), + ).toEqual({ + sql: '("age" NOT IN ($1::double precision))', + params: [EXCLUDED_AGE], + }); + }); + + it("compiles an empty candidate list to a form that still propagates the operand's NULL", () => { + // `IN ()` is a syntax error, and folding to a bare FALSE/TRUE would answer definitely for a NULL operand where the evaluator returns indeterminate. The integration suite executes both of these against a real server. + expect( + compile({ + kind: "memberOf", + op: "in", + operand: { kind: "reference", key: "name" }, + candidates: [], + }), + ).toEqual({ sql: '("name" IS NULL AND NULL::boolean)', params: [] }); + + expect( + compile({ + kind: "memberOf", + op: "notIn", + operand: { kind: "reference", key: "name" }, + candidates: [], + }), + ).toEqual({ sql: '("name" IS NOT NULL OR NULL::boolean)', params: [] }); + }); +}); + +describe("exists", () => { + it("compiles to IS NOT NULL", () => { + expect( + compile({ kind: "exists", operand: { kind: "reference", key: "note" } }), + ).toEqual({ sql: '("note" IS NOT NULL)', params: [] }); + }); +}); + +describe("parameters", () => { + it("numbers placeholders in emission order across a nested tree", () => { + const node: PredicateNode = { + kind: "allOf", + operands: [ + { + kind: "compare", + op: "gt", + left: { kind: "reference", key: "age" }, + right: { kind: "numberLiteral", value: LOWER_BOUND }, + }, + { + kind: "memberOf", + op: "in", + operand: { kind: "reference", key: "name" }, + candidates: [ + { kind: "textLiteral", value: "b" }, + { kind: "textLiteral", value: "c" }, + ], + }, + { + kind: "compare", + op: "lt", + left: { kind: "reference", key: "age" }, + right: { kind: "numberLiteral", value: UPPER_BOUND }, + }, + ], + }; + + expect(compile(node)).toEqual({ + sql: '(("age" > $1::double precision) AND ("name" IN ($2::text, $3::text)) AND ("age" < $4::double precision))', + params: [LOWER_BOUND, "b", "c", UPPER_BOUND], + }); + }); + + it("never writes a literal into the SQL text", () => { + const injection = "'; DROP TABLE subjects; --"; + const compiled = compile({ + kind: "textCompare", + op: "equals", + left: { kind: "reference", key: "name" }, + right: { kind: "textLiteral", value: injection }, + }); + + expect(compiled.sql).not.toContain("DROP"); + expect(compiled.sql).toBe('("name" = $1::text)'); + expect(compiled.params).toEqual([injection]); + }); +}); + +describe("column identifiers", () => { + function optionsReturning(column: string): SqlCompileOptions { + return { dialect: "postgres", columnFor: () => ({ column }) }; + } + + const noteExists: PredicateNode = { + kind: "exists", + operand: { kind: "reference", key: "anything" }, + }; + + it("quotes each dot-separated segment separately", () => { + expect( + compile(noteExists, optionsReturning("public.subjects.note")).sql, + ).toBe('("public"."subjects"."note" IS NOT NULL)'); + }); + + it("neutralises a column name carrying a quote by doubling it", () => { + const compiled = compile( + noteExists, + optionsReturning('note"; DROP TABLE subjects; --'), + ); + expect(compiled.sql).toBe( + '("note""; DROP TABLE subjects; --" IS NOT NULL)', + ); + }); + + it("rejects an empty column name", () => { + expect(() => compile(noteExists, optionsReturning(""))).toThrow( + InvalidColumnError, + ); + }); + + it("rejects an empty dot-separated segment", () => { + expect(() => compile(noteExists, optionsReturning("public..note"))).toThrow( + InvalidColumnError, + ); + }); + + it("propagates an error thrown by columnFor unchanged", () => { + expect(() => + compile({ + kind: "exists", + operand: { kind: "reference", key: "unmapped" }, + }), + ).toThrow("no column mapped for reference key 'unmapped'"); + }); + + it("asks columnFor once per distinct reference key", () => { + const columnFor = vi.fn(() => ({ + column: "age", + paramType: "number" as const, + })); + compile( + { + kind: "and", + left: ageOver, + right: { + kind: "compare", + op: "lt", + left: { kind: "reference", key: "age" }, + right: { kind: "numberLiteral", value: 65 }, + }, + }, + { dialect: "postgres", columnFor }, + ); + expect(columnFor).toHaveBeenCalledTimes(1); + }); +}); + +describe("refusal", () => { + it("throws UnsupportedNodeError carrying the offending kind and path", () => { + let thrown: unknown; + try { + compile({ + kind: "and", + left: ageOver, + right: { kind: "treeReference", key: "other" }, + }); + } catch (error) { + thrown = error; + } + + expect(thrown).toBeInstanceOf(UnsupportedNodeError); + if (!(thrown instanceof UnsupportedNodeError)) + throw new Error("unreachable"); + expect(thrown.name).toBe("UnsupportedNodeError"); + expect(thrown.nodeKind).toBe("treeReference"); + expect(thrown.path).toBe("$.right"); + expect(thrown.message).toContain( + "cannot compile 'treeReference' at $.right", + ); + }); + + it("emits nothing at all when it refuses", () => { + // The refusal is total: no partial fragment, no partially-populated parameter list, nothing a caller could mistake for a usable result. + expect(() => + compile({ + kind: "allOf", + operands: [ageOver, { kind: "some", collection: "xs", item: ageOver }], + }), + ).toThrow(UnsupportedNodeError); + }); + + it.each([ + ["some", { kind: "some", collection: "xs", item: ageOver }] satisfies [ + string, + PredicateNode, + ], + ["every", { kind: "every", collection: "xs", item: ageOver }] satisfies [ + string, + PredicateNode, + ], + [ + "fold", + { + kind: "compare", + op: "gt", + left: { + kind: "fold", + collection: "xs", + combiner: { + mode: "max", + item: { kind: "numberLiteral", value: LOWER_BOUND }, + }, + }, + right: { kind: "numberLiteral", value: UPPER_BOUND }, + }, + ] satisfies [string, PredicateNode], + ])( + "refuses a '%s' buried several levels down rather than dropping that branch", + (kind, unsupported) => { + // The failure mode this rules out is the dangerous one: a branch the compiler has no translation for quietly contributing nothing to the fragment, leaving a WHERE clause strictly more permissive than the tree it claims to stand for. The burial is deliberate -- under an `and`, then an `anyOf`, then a `not` -- because a check that only looks at the root would pass every one of these. + let thrown: unknown; + try { + compile({ + kind: "and", + left: ageOver, + right: { + kind: "anyOf", + operands: [ + { kind: "exists", operand: { kind: "reference", key: "note" } }, + { kind: "not", operand: unsupported }, + ], + }, + }); + } catch (error) { + thrown = error; + } + + expect(thrown).toBeInstanceOf(UnsupportedNodeError); + if (!(thrown instanceof UnsupportedNodeError)) + throw new Error("unreachable"); + expect(thrown.nodeKind).toBe(kind); + expect(thrown.path).toContain("$.right.operands[1].operand"); + }, + ); +}); diff --git a/packages/trilean-sql/src/compile.ts b/packages/trilean-sql/src/compile.ts new file mode 100644 index 0000000..8c61c13 --- /dev/null +++ b/packages/trilean-sql/src/compile.ts @@ -0,0 +1,234 @@ +import type { + ComparisonOperator, + ExpressionNode, + PredicateNode, + TextComparisonOperator, +} from "trilean"; +import { InvalidColumnError, UnsupportedNodeError } from "./errors"; +import { findUnpushableNodeKind } from "./guard"; +import type { + CompiledSql, + SqlColumnBinding, + SqlCompileOptions, + SqlParamType, +} from "./options"; +import { POSTGRES_TYPE_NAME } from "./options"; + +const COMPARISON_SQL: Readonly> = { + gt: ">", + gte: ">=", + lt: "<", + lte: "<=", + eq: "=", + neq: "<>", +}; + +/** `~` and `!~` are PostgreSQL's own regular-expression match operators, so a pattern is matched by the database rather than shipped back to be matched in process. See the "Regular expressions" caveat in README.md: PostgreSQL's advanced regular expressions and ECMAScript's are close but not the same language. */ +const TEXT_COMPARISON_SQL: Readonly> = { + equals: "=", + notEquals: "<>", + matches: "~", + notMatches: "!~", +}; + +/** + * The SQL type a literal placeholder is cast to: the one implied by the literal's own trilean kind. + * + * Casting every placeholder means a fragment's meaning never depends on how a particular driver decided to infer an untyped parameter, and it is what makes a comparison between two literals (`$1 < $2`, which PostgreSQL rejects outright as having undeterminable parameter types) compile to something executable at all. + * + * A mapped column's declared `paramType` deliberately does not override this. It cannot differ: the guard has already refused any comparison whose operand kinds disagree, and each declared kind maps to the same PostgreSQL type as the literal kind it must then match. Consulting it here would be a branch that can never change the output. + */ +const PARAM_TYPE_OF_LITERAL: Readonly< + Record< + "textLiteral" | "numberLiteral" | "booleanLiteral" | "instantLiteral", + SqlParamType + > +> = { + textLiteral: "text", + numberLiteral: "number", + booleanLiteral: "boolean", + instantLiteral: "timestamp", +}; + +interface CompileContext { + readonly options: SqlCompileOptions; + readonly params: unknown[]; +} + +function placeholder( + context: CompileContext, + value: unknown, + castTo: SqlParamType, +): string { + context.params.push(value); + return `$${String(context.params.length)}::${POSTGRES_TYPE_NAME[castTo]}`; +} + +/** + * Renders a column as a PostgreSQL identifier: each dot-separated segment double-quoted, with any embedded double quote doubled. + * + * A column name cannot be a bind parameter -- it is part of the statement's structure, not its data -- so it is the one caller-supplied string that reaches the SQL text. Quoting it unconditionally is what keeps that safe: the doubling makes even a name containing `"; DROP TABLE ...` a single, inert identifier that simply does not exist. Quoting also means a name is taken literally rather than case-folded, so `columnFor` must return the column's real, case-exact name. + */ +function quoteColumn( + referenceKey: string, + binding: Readonly, +): string { + const segments = binding.column.split("."); + if (segments.some((segment) => segment.length === 0)) { + throw new InvalidColumnError( + referenceKey, + binding.column, + binding.column.length === 0 + ? "the name is empty" + : "a dot-separated segment is empty", + ); + } + return segments + .map((segment) => `"${segment.replaceAll('"', '""')}"`) + .join("."); +} + +function bindingOf( + context: CompileContext, + node: ExpressionNode, +): { referenceKey: string; binding: SqlColumnBinding } | undefined { + if (node.kind !== "reference" || typeof node.key !== "string") { + return undefined; + } + return { + referenceKey: node.key, + binding: context.options.columnFor(node.key), + }; +} + +/** + * The compiler's own refusal, as distinct from the guard's. + * + * `compilePredicateNode` runs the guard first, so in a correct build nothing reaches here: every kind named in the unsupported branches below has already been reported with a real path and a real reason. These branches exist so that a disagreement between the guard's allow-list and this file's coverage -- the one way a node kind could ever be silently mistranslated -- is instead a loud, named failure. Every kind is spelled out rather than caught by a `default`, so adding one to trilean breaks this switch at compile time instead of falling into a catch-all. + */ +function refuse(kind: string, layer: "expression" | "predicate"): never { + throw new UnsupportedNodeError({ + kind, + path: "$", + reason: `the ${layer} passed the pushability check but has no compiler branch`, + }); +} + +function compileExpression( + node: ExpressionNode, + context: CompileContext, +): string { + switch (node.kind) { + case "reference": { + const resolved = bindingOf(context, node); + if (resolved === undefined) return refuse(node.kind, "expression"); + return quoteColumn(resolved.referenceKey, resolved.binding); + } + case "textLiteral": + case "numberLiteral": + case "booleanLiteral": + case "instantLiteral": + return placeholder(context, node.value, PARAM_TYPE_OF_LITERAL[node.kind]); + case "durationLiteral": + case "complexLiteral": + case "arithmetic": + case "negate": + case "call": + case "lookup": + case "conditional": + case "fold": + case "accumulator": + case "delegate": + case "treeReference": + break; + } + return refuse(node.kind, "expression"); +} + +function compilePredicate( + node: PredicateNode, + context: CompileContext, +): string { + switch (node.kind) { + case "not": + return `(NOT ${compilePredicate(node.operand, context)})`; + case "and": + return `(${compilePredicate(node.left, context)} AND ${compilePredicate(node.right, context)})`; + case "or": + return `(${compilePredicate(node.left, context)} OR ${compilePredicate(node.right, context)})`; + case "allOf": + case "anyOf": { + // An empty operand list is each connective's own identity, matching the evaluator exactly: `allOf` folds from `definite(true)` and `anyOf` from `definite(false)`. + if (node.operands.length === 0) { + return node.kind === "allOf" ? "(TRUE)" : "(FALSE)"; + } + const joiner = node.kind === "allOf" ? " AND " : " OR "; + return `(${node.operands.map((operand) => compilePredicate(operand, context)).join(joiner)})`; + } + case "compare": { + const left = compileExpression(node.left, context); + const right = compileExpression(node.right, context); + return `(${left} ${COMPARISON_SQL[node.op]} ${right})`; + } + case "textCompare": { + const left = compileExpression(node.left, context); + const right = compileExpression(node.right, context); + return `(${left} ${TEXT_COMPARISON_SQL[node.op]} ${right})`; + } + case "memberOf": { + const operand = compileExpression(node.operand, context); + if (node.candidates.length === 0) { + // `IN ()` is a syntax error, and the two constants it would be tempting to fold to are both wrong: an empty `in` is false and an empty `notIn` is true only once the operand itself is known, and stay unknown while it is NULL. These two forms reproduce that exactly -- `NULL IS NULL AND NULL` is NULL while ` IS NULL AND NULL` is FALSE, and the `notIn` form is its mirror image -- which a bare FALSE/TRUE would not, most visibly under a surrounding NOT. + return node.op === "in" + ? `(${operand} IS NULL AND NULL::boolean)` + : `(${operand} IS NOT NULL OR NULL::boolean)`; + } + const candidates = node.candidates + .map((candidate) => compileExpression(candidate, context)) + .join(", "); + return `(${operand} ${node.op === "in" ? "IN" : "NOT IN"} (${candidates}))`; + } + case "exists": + // `exists` is the one predicate trilean never returns indeterminate for, and `IS NOT NULL` is likewise the one comparison SQL never returns NULL from -- so this is an exact translation rather than a NULL-propagating one, and a NULL column under `exists` is FALSE here just as an unresolved reference is `definite(false)` there. + return `(${compileExpression(node.operand, context)} IS NOT NULL)`; + case "some": + case "every": + case "treeReference": + break; + } + return refuse(node.kind, "predicate"); +} + +/** + * Compiles a trilean predicate tree into a parameterised PostgreSQL boolean expression. + * + * Three-valued logic is not reimplemented on top of SQL; it is delegated to it. SQL's `AND`, `OR` and `NOT` over `TRUE`/`FALSE`/`NULL` are Kleene's strong three-valued tables, which are the same tables trilean's own `combineAnd`, `combineOr` and `not` implement, and a comparison against a NULL column yields `NULL` exactly where the evaluator would have returned `indeterminate` from an unresolved reference. A row excluded by `WHERE` because its condition was unknown is therefore excluded for the same reason, and by the same rule, as a subject the evaluator declines to judge. No indeterminacy column, sentinel value or `CASE` scaffolding is emitted, because none is needed. + * + * Every caller-supplied literal becomes a bind parameter. Nothing but structure, operators, and quoted column identifiers is ever written into the returned `sql`. + * + * @throws {UnsupportedNodeError} if any node in the tree is one this compiler will not translate -- see `findUnpushableNodeKind`, which this runs first and which a caller can run itself to choose between pushdown and in-process evaluation without provoking an exception. + * @throws {InvalidColumnError} if `columnFor` returns a column that cannot be rendered as an identifier. + */ +export function compilePredicateNode( + node: PredicateNode, + options: Readonly, +): CompiledSql { + // `columnFor` is called by the guard walk and again while compiling, so it is memoised for the duration of one compilation -- a caller's mapping may be a lookup of real cost, and it must not matter how many times the compiler happens to ask. + const bindings = new Map(); + const memoised: SqlCompileOptions = { + dialect: options.dialect, + columnFor: (referenceKey) => { + const cached = bindings.get(referenceKey); + if (cached !== undefined) return cached; + const binding = options.columnFor(referenceKey); + bindings.set(referenceKey, binding); + return binding; + }, + }; + + const unpushable = findUnpushableNodeKind(node, memoised); + if (unpushable !== undefined) throw new UnsupportedNodeError(unpushable); + + const context: CompileContext = { options: memoised, params: [] }; + return { sql: compilePredicate(node, context), params: context.params }; +} diff --git a/packages/trilean-sql/src/errors.ts b/packages/trilean-sql/src/errors.ts new file mode 100644 index 0000000..4818a60 --- /dev/null +++ b/packages/trilean-sql/src/errors.ts @@ -0,0 +1,55 @@ +/** Base class for every error this package raises, so a caller can distinguish a compilation failure from an error thrown by its own `columnFor` (which is called during compilation and whose exceptions propagate unchanged). */ +export class TrileanSqlError extends Error { + constructor(message: string) { + super(message); + this.name = "TrileanSqlError"; + } +} + +/** + * A node the compiler refuses to translate. The compiler never degrades: there is no "best effort" fragment, no silently dropped conjunct, and no approximation that answers differently from the in-process evaluator. Either the whole tree compiles to SQL that agrees with `evaluatePredicate` on every row, or this is thrown and the caller evaluates in process instead. + * + * That guarantee is about the tree's *structure*, and it stops at two things whose meaning is a property of the operand's own content rather than of any node kind: a `matches`/`notMatches` pattern, which the server matches under PostgreSQL's regular-expression language rather than ECMAScript's, and an `instantLiteral` string the two engines parse differently or that PostgreSQL cannot parse at all (which fails as a query error at execution time, not as this exception). Both are covered in README.md; neither is detectable by a walk over node kinds. + * + * `nodeKind` is the offending node's own `kind`, `path` locates it inside the tree, and `reason` says why it is not pushable -- which is not always about the kind alone (a `reference` carrying a `unit`, or a `compare` whose operands are statically of different kinds, are both refused despite `reference` and `compare` being supported kinds). + */ +export class UnsupportedNodeError extends TrileanSqlError { + readonly nodeKind: string; + readonly path: string; + readonly reason: string; + + constructor( + unpushable: Readonly<{ + kind: string; + path: string; + reason: string; + }>, + ) { + super( + `cannot compile '${unpushable.kind}' at ${unpushable.path}: ${unpushable.reason}`, + ); + this.name = "UnsupportedNodeError"; + this.nodeKind = unpushable.kind; + this.path = unpushable.path; + this.reason = unpushable.reason; + } +} + +/** + * A `columnFor` result whose `column` cannot be rendered as a PostgreSQL identifier. + * + * A column name is an identifier, not a value, so it is the one part of the emitted SQL that cannot be parameterised -- it has to be written into the statement text. It is always emitted double-quoted with any embedded quote doubled, which makes an arbitrary string safe, so this is not the injection defence; it rejects the two shapes that quoting cannot rescue into a valid identifier, an empty name and an empty dot-separated segment. + */ +export class InvalidColumnError extends TrileanSqlError { + readonly referenceKey: string; + readonly column: string; + + constructor(referenceKey: string, column: string, reason: string) { + super( + `columnFor(${JSON.stringify(referenceKey)}) returned ${JSON.stringify(column)}, which is not a usable column identifier: ${reason}`, + ); + this.name = "InvalidColumnError"; + this.referenceKey = referenceKey; + this.column = column; + } +} diff --git a/packages/trilean-sql/src/guard.test.ts b/packages/trilean-sql/src/guard.test.ts new file mode 100644 index 0000000..0d6a6af --- /dev/null +++ b/packages/trilean-sql/src/guard.test.ts @@ -0,0 +1,333 @@ +import type { ExpressionNode, PredicateNode } from "trilean"; +import { describe, expect, it, vi } from "vitest"; +import { findUnpushableNodeKind } from "./guard"; +import { subjectOptions } from "./test-support/columns"; + +const ageOver: PredicateNode = { + kind: "compare", + op: "gt", + left: { kind: "reference", key: "age" }, + right: { kind: "numberLiteral", value: 18 }, +}; + +describe("supported trees", () => { + it("passes a tree built only from the kinds the compiler translates", () => { + const node: PredicateNode = { + kind: "allOf", + operands: [ + { kind: "not", operand: ageOver }, + { + kind: "or", + left: { kind: "exists", operand: { kind: "reference", key: "note" } }, + right: { + kind: "textCompare", + op: "matches", + left: { kind: "reference", key: "name" }, + right: { kind: "textLiteral", value: "^a" }, + }, + }, + { + kind: "memberOf", + op: "notIn", + operand: { kind: "reference", key: "age" }, + candidates: [{ kind: "numberLiteral", value: 3 }], + }, + ], + }; + + expect(findUnpushableNodeKind(node, subjectOptions)).toBeUndefined(); + }); + + it("does not consult columnFor when called without options", () => { + const columnFor = vi.fn(() => ({ column: "age" })); + expect(findUnpushableNodeKind(ageOver, undefined)).toBeUndefined(); + expect(columnFor).not.toHaveBeenCalled(); + }); +}); + +describe("predicate kinds this version does not translate", () => { + it.each([ + [ + "some", + { kind: "some", collection: "xs", item: ageOver } satisfies PredicateNode, + ], + [ + "every", + { + kind: "every", + collection: "xs", + item: ageOver, + } satisfies PredicateNode, + ], + [ + "treeReference", + { kind: "treeReference", key: "other" } satisfies PredicateNode, + ], + ])("refuses '%s'", (kind, node) => { + expect(findUnpushableNodeKind(node, subjectOptions)).toMatchObject({ + kind, + path: "$", + }); + }); + + it("reports the path of a refused node nested inside the tree", () => { + expect( + findUnpushableNodeKind( + { + kind: "allOf", + operands: [ + ageOver, + { kind: "not", operand: { kind: "treeReference", key: "other" } }, + ], + }, + subjectOptions, + ), + ).toMatchObject({ kind: "treeReference", path: "$.operands[1].operand" }); + }); +}); + +describe("expression kinds this version does not translate", () => { + const unsupported: readonly [string, ExpressionNode][] = [ + ["durationLiteral", { kind: "durationLiteral", value: 5, unit: "min" }], + ["complexLiteral", { kind: "complexLiteral", re: 1, im: 2 }], + [ + "arithmetic", + { + kind: "arithmetic", + op: "add", + left: { kind: "numberLiteral", value: 1 }, + right: { kind: "numberLiteral", value: 2 }, + }, + ], + [ + "negate", + { kind: "negate", operand: { kind: "numberLiteral", value: 1 } }, + ], + ["call", { kind: "call", fn: "round", args: [] }], + ["lookup", { kind: "lookup", table: "rates", keys: [] }], + [ + "conditional", + { + kind: "conditional", + cases: [], + fallback: { kind: "numberLiteral", value: 0 }, + }, + ], + [ + "fold", + { + kind: "fold", + collection: "xs", + combiner: { mode: "max", item: { kind: "numberLiteral", value: 1 } }, + }, + ], + ["accumulator", { kind: "accumulator" }], + ["delegate", { kind: "delegate", system: "legacy", payload: null }], + ["treeReference", { kind: "treeReference", key: "other" }], + ]; + + it.each(unsupported)("refuses '%s' in a comparison operand", (kind, node) => { + expect( + findUnpushableNodeKind( + { + kind: "compare", + op: "eq", + left: { kind: "reference", key: "age" }, + right: node, + }, + subjectOptions, + ), + ).toMatchObject({ kind, path: "$.right" }); + }); +}); + +describe("references the compiler cannot map", () => { + it("refuses a non-string reference key", () => { + expect( + findUnpushableNodeKind( + { + kind: "exists", + operand: { kind: "reference", key: { nested: "key" } }, + }, + subjectOptions, + ), + ).toMatchObject({ kind: "reference", path: "$.operand" }); + }); + + it("refuses a reference declaring a unit, which no column can be checked against", () => { + expect( + findUnpushableNodeKind( + { + kind: "compare", + op: "gt", + left: { kind: "reference", key: "age", unit: { year: 1 } }, + right: { kind: "numberLiteral", value: 18 }, + }, + subjectOptions, + ), + ).toMatchObject({ kind: "reference", path: "$.left" }); + }); + + it("refuses a unit-tagged number literal", () => { + expect( + findUnpushableNodeKind( + { + kind: "compare", + op: "gt", + left: { kind: "reference", key: "age" }, + right: { kind: "numberLiteral", value: 18, unit: { year: 1 } }, + }, + subjectOptions, + ), + ).toMatchObject({ kind: "numberLiteral", path: "$.right" }); + }); + + it("refuses a NaN number literal, which the two engines compare oppositely", () => { + expect( + findUnpushableNodeKind( + { + kind: "compare", + op: "eq", + left: { kind: "reference", key: "age" }, + right: { kind: "numberLiteral", value: Number.NaN }, + }, + subjectOptions, + ), + ).toMatchObject({ kind: "numberLiteral", path: "$.right" }); + }); + + it("refuses a NaN candidate inside a memberOf, not only a comparison operand", () => { + expect( + findUnpushableNodeKind( + { + kind: "memberOf", + op: "in", + operand: { kind: "reference", key: "age" }, + candidates: [ + { kind: "numberLiteral", value: 1 }, + { kind: "numberLiteral", value: Number.NaN }, + ], + }, + subjectOptions, + ), + ).toMatchObject({ kind: "numberLiteral", path: "$.candidates[1]" }); + }); + + it("allows an infinity, which both engines order and compare identically", () => { + expect( + findUnpushableNodeKind( + { + kind: "compare", + op: "gt", + left: { kind: "numberLiteral", value: Number.POSITIVE_INFINITY }, + right: { kind: "reference", key: "age" }, + }, + subjectOptions, + ), + ).toBeUndefined(); + }); +}); + +describe("operand kinds trilean and PostgreSQL would answer differently", () => { + it("refuses a compare whose operands are of different declared kinds", () => { + expect( + findUnpushableNodeKind( + { + kind: "compare", + op: "eq", + left: { kind: "reference", key: "age" }, + right: { kind: "instantLiteral", value: "2020-01-01T00:00:00Z" }, + }, + subjectOptions, + ), + ).toMatchObject({ kind: "compare", path: "$" }); + }); + + it("refuses a compare against text, which trilean directs to textCompare", () => { + expect( + findUnpushableNodeKind( + { + kind: "compare", + op: "eq", + left: { kind: "reference", key: "name" }, + right: { kind: "textLiteral", value: "ada" }, + }, + subjectOptions, + ), + ).toMatchObject({ kind: "compare", path: "$" }); + }); + + it("refuses an ordering comparison on booleans, which trilean has no order for", () => { + expect( + findUnpushableNodeKind( + { + kind: "compare", + op: "gt", + left: { kind: "reference", key: "active" }, + right: { kind: "booleanLiteral", value: false }, + }, + subjectOptions, + ), + ).toMatchObject({ kind: "compare", path: "$" }); + }); + + it("allows equality on booleans, which trilean does define", () => { + expect( + findUnpushableNodeKind( + { + kind: "compare", + op: "eq", + left: { kind: "reference", key: "active" }, + right: { kind: "booleanLiteral", value: false }, + }, + subjectOptions, + ), + ).toBeUndefined(); + }); + + it("refuses a textCompare against a non-text operand", () => { + expect( + findUnpushableNodeKind( + { + kind: "textCompare", + op: "equals", + left: { kind: "reference", key: "age" }, + right: { kind: "textLiteral", value: "18" }, + }, + subjectOptions, + ), + ).toMatchObject({ kind: "textCompare", path: "$" }); + }); + + it("refuses a memberOf whose candidates are not all of the operand's kind", () => { + expect( + findUnpushableNodeKind( + { + kind: "memberOf", + op: "in", + operand: { kind: "reference", key: "age" }, + candidates: [ + { kind: "numberLiteral", value: 1 }, + { kind: "textLiteral", value: "two" }, + ], + }, + subjectOptions, + ), + ).toMatchObject({ kind: "memberOf", path: "$" }); + }); + + it("cannot detect a mismatch against a column with no declared paramType", () => { + // Not a gap to fix by guessing: without a declared type there is nothing to compare the literal's kind against. It is the concrete reason to declare paramType, and stating it as a test keeps the limitation deliberate. + expect( + findUnpushableNodeKind( + { + kind: "compare", + op: "gt", + left: { kind: "reference", key: "note" }, + right: { kind: "numberLiteral", value: 1 }, + }, + subjectOptions, + ), + ).toBeUndefined(); + }); +}); diff --git a/packages/trilean-sql/src/guard.ts b/packages/trilean-sql/src/guard.ts new file mode 100644 index 0000000..0c138b8 --- /dev/null +++ b/packages/trilean-sql/src/guard.ts @@ -0,0 +1,346 @@ +import type { ExpressionNode, PredicateNode } from "trilean"; +import type { SqlCompileOptions, SqlParamType } from "./options"; + +/** Where the walk stopped, and why. `kind` is the offending node's own `kind` even when the objection is not to the kind itself. */ +export interface UnpushableNode { + kind: string; + /** A dotted path from the root of the tree, e.g. `$.operands[1].left`. */ + path: string; + reason: string; +} + +/** + * trilean's own kind for a value, as far as it can be determined without running anything: from a literal's own node kind, or from a mapped column's declared `paramType`. `undefined` means the value's kind is only knowable at execution time, which is the normal case for a column the caller chose not to describe. + * + * This exists because several of trilean's comparison rules are about the operands' kinds rather than their values, and PostgreSQL's rules for the same operand pair are different. Where both kinds are knowable, the difference is detectable at compile time and refused; where one is not, it is not detectable at all, which is the honest reason to declare `paramType`. + */ +type StaticValueKind = "text" | "number" | "boolean" | "instant"; + +const STATIC_KIND_OF_PARAM_TYPE: Readonly< + Record +> = { + text: "text", + number: "number", + boolean: "boolean", + timestamp: "instant", +}; + +/** The comparison operators that impose an ordering, as opposed to testing equality. trilean refuses these on booleans -- `true > false` is not a fact about the domain -- while PostgreSQL answers them, so a boolean operand under one of these is a real divergence rather than a stylistic one. */ +const ORDERING_OPERATORS: ReadonlySet = new Set([ + "gt", + "gte", + "lt", + "lte", +]); + +/** A literal's own node kind is its value's kind. Written as a lookup rather than a switch so that every other expression kind falls out as "not statically knowable" by absence, which is the honest answer for all of them, rather than needing a branch each. */ +const STATIC_KIND_OF_LITERAL: Readonly> = { + textLiteral: "text", + numberLiteral: "number", + booleanLiteral: "boolean", + instantLiteral: "instant", +}; + +function staticValueKindOf( + node: ExpressionNode, + options: SqlCompileOptions | undefined, +): StaticValueKind | undefined { + if (node.kind !== "reference") return STATIC_KIND_OF_LITERAL[node.kind]; + if (options === undefined || typeof node.key !== "string") return undefined; + const paramType = options.columnFor(node.key).paramType; + return paramType === undefined + ? undefined + : STATIC_KIND_OF_PARAM_TYPE[paramType]; +} + +function findUnpushableExpression( + node: ExpressionNode, + path: string, +): UnpushableNode | undefined { + // Read before the switch narrows `node` to `never` in its default branch, where the kind is still what the report needs to name. + const unrecognisedKind: string = node.kind; + switch (node.kind) { + case "reference": { + if (typeof node.key !== "string") { + return { + kind: node.kind, + path, + reason: + "only a string reference key can be mapped to a column; this key is a non-string JSON value", + }; + } + if (node.unit !== undefined) { + return { + kind: node.kind, + path, + reason: + "a reference declaring a unit asserts that the resolved value carries that same unit, and a SQL column carries no unit for that assertion to be checked against", + }; + } + return undefined; + } + case "numberLiteral": + if (node.unit !== undefined) { + return { + kind: node.kind, + path, + reason: + "a unit-tagged number is only comparable with an operand of the same unit, and SQL has no unit to compare", + }; + } + // NaN is the one double-precision value the two engines disagree about. trilean compares numbers with `===`, under which NaN is equal to nothing including itself; PostgreSQL defines NaN as equal to itself and greater than every other double, so `NaN = NaN` there is TRUE and selects every row of a table this predicate should have matched none of. Infinities are deliberately not refused alongside it: both engines order them identically and compare them equal to themselves, so they translate faithfully. Reachable despite `NumberLiteralNodeSchema` rejecting NaN, because this compiler's input is the inferred `PredicateNode` type -- TypeScript's `number` includes NaN -- and a tree built in code rather than parsed never meets that schema. + if (Number.isNaN(node.value)) { + return { + kind: node.kind, + path, + reason: + "NaN is equal to nothing in trilean, not even itself, whereas PostgreSQL defines NaN as equal to itself and greater than every other double", + }; + } + return undefined; + case "textLiteral": + case "booleanLiteral": + case "instantLiteral": + return undefined; + case "durationLiteral": + return { + kind: node.kind, + path, + reason: + "trilean compares durations by normalising both operands to milliseconds, which has no column-level equivalent to normalise against", + }; + case "complexLiteral": + return { + kind: node.kind, + path, + reason: "PostgreSQL has no complex number type", + }; + case "arithmetic": + case "negate": + return { + kind: node.kind, + path, + reason: + "arithmetic carries and combines units, and pushing it down would drop that dimensional analysis without saying so", + }; + case "call": + return { + kind: node.kind, + path, + reason: + "a function's implementation lives in the caller's FunctionRegistry, not in the database", + }; + case "lookup": + return { + kind: node.kind, + path, + reason: "a lookup table is resolved by the caller's resolvers", + }; + case "conditional": + return { + kind: node.kind, + path, + reason: + "conditional evaluation is not implemented in this version of the compiler", + }; + case "fold": + return { + kind: node.kind, + path, + reason: + "a fold ranges over a collection the caller's resolvers supply, which is not this query's row set", + }; + case "accumulator": + return { + kind: node.kind, + path, + reason: "an accumulator is only meaningful inside a reduce fold", + }; + case "delegate": + return { + kind: node.kind, + path, + reason: "a delegated decision is made by an external system", + }; + case "treeReference": + return { + kind: node.kind, + path, + reason: + "a referenced tree is resolved by the caller's resolvers; inline it before compiling", + }; + default: { + // Unreachable while the switch above covers every ExpressionNode kind, and the assertion is what makes a kind added to trilean later a compile error here rather than a silent fall-through. The branch still returns rather than throwing, so a tree built by an older or newer trilean than this package was compiled against is reported as unpushable instead of crashing the walk. + node satisfies never; + return { + kind: unrecognisedKind, + path, + reason: "unrecognised expression node kind", + }; + } + } +} + +/** Checks the operands of a comparison-shaped predicate for a kind pairing trilean and PostgreSQL would answer differently. Returns the objection against `path` itself, since the divergence is a property of the pairing rather than of either operand alone. */ +function findKindDivergence( + kind: string, + path: string, + operands: readonly { node: ExpressionNode; path: string }[], + options: SqlCompileOptions | undefined, +): UnpushableNode | undefined { + const kinds = operands.map((operand) => ({ + path: operand.path, + staticKind: staticValueKindOf(operand.node, options), + })); + const known = kinds.filter( + (entry): entry is { path: string; staticKind: StaticValueKind } => + entry.staticKind !== undefined, + ); + const first = known[0]; + if (first === undefined) return undefined; + const mismatched = known.find( + (entry) => entry.staticKind !== first.staticKind, + ); + if (mismatched !== undefined) { + return { + kind, + path, + reason: `trilean treats a comparison between a '${first.staticKind}' value (${first.path}) and a '${mismatched.staticKind}' value (${mismatched.path}) as wrong-type, whereas PostgreSQL may coerce one to the other and answer definitely`, + }; + } + return undefined; +} + +function findUnpushablePredicate( + node: PredicateNode, + path: string, + options: SqlCompileOptions | undefined, +): UnpushableNode | undefined { + const unrecognisedKind: string = node.kind; + switch (node.kind) { + case "not": + return findUnpushablePredicate(node.operand, `${path}.operand`, options); + case "and": + case "or": + return ( + findUnpushablePredicate(node.left, `${path}.left`, options) ?? + findUnpushablePredicate(node.right, `${path}.right`, options) + ); + case "allOf": + case "anyOf": { + for (const [index, operand] of node.operands.entries()) { + const unpushable = findUnpushablePredicate( + operand, + `${path}.operands[${String(index)}]`, + options, + ); + if (unpushable !== undefined) return unpushable; + } + return undefined; + } + case "compare": { + const operands = [ + { node: node.left, path: `${path}.left` }, + { node: node.right, path: `${path}.right` }, + ]; + for (const operand of operands) { + const unpushable = findUnpushableExpression(operand.node, operand.path); + if (unpushable !== undefined) return unpushable; + } + const divergence = findKindDivergence(node.kind, path, operands, options); + if (divergence !== undefined) return divergence; + for (const operand of operands) { + const staticKind = staticValueKindOf(operand.node, options); + if (staticKind === "text") { + return { + kind: node.kind, + path, + reason: `'compare' never compares text in trilean -- it returns wrong-type and directs the caller to 'textCompare' -- but PostgreSQL would order the text operand at ${operand.path} collation-wise and answer definitely`, + }; + } + if (staticKind === "boolean" && ORDERING_OPERATORS.has(node.op)) { + return { + kind: node.kind, + path, + reason: `booleans have no ordering in trilean, so '${node.op}' against the boolean operand at ${operand.path} is wrong-type there, whereas PostgreSQL orders false before true`, + }; + } + } + return undefined; + } + case "textCompare": { + const operands = [ + { node: node.left, path: `${path}.left` }, + { node: node.right, path: `${path}.right` }, + ]; + for (const operand of operands) { + const unpushable = findUnpushableExpression(operand.node, operand.path); + if (unpushable !== undefined) return unpushable; + const staticKind = staticValueKindOf(operand.node, options); + if (staticKind !== undefined && staticKind !== "text") { + return { + kind: node.kind, + path, + reason: `'textCompare' requires text operands in trilean, and the operand at ${operand.path} is a '${staticKind}' value`, + }; + } + } + return undefined; + } + case "memberOf": { + const operands = [ + { node: node.operand, path: `${path}.operand` }, + ...node.candidates.map((candidate, index) => ({ + node: candidate, + path: `${path}.candidates[${String(index)}]`, + })), + ]; + for (const operand of operands) { + const unpushable = findUnpushableExpression(operand.node, operand.path); + if (unpushable !== undefined) return unpushable; + } + return findKindDivergence(node.kind, path, operands, options); + } + case "exists": + return findUnpushableExpression(node.operand, `${path}.operand`); + case "some": + case "every": + return { + kind: node.kind, + path, + reason: + "quantification ranges over a collection the caller's resolvers supply, which is not this query's row set", + }; + case "treeReference": + return { + kind: node.kind, + path, + reason: + "a referenced tree is resolved by the caller's resolvers; inline it before compiling", + }; + default: { + node satisfies never; + return { + kind: unrecognisedKind, + path, + reason: "unrecognised predicate node kind", + }; + } + } +} + +/** + * Walks the tree against the allow-list of what this compiler translates, and reports the first node it will not. + * + * The walk is an allow-list rather than a deny-list on purpose: a node kind added to trilean after this package was written is refused by default, instead of falling through to whatever branch happened to be last. That is the difference between a caller learning it must evaluate in process and a caller silently receiving a `WHERE` clause that answers a different question. + * + * `compilePredicateNode` runs this first and throws `UnsupportedNodeError` on any result, so calling it separately is only necessary to *decide* between pushdown and in-process evaluation without provoking an exception. + * + * Passing `options` widens the check: without them the walk is purely structural, and with them it also applies the operand-kind rules that depend on each mapped column's declared `paramType` (and therefore calls `columnFor`). + */ +export function findUnpushableNodeKind( + node: PredicateNode, + options?: SqlCompileOptions, +): UnpushableNode | undefined { + return findUnpushablePredicate(node, "$", options); +} diff --git a/packages/trilean-sql/src/index.ts b/packages/trilean-sql/src/index.ts new file mode 100644 index 0000000..6fbd956 --- /dev/null +++ b/packages/trilean-sql/src/index.ts @@ -0,0 +1,17 @@ +export type { + CompiledSql, + SqlColumnBinding, + SqlCompileOptions, + SqlParamType, +} from "./options"; + +export type { UnpushableNode } from "./guard"; +export { findUnpushableNodeKind } from "./guard"; + +export { compilePredicateNode } from "./compile"; + +export { + InvalidColumnError, + TrileanSqlError, + UnsupportedNodeError, +} from "./errors"; diff --git a/packages/trilean-sql/src/options.ts b/packages/trilean-sql/src/options.ts new file mode 100644 index 0000000..c8725df --- /dev/null +++ b/packages/trilean-sql/src/options.ts @@ -0,0 +1,40 @@ +/** The declared SQL value kind of a mapped column. `"timestamp"` names trilean's `instant` kind, whose values are ISO-8601 strings. */ +export type SqlParamType = "text" | "number" | "boolean" | "timestamp"; + +/** What a `reference` node's key maps onto in the target schema. */ +export interface SqlColumnBinding { + /** The column, optionally qualified with a table or schema by dots (`"orders.total"`). Each dot-separated segment is emitted as its own double-quoted identifier, so a segment may contain any character except a dot itself. */ + column: string; + /** + * The column's value kind, if the caller knows it. Optional, and worth supplying: it is the only thing that lets the compiler check a comparison against the column for the kind mismatches trilean itself treats as `wrong-type` -- comparing a text column with `compare` rather than `textCompare`, ordering a boolean, comparing a number against an instant. Left undeclared, those comparisons compile, and PostgreSQL may coerce its way to a definite answer where trilean would have returned indeterminate. + * + * It does not affect the emitted SQL, only whether the comparison is emitted at all. Every literal placeholder is already cast to the type its own trilean kind implies, and a comparison that survives the check is one whose operand kinds agree, so there is nothing left for the column's declared kind to change. + */ + paramType?: SqlParamType; +} + +export interface SqlCompileOptions { + /** PostgreSQL is the only dialect implemented. It is a required field rather than a default so that adding a second dialect later is a new value here, not a change of behaviour for callers who never said which one they meant. */ + dialect: "postgres"; + /** + * Maps a `reference` node's key onto a column. Called once per reference occurrence. + * + * Only string reference keys reach it: trilean allows any JSON value as a key, and a non-string one is refused as unpushable before this is called. Throwing from here is how a caller rejects a key it has no column for -- the exception propagates out of `compilePredicateNode` unchanged, rather than being wrapped or swallowed. + */ + columnFor: (referenceKey: string) => SqlColumnBinding; +} + +export interface CompiledSql { + /** A self-contained boolean expression, always parenthesised, suitable for dropping straight into a `WHERE` clause (or a `CHECK`, a `HAVING`, or a filtered index predicate). It carries no `WHERE` keyword of its own. */ + sql: string; + /** Positional parameters, in `$1`-first order, to pass alongside `sql`. Every caller-supplied literal in the tree is here; none is ever written into `sql`. */ + params: unknown[]; +} + +/** The PostgreSQL type each declared value kind is cast to. `timestamptz` rather than `timestamp`: trilean's `instant` is an ISO-8601 string that may carry an offset, and parsing one as a naive `timestamp` would silently discard it. */ +export const POSTGRES_TYPE_NAME: Readonly> = { + text: "text", + number: "double precision", + boolean: "boolean", + timestamp: "timestamptz", +}; diff --git a/packages/trilean-sql/src/test-support/columns.ts b/packages/trilean-sql/src/test-support/columns.ts new file mode 100644 index 0000000..5d4f2f0 --- /dev/null +++ b/packages/trilean-sql/src/test-support/columns.ts @@ -0,0 +1,25 @@ +import type { SqlColumnBinding, SqlCompileOptions } from "../options"; + +/** + * The schema the unit tests and the integration suite both compile against, so a fragment asserted as a string in one is the same fragment executed against a real server in the other. + * + * `age`, `name`, `active` and `joined` each declare a `paramType`; `note` deliberately does not, which is what exercises the compiler's undeclared-column path (no operand-kind checking, and literal placeholders cast by their own kind instead of the column's). + */ +export const SUBJECT_COLUMNS: Readonly> = { + age: { column: "age", paramType: "number" }, + name: { column: "name", paramType: "text" }, + active: { column: "active", paramType: "boolean" }, + joined: { column: "joined", paramType: "timestamp" }, + note: { column: "note" }, +}; + +export const subjectOptions: SqlCompileOptions = { + dialect: "postgres", + columnFor: (referenceKey) => { + const binding = SUBJECT_COLUMNS[referenceKey]; + if (binding === undefined) { + throw new Error(`no column mapped for reference key '${referenceKey}'`); + } + return binding; + }, +}; diff --git a/packages/trilean-sql/test/integration/postgres.test.ts b/packages/trilean-sql/test/integration/postgres.test.ts new file mode 100644 index 0000000..5026da3 --- /dev/null +++ b/packages/trilean-sql/test/integration/postgres.test.ts @@ -0,0 +1,487 @@ +import { + PostgreSqlContainer, + type StartedPostgreSqlContainer, +} from "@testcontainers/postgresql"; +import pg from "pg"; +import type { + ComputedValue, + JsonValue, + PredicateNode, + Resolution, + Resolvers, +} from "trilean"; +import { evaluatePredicate } from "trilean"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { compilePredicateNode } from "../../src/compile"; +import type { SqlCompileOptions } from "../../src/options"; +import { subjectOptions } from "../../src/test-support/columns"; + +/** + * The suite that turns this package's central claim from a design statement into a measured one. + * + * A compiled fragment's three-valued behaviour cannot be established by asserting its text: `("age" > $1)` is only indeterminate-preserving because of what PostgreSQL's planner does with a NULL `age`, and that is a fact about PostgreSQL, not about the string. So every case here compiles a tree, executes the fragment as a real `WHERE` clause against a real server, and compares the rows it returns against the rows trilean's own evaluator judges `definite(true)` for the same tree. Agreement on the rows *and* on their absence is the property under test; a divergence is a compiler bug regardless of what the SQL looks like. + */ + +const SCHEMA = ` + CREATE TABLE subjects ( + id text PRIMARY KEY, + age double precision, + name text, + active boolean, + joined timestamptz, + note text + ); +`; + +interface SubjectRow { + id: string; + age: number | null; + name: string | null; + active: boolean | null; + joined: string | null; + note: string | null; +} + +/** `null` in a column means the same thing as a reference that resolves to nothing: the value is not known. Every row below carries at least one, because a table of fully-populated rows would exercise none of what this suite exists to check. */ +const SUBJECTS: readonly SubjectRow[] = [ + { + id: "ada", + age: 30, + name: "ada", + active: true, + joined: "2020-01-01T00:00:00Z", + note: "hello", + }, + { + id: "grace", + age: 12, + name: "grace", + active: false, + joined: "2024-06-01T12:00:00Z", + note: "hi", + }, + { + id: "lin", + age: null, + name: "lin", + active: true, + joined: "2021-03-03T00:00:00Z", + note: null, + }, + { + id: "unknown", + age: 45, + name: null, + active: null, + joined: null, + note: null, + }, +]; + +/** + * Resolves a reference key against one row, mapping a NULL column to `found: false`. + * + * That mapping is the correspondence the whole design rests on, and stating it in one place here is what makes the parity assertions below meaningful: the evaluator is being given exactly the knowledge PostgreSQL has about the same row, so any disagreement between them is the compiler's, not the fixture's. + */ +function resolversFor(row: Readonly): Resolvers { + const known: Record = { + ...(row.age !== null && { + age: { kind: "number", value: row.age }, + }), + ...(row.name !== null && { name: { kind: "text", value: row.name } }), + ...(row.note !== null && { note: { kind: "text", value: row.note } }), + ...(row.active !== null && { + active: { kind: "boolean", value: row.active }, + }), + ...(row.joined !== null && { + joined: { kind: "instant", value: row.joined }, + }), + }; + + return { + resolveValue: async (key: JsonValue) => { + const value = typeof key === "string" ? known[key] : undefined; + return Promise.resolve( + value === undefined ? { found: false } : { found: true, value }, + ); + }, + resolveLookup: () => { + throw new Error("no tree in this suite uses a lookup"); + }, + resolveCollection: () => { + throw new Error("no tree in this suite uses a collection"); + }, + }; +} + +let container: StartedPostgreSqlContainer; +let client: pg.Client; + +beforeAll(async () => { + container = await new PostgreSqlContainer("postgres:17-alpine").start(); + client = new pg.Client({ connectionString: container.getConnectionUri() }); + await client.connect(); + await client.query(SCHEMA); + for (const row of SUBJECTS) { + await client.query( + "INSERT INTO subjects (id, age, name, active, joined, note) VALUES ($1, $2, $3, $4, $5, $6)", + [row.id, row.age, row.name, row.active, row.joined, row.note], + ); + } +}); + +afterAll(async () => { + await client.end(); + await container.stop(); +}); + +async function selectMatching(node: PredicateNode): Promise { + const compiled = compilePredicateNode(node, subjectOptions); + const result = await client.query<{ id: string }>( + `SELECT id FROM subjects WHERE ${compiled.sql} ORDER BY id`, + compiled.params, + ); + return result.rows.map((row) => row.id); +} + +async function evaluatorMatching(node: PredicateNode): Promise { + const matched: string[] = []; + for (const row of SUBJECTS) { + const evaluation = await evaluatePredicate( + node, + undefined, + resolversFor(row), + ); + if (evaluation.status === "definite" && evaluation.value) { + matched.push(row.id); + } + } + return matched.sort(); +} + +/** Runs the tree both ways and asserts they agree, then hands back the row set so a case can also state what that set should be. Agreement alone would be satisfied by both being wrong in the same way, so every caller asserts the expected ids too. */ +async function agreeingRows(node: PredicateNode): Promise { + const [viaSql, viaEvaluator] = await Promise.all([ + selectMatching(node), + evaluatorMatching(node), + ]); + expect(viaSql).toEqual(viaEvaluator); + return viaSql; +} + +describe("comparisons against a column that can be NULL", () => { + const olderThan18: PredicateNode = { + kind: "compare", + op: "gt", + left: { kind: "reference", key: "age" }, + right: { kind: "numberLiteral", value: 18 }, + }; + + it("excludes the row whose age is unknown", async () => { + await expect(agreeingRows(olderThan18)).resolves.toEqual([ + "ada", + "unknown", + ]); + }); + + it("still excludes it under negation, which two-valued logic could not do", async () => { + // The load-bearing case. Under two-valued logic every row appears in exactly one of a predicate and its negation, so `lin` would have to turn up here. It does not, in either engine: NOT UNKNOWN is UNKNOWN in PostgreSQL exactly as `not(indeterminate)` is indeterminate in trilean. + await expect( + agreeingRows({ kind: "not", operand: olderThan18 }), + ).resolves.toEqual(["grace"]); + }); + + it("keeps a row whose unknown comparison is absorbed by a true disjunct", async () => { + await expect( + agreeingRows({ + kind: "anyOf", + operands: [ + olderThan18, + { + kind: "textCompare", + op: "equals", + left: { kind: "reference", key: "name" }, + right: { kind: "textLiteral", value: "lin" }, + }, + ], + }), + ).resolves.toEqual(["ada", "lin", "unknown"]); + }); + + it("collapses an unknown conjunct absorbed by a false one, observably under negation", async () => { + // `unknown AND false` has to be FALSE rather than UNKNOWN, and the difference only shows through a NOT: a genuinely FALSE conjunction negates to TRUE and the row appears, whereas an UNKNOWN one would negate to UNKNOWN and it would not. `lin` appearing here is that absorption being exercised. + await expect( + agreeingRows({ + kind: "not", + operand: { + kind: "allOf", + operands: [ + olderThan18, + { + kind: "textCompare", + op: "equals", + left: { kind: "reference", key: "name" }, + right: { kind: "textLiteral", value: "nobody" }, + }, + ], + }, + }), + ).resolves.toEqual(["ada", "grace", "lin"]); + }); + + it("compares instants across a NULL", async () => { + await expect( + agreeingRows({ + kind: "compare", + op: "lt", + left: { kind: "reference", key: "joined" }, + right: { kind: "instantLiteral", value: "2022-01-01T00:00:00Z" }, + }), + ).resolves.toEqual(["ada", "lin"]); + }); + + it("compares booleans for equality across a NULL", async () => { + await expect( + agreeingRows({ + kind: "compare", + op: "eq", + left: { kind: "reference", key: "active" }, + right: { kind: "booleanLiteral", value: true }, + }), + ).resolves.toEqual(["ada", "lin"]); + }); +}); + +describe("exists", () => { + const hasNote: PredicateNode = { + kind: "exists", + operand: { kind: "reference", key: "note" }, + }; + + it("partitions the table, because it is the one predicate neither engine leaves unknown", async () => { + const present = await agreeingRows(hasNote); + const absent = await agreeingRows({ kind: "not", operand: hasNote }); + expect(present).toEqual(["ada", "grace"]); + expect(absent).toEqual(["lin", "unknown"]); + expect([...present, ...absent].sort()).toEqual( + SUBJECTS.map((row) => row.id).sort(), + ); + }); +}); + +describe("textCompare", () => { + it("matches a pattern with PostgreSQL's own regular-expression operator", async () => { + await expect( + agreeingRows({ + kind: "textCompare", + op: "matches", + left: { kind: "reference", key: "name" }, + right: { kind: "textLiteral", value: "^(a|g)" }, + }), + ).resolves.toEqual(["ada", "grace"]); + }); + + it("leaves a NULL operand unknown under a negated match", async () => { + await expect( + agreeingRows({ + kind: "textCompare", + op: "notMatches", + left: { kind: "reference", key: "name" }, + right: { kind: "textLiteral", value: "^a" }, + }), + ).resolves.toEqual(["grace", "lin"]); + }); +}); + +describe("memberOf", () => { + it("matches a candidate list", async () => { + await expect( + agreeingRows({ + kind: "memberOf", + op: "in", + operand: { kind: "reference", key: "name" }, + candidates: [ + { kind: "textLiteral", value: "ada" }, + { kind: "textLiteral", value: "nobody" }, + ], + }), + ).resolves.toEqual(["ada"]); + }); + + it("leaves NOT IN unknown for a NULL operand", async () => { + await expect( + agreeingRows({ + kind: "memberOf", + op: "notIn", + operand: { kind: "reference", key: "name" }, + candidates: [{ kind: "textLiteral", value: "ada" }], + }), + ).resolves.toEqual(["grace", "lin"]); + }); + + it("compiles an empty 'in' to something that is false, not unknown, for a known operand", async () => { + // Executed rather than asserted as a string, because `(x IS NULL AND NULL::boolean)` is only the right encoding if PostgreSQL really does evaluate it to FALSE for a known operand and NULL for an unknown one. Negating it is what separates those two outcomes: only the rows with a known name come back. + const node: PredicateNode = { + kind: "memberOf", + op: "in", + operand: { kind: "reference", key: "name" }, + candidates: [], + }; + await expect(agreeingRows(node)).resolves.toEqual([]); + await expect(agreeingRows({ kind: "not", operand: node })).resolves.toEqual( + ["ada", "grace", "lin"], + ); + }); + + it("compiles an empty 'notIn' to something that is true, not unknown, for a known operand", async () => { + const node: PredicateNode = { + kind: "memberOf", + op: "notIn", + operand: { kind: "reference", key: "name" }, + candidates: [], + }; + await expect(agreeingRows(node)).resolves.toEqual(["ada", "grace", "lin"]); + await expect(agreeingRows({ kind: "not", operand: node })).resolves.toEqual( + [], + ); + }); +}); + +describe("degenerate and adversarial fragments", () => { + it("executes a comparison between two literals, which needs both placeholders typed", async () => { + await expect( + agreeingRows({ + kind: "compare", + op: "lt", + left: { kind: "numberLiteral", value: 1 }, + right: { kind: "numberLiteral", value: 2 }, + }), + ).resolves.toEqual(["ada", "grace", "lin", "unknown"]); + }); + + it("executes an empty allOf and anyOf as their identities", async () => { + await expect( + agreeingRows({ kind: "allOf", operands: [] }), + ).resolves.toEqual(["ada", "grace", "lin", "unknown"]); + await expect( + agreeingRows({ kind: "anyOf", operands: [] }), + ).resolves.toEqual([]); + }); + + it("refuses NaN, and measures the divergence that refusal exists to prevent", async () => { + // The refusal is in the guard, so this could have been a unit test -- but a unit test could only assert that NaN is refused, not that refusing it was right. What makes it right is a fact about PostgreSQL: it defines NaN as equal to itself, so `NaN = NaN` there is TRUE and would have selected the whole table, while trilean's `===` makes the same tree match nothing. Both halves are measured here. + const node: PredicateNode = { + kind: "compare", + op: "eq", + left: { kind: "numberLiteral", value: Number.NaN }, + right: { kind: "numberLiteral", value: Number.NaN }, + }; + expect(() => compilePredicateNode(node, subjectOptions)).toThrow( + /cannot compile 'numberLiteral'/, + ); + + const wouldHaveMatched = await client.query<{ id: string }>( + "SELECT id FROM subjects WHERE ($1::double precision = $2::double precision) ORDER BY id", + [Number.NaN, Number.NaN], + ); + expect(wouldHaveMatched.rows.map((row) => row.id)).toEqual( + SUBJECTS.map((row) => row.id).sort(), + ); + await expect(evaluatorMatching(node)).resolves.toEqual([]); + }); + + it("treats an injection attempt as data and leaves the table standing", async () => { + await expect( + agreeingRows({ + kind: "textCompare", + op: "equals", + left: { kind: "reference", key: "name" }, + right: { + kind: "textLiteral", + value: "ada'; DROP TABLE subjects; --", + }, + }), + ).resolves.toEqual([]); + + const surviving = await client.query<{ count: string }>( + "SELECT count(*)::text AS count FROM subjects", + ); + expect(surviving.rows[0]?.count).toBe(String(SUBJECTS.length)); + }); + + it("neutralises a hostile column name into one identifier the server rejects", async () => { + // A column name is the one caller-supplied string that has to reach the SQL text, so quoting is what makes it safe rather than parameterisation. Asserting the quoted string is not the same as establishing that PostgreSQL reads it as a single inert identifier: this executes it, and the server refusing it as a column that does not exist is the proof. The failure it rules out is the opposite outcome -- the injected `OR` taking effect and the fragment matching every row. + const hostile: SqlCompileOptions = { + dialect: "postgres", + columnFor: () => ({ column: `name" = name OR "1` }), + }; + const compiled = compilePredicateNode( + { + kind: "textCompare", + op: "equals", + left: { kind: "reference", key: "name" }, + right: { kind: "textLiteral", value: "ada" }, + }, + hostile, + ); + expect(compiled.sql).toBe(`("name"" = name OR ""1" = $1::text)`); + + await expect( + client.query( + `SELECT id FROM subjects WHERE ${compiled.sql}`, + compiled.params, + ), + ).rejects.toThrow(/does not exist/i); + }); +}); + +describe("a tree deep enough to mix every supported kind", () => { + it("agrees with the evaluator row for row", async () => { + const node: PredicateNode = { + kind: "anyOf", + operands: [ + { + kind: "and", + left: { + kind: "compare", + op: "gte", + left: { kind: "reference", key: "age" }, + right: { kind: "numberLiteral", value: 18 }, + }, + right: { + kind: "not", + operand: { + kind: "memberOf", + op: "in", + operand: { kind: "reference", key: "name" }, + candidates: [{ kind: "textLiteral", value: "grace" }], + }, + }, + }, + { + kind: "allOf", + operands: [ + { kind: "exists", operand: { kind: "reference", key: "note" } }, + { + kind: "or", + left: { + kind: "textCompare", + op: "matches", + left: { kind: "reference", key: "note" }, + right: { kind: "textLiteral", value: "^h" }, + }, + right: { + kind: "compare", + op: "eq", + left: { kind: "reference", key: "active" }, + right: { kind: "booleanLiteral", value: false }, + }, + }, + ], + }, + ], + }; + + await expect(agreeingRows(node)).resolves.toEqual(["ada", "grace"]); + }); +}); diff --git a/packages/trilean-sql/tsconfig.json b/packages/trilean-sql/tsconfig.json new file mode 100644 index 0000000..d882bde --- /dev/null +++ b/packages/trilean-sql/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + // Same isomorphism gate packages/trilean uses: no "node" types and a WebWorker lib, so a Node-only global referenced from runtime src/ fails to compile here rather than at some consumer's runtime. The compiler emits SQL text and a parameter array and touches no I/O, so it has no reason to reach for a platform API at all. + "lib": ["ES2024", "WebWorker"], + "types": [] + }, + "include": ["src"], + "exclude": ["src/**/*.test.ts", "src/test-support/**/*.ts"] +} diff --git a/packages/trilean-sql/tsconfig.node.json b/packages/trilean-sql/tsconfig.node.json new file mode 100644 index 0000000..5780c40 --- /dev/null +++ b/packages/trilean-sql/tsconfig.node.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "lib": ["ES2024"], + "types": ["node"], + "allowImportingTsExtensions": true + }, + "include": [ + "src/**/*.test.ts", + "src/test-support/**/*.ts", + "test/**/*.ts", + "*.config.ts", + "eslint.config.ts" + ], + "exclude": [] +} diff --git a/packages/trilean-sql/tsdown.config.ts b/packages/trilean-sql/tsdown.config.ts new file mode 100644 index 0000000..5b529c6 --- /dev/null +++ b/packages/trilean-sql/tsdown.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: ["src/*.ts", "!src/**/*.test.ts"], + format: ["esm", "cjs"], + dts: true, + platform: "neutral", + clean: true, +}); diff --git a/packages/trilean-sql/vitest.config.ts b/packages/trilean-sql/vitest.config.ts new file mode 100644 index 0000000..569aa81 --- /dev/null +++ b/packages/trilean-sql/vitest.config.ts @@ -0,0 +1,32 @@ +import { defineConfig } from "vitest/config"; + +// Two projects, mirroring packages/trilean's own split: "unit" covers the compiler itself (pure string and parameter production, no I/O), "integration" runs the compiled fragments against a real PostgreSQL server started as an ephemeral container. +// +// The integration project is where the three-valued-logic claim is actually tested rather than asserted: a compiled fragment's truth table is only meaningful once PostgreSQL's own planner has evaluated it against real NULLs, so those tests execute SQL rather than comparing strings. It needs a working Docker daemon (testcontainers), which is why it is a separate project and a separate CI job rather than part of the default `pnpm test`. +export default defineConfig({ + test: { + coverage: { + provider: "v8", + include: ["src/**/*.ts"], + exclude: ["src/**/*.test.ts"], + reporter: ["text", "html", "lcov", "cobertura"], + }, + projects: [ + { + test: { + name: "unit", + include: ["src/**/*.test.ts"], + }, + }, + { + test: { + name: "integration", + include: ["test/integration/**/*.test.ts"], + // Starting the container, applying the schema and seeding it happens once for the whole file; a cold `docker pull` on a runner with no cached image dominates that. The default 5s would fail on image pull alone. + testTimeout: 120_000, + hookTimeout: 300_000, + }, + }, + ], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 924dbb3..cf025bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -145,6 +145,67 @@ importers: specifier: ^4.1.11 version: 4.1.11(@types/node@26.4.1)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.13)(yaml@2.9.0)) + packages/trilean-sql: + dependencies: + trilean: + specifier: workspace:^ + version: link:../trilean + devDependencies: + '@arethetypeswrong/cli': + specifier: ^0.18.5 + version: 0.18.5 + '@exadev/eslint-config': + specifier: ^2.10.2 + version: 2.10.4(eslint@10.9.1(jiti@2.6.1))(typescript-eslint@8.69.0(eslint@10.9.1(jiti@2.6.1))(typescript@6.0.3))(typescript@6.0.3) + '@testcontainers/postgresql': + specifier: ^12.1.0 + version: 12.1.0 + '@types/node': + specifier: ^26.4.0 + version: 26.4.1 + '@types/pg': + specifier: ^8.15.6 + version: 8.23.1 + '@vitest/coverage-v8': + specifier: ^4.1.11 + version: 4.1.11(vitest@4.1.11) + eslint: + specifier: ^10.9.1 + version: 10.9.1(jiti@2.6.1) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@10.9.1(jiti@2.6.1)) + eslint-plugin-prettier: + specifier: ^5.5.6 + version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.9.1(jiti@2.6.1)))(eslint@10.9.1(jiti@2.6.1))(prettier@3.9.6) + globals: + specifier: ^17.11.0 + version: 17.12.0 + pg: + specifier: ^8.16.3 + version: 8.23.0 + prettier: + specifier: ^3.9.6 + version: 3.9.6 + publint: + specifier: ^0.3.24 + version: 0.3.24 + tsdown: + specifier: ^0.22.14 + version: 0.22.14(@arethetypeswrong/core@0.18.5)(publint@0.3.24)(tsx@4.23.13)(typescript@6.0.3) + turbo: + specifier: ^2.10.12 + version: 2.10.12 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + typescript-eslint: + specifier: ^8.68.0 + version: 8.69.0(eslint@10.9.1(jiti@2.6.1))(typescript@6.0.3) + vitest: + specifier: ^4.1.11 + version: 4.1.11(@types/node@26.4.1)(@vitest/coverage-v8@4.1.11)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.13)(yaml@2.9.0)) + packages: '@actions/core@3.0.1': @@ -192,6 +253,9 @@ packages: resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} engines: {node: '>=6.9.0'} + '@balena/dockerignore@1.0.2': + resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} + '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -730,6 +794,20 @@ packages: '@semantic-release/release-notes-generator': ^14.1.1 semantic-release: ^25.0.9 + '@grpc/grpc-js@1.14.4': + resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + + '@grpc/proto-loader@0.8.1': + resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==} + engines: {node: '>=6'} + hasBin: true + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -912,6 +990,10 @@ packages: cpu: [x64] os: [win32] + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -925,6 +1007,12 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + + '@kwsites/file-exists@1.1.1': + resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} + '@loaderkit/resolve@1.0.6': resolution: {integrity: sha512-G8FdIoF5CypfwmD9rl8BXod5HDn8JqB0CCNBXDTaRZ+yRYhARrrSToX1zg1zy9jX3zLqigsELwhT4gNtkdQAUg==} @@ -991,6 +1079,10 @@ packages: '@oxc-project/types@0.148.0': resolution: {integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@pkgr/core@0.3.6': resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1016,6 +1108,33 @@ packages: '@poppinss/exception@1.2.3': resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.2': + resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + '@publint/pack@0.1.7': resolution: {integrity: sha512-4EDEmvxWtgsCnnVeBvtFIFZtUhPPt1+bA9JrSwU4Sa//6oKtzCSlGGXYJr44OD9aGISymbieJ4mCKHUygUDU+g==} engines: {node: '>=18'} @@ -1195,6 +1314,9 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@testcontainers/postgresql@12.1.0': + resolution: {integrity: sha512-Pjf2VSVNirEPfz36nidyrVAnZvc2YhajOznY4VgyEsvfTd5qiMNOuPq96drREvxAUtXl5SFLX7vXj7sSq4aTcA==} + '@turbo/darwin-64@2.10.12': resolution: {integrity: sha512-9nKgKoF6ZOUsM+or0OtNf+TTJSfGvDNP7ZFv/ZGWVwOSCkumyctQiTeHwB4UNljHTnC41AqylgbunLDHoccNrA==} cpu: [x64] @@ -1231,6 +1353,12 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/docker-modem@3.0.6': + resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} + + '@types/dockerode@4.0.1': + resolution: {integrity: sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==} + '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} @@ -1240,12 +1368,27 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + '@types/node@26.4.1': resolution: {integrity: sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/pg@8.23.1': + resolution: {integrity: sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==} + + '@types/ssh2-streams@0.1.13': + resolution: {integrity: sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==} + + '@types/ssh2@0.5.52': + resolution: {integrity: sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==} + + '@types/ssh2@1.15.6': + resolution: {integrity: sha512-oGdxhBqcRTwSTKFm+9EiKzkNVYRLEFkcW44lhguvBalGJbWfGnDt/ezwSUZc+SF9m9bMc3VyklNAtp7zICjS5w==} + '@typescript-eslint/eslint-plugin@8.69.0': resolution: {integrity: sha512-t5jQTKPIgVW1PE6dR6H6Qz5gm8zjMlX5/2gRaOGd9eO6V7J+tQc6iWKukEe7dY8u9HyYasQ0yfF0/FSSTEO2gA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1478,6 +1621,10 @@ packages: '@yuku-toolchain/types@0.8.7': resolution: {integrity: sha512-2Z53dNxAJL6UvFoIrDZvYf3zlO8s4VJK4O2hhaB4mXVwwpX/7ajtss3cmfqKvamlNLWyt9FSWs4eoYdlbxpnHA==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1533,6 +1680,14 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1546,6 +1701,9 @@ packages: array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1553,19 +1711,85 @@ packages: ast-v8-to-istanbul@1.0.5: resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==} + async-lock@1.4.1: + resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + b4a@1.8.1: + resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + bare-events@2.9.2: + resolution: {integrity: sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.8.1: + resolution: {integrity: sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==} + engines: {bare: '>=1.28.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-path@3.1.2: + resolution: {integrity: sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==} + + bare-stream@2.13.4: + resolution: {integrity: sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.5.4: + resolution: {integrity: sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} bottleneck@2.19.5: resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} + brace-expansion@2.1.4: + resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} + brace-expansion@5.0.9: resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} engines: {node: 20 || >=22} @@ -1574,6 +1798,24 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + buildcheck@0.0.7: + resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==} + engines: {node: '>=10.0.0'} + + byline@5.0.0: + resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} + engines: {node: '>=0.10.0'} + cac@7.0.0: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} @@ -1607,6 +1849,9 @@ packages: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + cjs-module-lexer@1.2.3: resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} @@ -1629,6 +1874,10 @@ packages: cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + cliui@9.0.1: resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} engines: {node: '>=20'} @@ -1657,6 +1906,10 @@ packages: compare-func@2.0.0: resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} @@ -1735,6 +1988,19 @@ packages: typescript: optional: true + cpu-features@0.0.10: + resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} + engines: {node: '>=10.0.0'} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1770,6 +2036,18 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + docker-compose@1.4.2: + resolution: {integrity: sha512-rPHigTKGaEHpkUmfd69QgaOp+Os5vGJwG/Ry8lcr8W/382AmI+z/D7qoa9BybKIkqNppaIbs8RYeHSevdQjWww==} + engines: {node: '>= 6.0.0'} + + docker-modem@5.0.7: + resolution: {integrity: sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==} + engines: {node: '>= 8.0'} + + dockerode@5.0.1: + resolution: {integrity: sha512-avsq/xk4YPIrn0CgleX5bjT9Y8IT1p9PxrNQ++RBQ2WEyFfHCTDsT9kmyxz+H/axnjAwg8wJWEIuPGOUuNupiA==} + engines: {node: '>= 14.17'} + dot-prop@5.3.0: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} @@ -1786,12 +2064,18 @@ packages: duplexer2@0.1.4: resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + emojilib@2.4.0: resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} @@ -1799,6 +2083,9 @@ packages: resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} engines: {node: '>=14'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + env-ci@11.2.0: resolution: {integrity: sha512-D5kWfzkmaOQDioPmiviWAVtKmpPT4/iJmMVQxWxMPJTFyTkdc5JQUfc5iXEeWxcOdsYTKSAiA/Age4NUOqKsRA==} engines: {node: ^18.17 || >=20.6.1} @@ -1914,6 +2201,17 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + execa@10.0.1: resolution: {integrity: sha512-ge98qjkRK4IB7tL7Ju/6qmm5LHoH1eEMt5FNZrz3f4UIYhF28lggX20z3FaX1sgc67msLEn0N0BscOs29iuwyw==} engines: {node: '>=22'} @@ -1936,6 +2234,9 @@ packages: fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -1996,6 +2297,13 @@ packages: flatted@3.4.4: resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@11.4.0: resolution: {integrity: sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==} engines: {node: '>=14.14'} @@ -2017,6 +2325,10 @@ packages: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} + get-port@5.1.1: + resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} + engines: {node: '>=8'} + get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -2040,6 +2352,11 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + global-directory@5.0.0: resolution: {integrity: sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==} engines: {node: '>=20'} @@ -2109,6 +2426,9 @@ packages: engines: {node: '>=18'} hasBin: true + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2181,6 +2501,10 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + is-stream@3.0.0: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2215,6 +2539,9 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + java-properties@1.0.2: resolution: {integrity: sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==} engines: {node: '>= 0.6.0'} @@ -2264,6 +2591,10 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -2365,6 +2696,9 @@ packages: lodash-es@4.18.1: resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.capitalize@4.2.1: resolution: {integrity: sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==} @@ -2380,6 +2714,12 @@ packages: lodash.uniqby@4.7.0: resolution: {integrity: sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -2445,9 +2785,29 @@ packages: resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -2458,6 +2818,9 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nan@2.28.0: + resolution: {integrity: sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==} + nanoid@3.3.18: resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2484,6 +2847,10 @@ packages: resolution: {integrity: sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==} engines: {node: ^20.17.0 || >=22.9.0} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + normalize-url@9.0.1: resolution: {integrity: sha512-ARftfC5HdUNu9jJeL8pHj8debUIHA2b91FizCoMzY4lG6dDX13jdvTK0TBe24IBDRf2HvJSzzwEPvmbkQWHRSg==} engines: {node: '>=20'} @@ -2575,6 +2942,9 @@ packages: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@6.0.0: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} @@ -2627,6 +2997,9 @@ packages: resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} engines: {node: '>=4'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@1.8.0: resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} @@ -2675,6 +3048,10 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -2685,6 +3062,40 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.16.0: + resolution: {integrity: sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.23.0: + resolution: {integrity: sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2708,6 +3119,22 @@ packages: resolution: {integrity: sha512-79Iho8QeYyooJ8e9lCRyTVlyTAkS/kXBYKff6TMzS3kEWGQ8Ds5UEtXpGrSUDLUWok6QTvxeYy0GO8fopHnaSA==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -2728,9 +3155,24 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + properties-reader@3.0.1: + resolution: {integrity: sha512-WPn+h9RGEExOKdu4bsF4HksG/uzd3cFq3MFtq8PsFeExPse5Ha/VOjQNyHhjboBFwGXGev6muJYTSPAOkROq2g==} + engines: {node: '>=18'} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + protobufjs@7.6.6: + resolution: {integrity: sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==} + engines: {node: '>=12.0.0'} + proxy-agent-negotiate@1.1.0: resolution: {integrity: sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==} engines: {node: '>= 20'} @@ -2745,6 +3187,9 @@ packages: engines: {node: '>=18'} hasBin: true + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2775,6 +3220,17 @@ packages: readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + registry-auth-token@5.1.1: resolution: {integrity: sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==} engines: {node: '>=14'} @@ -2798,6 +3254,10 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + rolldown-plugin-dts@0.27.14: resolution: {integrity: sha512-ZvuDDwoIpRK9RPxDXratCpklFO9QZZWndf/sd0VBFb4LEj0jj07UcHK9OCh7V4XiFz2Z89ziyBC2K6tJiDjrbw==} engines: {node: ^22.18.0 || >=24.11.0} @@ -2829,6 +3289,12 @@ packages: safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + semantic-release@25.0.9: resolution: {integrity: sha512-bxve7csK0/Txr++CkfrmV+X1r4jqiSOw2WsSad9E2S68R+ZfLBwDn8IceM8WfiOmKQIHgsQc1cNA8Dzg7U75pg==} engines: {node: ^22.14.0 || >= 24.10.0} @@ -2858,6 +3324,9 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -2893,9 +3362,23 @@ packages: spdx-license-ids@3.0.23: resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} + split-ca@1.0.1: + resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} + split2@1.0.0: resolution: {integrity: sha512-NKywug4u4pX/AZBB1FCPzZ6/7O+Xhz1qMVbzTvvKvikjO99oPN87SkK08mEY9P63/5lWjK+wgOOgApnTg5r6qg==} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + ssh-remote-port-forward@1.0.4: + resolution: {integrity: sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==} + + ssh2@1.17.0: + resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==} + engines: {node: '>=10.16.0'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -2905,6 +3388,9 @@ packages: stream-combiner2@1.1.1: resolution: {integrity: sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==} + streamx@2.28.1: + resolution: {integrity: sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -2913,6 +3399,10 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} @@ -2924,6 +3414,9 @@ packages: string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -2976,6 +3469,22 @@ packages: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} + tar-fs@2.1.5: + resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==} + + tar-fs@3.1.3: + resolution: {integrity: sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar-stream@3.2.1: + resolution: {integrity: sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==} + + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + temp-dir@3.0.0: resolution: {integrity: sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==} engines: {node: '>=14.16'} @@ -2984,6 +3493,13 @@ packages: resolution: {integrity: sha512-d79HhZya5Djd7am0q+W4RTsSU+D/aJzM+4Y4AGJGuGlgM2L6sx5ZvOYTmZjqPhrDrV6xJTtRSm1JCLj6V6LHLQ==} engines: {node: '>=14.16'} + testcontainers@12.1.0: + resolution: {integrity: sha512-YjDLqIITuhGLMnM10yhg3oV6lIG5IMpz1R1DPBZoOOks83q7i7IVpeSWRTiyl7roozjiyLmwIoLK/KY8OnZmIA==} + engines: {node: '>= 22.22'} + + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -3013,6 +3529,10 @@ packages: resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3081,6 +3601,9 @@ packages: resolution: {integrity: sha512-AswgMPnpOoaVZHrrSBejETzEbuIA69OVGwfkHwfrY0A23VjWXBANzgq9+OymWOHAIArB7D1+1z498WY8fGg1Jw==} hasBin: true + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -3126,6 +3649,9 @@ packages: unconfig-core@7.5.0: resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} @@ -3137,6 +3663,10 @@ packages: resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} engines: {node: '>=20.18.1'} + undici@8.10.1: + resolution: {integrity: sha512-YQ3WlbqjYMmNpdvDH64jAgLjxuAR9+649calDWhbshYaeQGO2bR4nI94ORJmwI3J9YhoKQnpyGOK+0zlWS5N5Q==} + engines: {node: '>=22.19.0'} + unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} @@ -3320,10 +3850,17 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.21.0: resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} @@ -3353,6 +3890,10 @@ packages: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + yargs-parser@22.0.0: resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} @@ -3361,6 +3902,10 @@ packages: resolution: {integrity: sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==} engines: {node: '>=10'} + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + yargs@18.1.0: resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} @@ -3388,6 +3933,10 @@ packages: yuku-parser@0.8.7: resolution: {integrity: sha512-vRD9nwt4L3aYpxNqeSC4WqLv58xrXef0Ong1Mc45CTXTIpvLafx7JO05sczmQZwdLEZvywrLOGdNC5+Rp5N1BQ==} + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} @@ -3454,6 +4003,8 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@balena/dockerignore@1.0.2': {} + '@bcoe/v8-coverage@1.0.2': {} '@braidai/lang@1.1.2': {} @@ -3846,6 +4397,25 @@ snapshots: transitivePeerDependencies: - typescript + '@grpc/grpc-js@1.14.4': + dependencies: + '@grpc/proto-loader': 0.8.1 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.6 + yargs: 17.7.3 + + '@grpc/proto-loader@0.8.1': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.6 + yargs: 17.7.3 + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -3968,6 +4538,15 @@ snapshots: '@img/sharp-win32-x64@0.35.2': optional: true + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.6.0': {} @@ -3982,6 +4561,14 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.6.0 + '@js-sdsl/ordered-map@4.4.2': {} + + '@kwsites/file-exists@1.1.1': + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@loaderkit/resolve@1.0.6': dependencies: '@braidai/lang': 1.1.2 @@ -4060,6 +4647,9 @@ snapshots: '@oxc-project/types@0.148.0': {} + '@pkgjs/parseargs@0.11.0': + optional: true + '@pkgr/core@0.3.6': {} '@pnpm/config.env-replace@1.1.0': {} @@ -4086,6 +4676,26 @@ snapshots: '@poppinss/exception@1.2.3': {} + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.2': {} + '@publint/pack@0.1.7': dependencies: tinyexec: 1.3.1 @@ -4255,6 +4865,15 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@testcontainers/postgresql@12.1.0': + dependencies: + testcontainers: 12.1.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color + '@turbo/darwin-64@2.10.12': optional: true @@ -4280,18 +4899,52 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/docker-modem@3.0.6': + dependencies: + '@types/node': 26.4.1 + '@types/ssh2': 1.15.6 + + '@types/dockerode@4.0.1': + dependencies: + '@types/docker-modem': 3.0.6 + '@types/node': 26.4.1 + '@types/ssh2': 1.15.6 + '@types/esrecurse@4.3.1': {} '@types/estree@1.0.9': {} '@types/json-schema@7.0.15': {} + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + '@types/node@26.4.1': dependencies: undici-types: 8.3.0 '@types/normalize-package-data@2.4.4': {} + '@types/pg@8.23.1': + dependencies: + '@types/node': 26.4.1 + pg-protocol: 1.16.0 + pg-types: 2.2.0 + + '@types/ssh2-streams@0.1.13': + dependencies: + '@types/node': 26.4.1 + + '@types/ssh2@0.5.52': + dependencies: + '@types/node': 26.4.1 + '@types/ssh2-streams': 0.1.13 + + '@types/ssh2@1.15.6': + dependencies: + '@types/node': 18.19.130 + '@typescript-eslint/eslint-plugin@8.69.0(@typescript-eslint/parser@8.69.0(eslint@10.9.1(jiti@2.6.1))(typescript@6.0.3))(eslint@10.9.1(jiti@2.6.1))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -4512,6 +5165,10 @@ snapshots: '@yuku-toolchain/types@0.8.7': {} + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + acorn-jsx@5.3.2(acorn@8.18.0): dependencies: acorn: 8.18.0 @@ -4561,6 +5218,30 @@ snapshots: any-promise@1.3.0: {} + archiver-utils@5.0.2: + dependencies: + glob: 10.5.0 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.18.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.7.0 + readdir-glob: 1.1.3 + tar-stream: 3.2.1 + zip-stream: 6.0.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + argparse@2.0.1: {} argue-cli@3.2.0: {} @@ -4569,6 +5250,10 @@ snapshots: array-ify@1.0.0: {} + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + assertion-error@2.0.1: {} ast-v8-to-istanbul@1.0.5: @@ -4577,14 +5262,67 @@ snapshots: estree-walker: 3.0.3 js-tokens: 10.0.0 + async-lock@1.4.1: {} + + async@3.2.6: {} + + b4a@1.8.1: {} + + balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + bare-events@2.9.2: {} + + bare-fs@4.8.1: + dependencies: + bare-events: 2.9.2 + bare-path: 3.1.2 + bare-stream: 2.13.4(bare-events@2.9.2) + bare-url: 2.5.4 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-path@3.1.2: {} + + bare-stream@2.13.4(bare-events@2.9.2): + dependencies: + b4a: 1.8.1 + streamx: 2.28.1 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.9.2 + transitivePeerDependencies: + - react-native-b4a + + bare-url@2.5.4: + dependencies: + bare-path: 3.1.2 + + base64-js@1.5.1: {} + + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + before-after-hook@4.0.0: {} + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + blake3-wasm@2.1.5: {} bottleneck@2.19.5: {} + brace-expansion@2.1.4: + dependencies: + balanced-match: 1.0.2 + brace-expansion@5.0.9: dependencies: balanced-match: 4.0.4 @@ -4593,6 +5331,23 @@ snapshots: dependencies: fill-range: 7.1.1 + buffer-crc32@1.0.0: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buildcheck@0.0.7: + optional: true + + byline@5.0.0: {} + cac@7.0.0: {} callsites@3.1.0: {} @@ -4616,6 +5371,8 @@ snapshots: char-regex@1.0.2: {} + chownr@1.1.4: {} + cjs-module-lexer@1.2.3: {} cjs-module-lexer@1.4.3: {} @@ -4645,6 +5402,12 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + cliui@9.0.1: dependencies: string-width: 7.2.0 @@ -4672,6 +5435,14 @@ snapshots: array-ify: 1.0.0 dot-prop: 5.3.0 + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + config-chain@1.1.13: dependencies: ini: 1.3.8 @@ -4745,6 +5516,19 @@ snapshots: optionalDependencies: typescript: 6.0.3 + cpu-features@0.0.10: + dependencies: + buildcheck: 0.0.7 + nan: 2.28.0 + optional: true + + crc-32@1.2.2: {} + + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.7.0 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -4771,6 +5555,30 @@ snapshots: dependencies: path-type: 4.0.0 + docker-compose@1.4.2: + dependencies: + yaml: 2.9.0 + + docker-modem@5.0.7: + dependencies: + debug: 4.4.3 + readable-stream: 3.6.2 + split-ca: 1.0.1 + ssh2: 1.17.0 + transitivePeerDependencies: + - supports-color + + dockerode@5.0.1: + dependencies: + '@balena/dockerignore': 1.0.2 + '@grpc/grpc-js': 1.14.4 + '@grpc/proto-loader': 0.7.15 + docker-modem: 5.0.7 + protobufjs: 7.6.6 + tar-fs: 2.1.5 + transitivePeerDependencies: + - supports-color + dot-prop@5.3.0: dependencies: is-obj: 2.0.0 @@ -4781,14 +5589,22 @@ snapshots: dependencies: readable-stream: 2.3.8 + eastasianwidth@0.2.0: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + emojilib@2.4.0: {} empathic@2.0.1: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + env-ci@11.2.0: dependencies: execa: 8.0.1 @@ -4957,6 +5773,16 @@ snapshots: esutils@2.0.3: {} + event-target-shim@5.0.1: {} + + events-universal@1.0.1: + dependencies: + bare-events: 2.9.2 + transitivePeerDependencies: + - bare-abort-controller + + events@3.3.0: {} + execa@10.0.1: dependencies: '@sindresorhus/merge-streams': 4.0.0 @@ -5005,6 +5831,8 @@ snapshots: fast-diff@1.3.0: {} + fast-fifo@1.3.2: {} + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -5056,6 +5884,13 @@ snapshots: flatted@3.4.4: {} + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fs-constants@1.0.0: {} + fs-extra@11.4.0: dependencies: graceful-fs: 4.2.11 @@ -5071,6 +5906,8 @@ snapshots: get-east-asian-width@1.6.0: {} + get-port@5.1.1: {} + get-stream@6.0.1: {} get-stream@8.0.1: {} @@ -5097,6 +5934,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + global-directory@5.0.0: dependencies: ini: 6.0.0 @@ -5160,6 +6006,8 @@ snapshots: husky@9.1.7: {} + ieee754@1.2.1: {} + ignore@5.3.2: {} ignore@7.0.8: {} @@ -5208,6 +6056,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-stream@2.0.1: {} + is-stream@3.0.0: {} is-stream@4.0.1: {} @@ -5239,6 +6089,12 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + java-properties@1.0.2: {} jiti@2.6.1: {} @@ -5277,6 +6133,10 @@ snapshots: kleur@4.1.5: {} + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -5359,6 +6219,8 @@ snapshots: lodash-es@4.18.1: {} + lodash.camelcase@4.3.0: {} + lodash.capitalize@4.2.1: {} lodash.escaperegexp@4.1.2: {} @@ -5369,6 +6231,10 @@ snapshots: lodash.uniqby@4.7.0: {} + lodash@4.18.1: {} + + long@5.3.2: {} + lru-cache@10.4.3: {} lru-cache@11.5.2: {} @@ -5448,8 +6314,22 @@ snapshots: dependencies: brace-expansion: 5.0.9 + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.4 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.4 + minimist@1.2.8: {} + minipass@7.1.3: {} + + mkdirp-classic@0.5.3: {} + + mkdirp@3.0.1: {} + mri@1.2.0: {} ms@2.1.3: {} @@ -5460,6 +6340,9 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nan@2.28.0: + optional: true + nanoid@3.3.18: {} natural-compare@1.4.0: {} @@ -5487,6 +6370,8 @@ snapshots: semver: 7.8.5 validate-npm-package-license: 3.0.4 + normalize-path@3.0.0: {} + normalize-url@9.0.1: {} npm-run-path@5.3.0: @@ -5504,6 +6389,10 @@ snapshots: obug@2.1.4: {} + once@1.4.0: + dependencies: + wrappy: 1.0.2 + onetime@6.0.0: dependencies: mimic-fn: 4.0.0 @@ -5551,6 +6440,8 @@ snapshots: p-try@1.0.0: {} + package-json-from-dist@1.0.1: {} + package-manager-detector@1.8.0: {} parent-module@1.0.1: @@ -5593,12 +6484,52 @@ snapshots: path-key@4.0.0: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + path-to-regexp@6.3.0: {} path-type@4.0.0: {} pathe@2.0.3: {} + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.14.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.23.0): + dependencies: + pg: 8.23.0 + + pg-protocol@1.16.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.23.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.23.0) + pg-protocol: 1.16.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -5618,6 +6549,16 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.1: @@ -5632,8 +6573,37 @@ snapshots: process-nextick-args@2.0.1: {} + process@0.11.10: {} + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + properties-reader@3.0.1: + dependencies: + '@kwsites/file-exists': 1.1.1 + mkdirp: 3.0.1 + transitivePeerDependencies: + - supports-color + proto-list@1.2.4: {} + protobufjs@7.6.6: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.2 + '@types/node': 26.4.1 + long: 5.3.2 + proxy-agent-negotiate@1.1.0: {} publint@0.3.24: @@ -5643,6 +6613,11 @@ snapshots: picocolors: 1.1.1 sade: 1.8.1 + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@2.3.1: {} quansync@1.0.0: {} @@ -5692,6 +6667,24 @@ snapshots: string_decoder: 1.1.1 util-deprecate: 1.0.2 + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.9 + registry-auth-token@5.1.1: dependencies: '@pnpm/npm-conf': 3.0.3 @@ -5706,6 +6699,8 @@ snapshots: resolve-pkg-maps@1.0.0: {} + retry@0.12.0: {} + rolldown-plugin-dts@0.27.14(rolldown@1.2.7)(typescript@6.0.3): dependencies: dts-resolver: 3.0.0 @@ -5747,6 +6742,10 @@ snapshots: safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + semantic-release@25.0.9(typescript@6.0.3): dependencies: '@semantic-release/commit-analyzer': 13.0.1(semantic-release@25.0.9(typescript@6.0.3)) @@ -5826,6 +6825,8 @@ snapshots: siginfo@2.0.0: {} + signal-exit@3.0.7: {} + signal-exit@4.1.0: {} signale@1.4.0: @@ -5858,10 +6859,27 @@ snapshots: spdx-license-ids@3.0.23: {} + split-ca@1.0.1: {} + split2@1.0.0: dependencies: through2: 2.0.5 + split2@4.2.0: {} + + ssh-remote-port-forward@1.0.4: + dependencies: + '@types/ssh2': 0.5.52 + ssh2: 1.17.0 + + ssh2@1.17.0: + dependencies: + asn1: 0.2.6 + bcrypt-pbkdf: 1.0.2 + optionalDependencies: + cpu-features: 0.0.10 + nan: 2.28.0 + stackback@0.0.2: {} std-env@4.2.0: {} @@ -5871,6 +6889,15 @@ snapshots: duplexer2: 0.1.4 readable-stream: 2.3.8 + streamx@2.28.1: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + string-argv@0.3.2: {} string-width@4.2.3: @@ -5879,6 +6906,12 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + string-width@7.2.0: dependencies: emoji-regex: 10.6.0 @@ -5894,6 +6927,10 @@ snapshots: dependencies: safe-buffer: 5.1.2 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -5937,6 +6974,51 @@ snapshots: tagged-tag@1.0.0: {} + tar-fs@2.1.5: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + + tar-fs@3.1.3: + dependencies: + pump: 3.0.4 + tar-stream: 3.2.1 + optionalDependencies: + bare-fs: 4.8.1 + bare-path: 3.1.2 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar-stream@3.2.1: + dependencies: + b4a: 1.8.1 + bare-fs: 4.8.1 + fast-fifo: 1.3.2 + streamx: 2.28.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + teex@1.0.1: + dependencies: + streamx: 2.28.1 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + temp-dir@3.0.0: {} tempy@3.2.0: @@ -5946,6 +7028,35 @@ snapshots: type-fest: 2.19.0 unique-string: 3.0.0 + testcontainers@12.1.0: + dependencies: + '@balena/dockerignore': 1.0.2 + '@types/dockerode': 4.0.1 + archiver: 7.0.1 + async-lock: 1.4.1 + byline: 5.0.0 + debug: 4.4.3 + docker-compose: 1.4.2 + dockerode: 5.0.1 + get-port: 5.1.1 + proper-lockfile: 4.1.2 + properties-reader: 3.0.1 + ssh-remote-port-forward: 1.0.4 + tar-fs: 3.1.3 + tmp: 0.2.7 + undici: 8.10.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color + + text-decoder@1.2.7: + dependencies: + b4a: 1.8.1 + transitivePeerDependencies: + - react-native-b4a + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -5974,6 +7085,8 @@ snapshots: tinyrainbow@3.1.1: {} + tmp@0.2.7: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -6034,6 +7147,8 @@ snapshots: '@turbo/windows-64': 2.10.12 '@turbo/windows-arm64': 2.10.12 + tweetnacl@0.14.5: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -6071,12 +7186,16 @@ snapshots: '@quansync/fs': 1.0.0 quansync: 1.0.0 + undici-types@5.26.5: {} + undici-types@8.3.0: {} undici@6.28.0: {} undici@7.29.0: {} + undici@8.10.1: {} + unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 @@ -6206,12 +7325,20 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 string-width: 7.2.0 strip-ansi: 7.2.0 + wrappy@1.0.2: {} + ws@8.21.0: {} xtend@4.0.2: {} @@ -6222,6 +7349,8 @@ snapshots: yargs-parser@20.2.9: {} + yargs-parser@21.1.1: {} + yargs-parser@22.0.0: {} yargs@16.2.2: @@ -6234,6 +7363,16 @@ snapshots: y18n: 5.0.8 yargs-parser: 20.2.9 + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yargs@18.1.0: dependencies: cliui: 9.0.1 @@ -6299,6 +7438,12 @@ snapshots: '@yuku-parser/binding-win32-arm64': 0.8.7 '@yuku-parser/binding-win32-x64': 0.8.7 + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.7.0 + zod@4.4.3: {} zod@4.5.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4152b63..f9e8aac 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,9 +4,16 @@ packages: # pnpm 11 defaults this to false, which means a dependency on a sibling package resolves from the npm registry even when the workspace holds a version satisfying the range. Setting it true links a sibling whenever the declared range is satisfied by the workspace version, and falls back to the registry when it is not. There is one package here today, so nothing exercises it yet -- it is set now so the first sibling added is linked rather than silently downloaded. linkWorkspacePackages: true -# workerd (the Cloudflare Workers runtime @cloudflare/vitest-pool-workers drives the workers test suite through) and esbuild (its bundler) both ship native binaries installed via a postinstall script -- pnpm 11 ignores those by default, so allow them explicitly. +# Every dependency in the tree that ships an install script, and whether it may run. pnpm 11 ignores install scripts unless listed, and fails the install outright while any is neither allowed nor refused -- so an entry is a decision recorded, not merely a permission granted, and `false` is as meaningful an answer as `true`. +# +# workerd (the Cloudflare Workers runtime @cloudflare/vitest-pool-workers drives the workers test suite through) and esbuild (its bundler) both need theirs: the script is how each fetches its own platform's binary, and neither runs at all without it. +# +# The three refused below arrive under testcontainers, which packages/trilean-sql's integration suite uses to start PostgreSQL. Each script is optional to the way this workspace uses the package: ssh2 and its cpu-features helper build native acceleration for SSH transports, and testcontainers reaches Docker over a local socket rather than SSH; protobufjs's script only regenerates bundled artefacts the published tarball already contains. Refusing them keeps a test-only dependency chain from executing arbitrary code at install time. allowBuilds: + cpu-features: false esbuild: true + protobufjs: false + ssh2: false workerd: true # 60 minutes. pnpm reads workspace-level settings only from the workspace root, so this file is the one place they can take effect at all.