diff --git a/.commitlintrc b/.commitlintrc new file mode 100644 index 0000000..0df1d25 --- /dev/null +++ b/.commitlintrc @@ -0,0 +1,5 @@ +{ + "extends": [ + "@commitlint/config-conventional" + ] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2578948 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abdf311..09f8bd1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: strategy: matrix: - pg: ["postgres:9.5-alpine", "postgres:9.6-alpine", "postgres:10-alpine", "postgres:11-alpine", "postgres:12-alpine", "postgres:13-alpine", "postgres:14-alpine", "postgres:15-alpine", "postgres:16-alpine", "postgres:17-alpine"] + pg: ["postgres:13-alpine", "postgres:14-alpine", "postgres:15-alpine", "postgres:16-alpine", "postgres:17-alpine"] steps: - uses: actions/checkout@v4 @@ -41,3 +41,11 @@ jobs: - name: Run tests no check run: | make run_test_nochecks + + - name: Run Node.js e2e Tests + uses: actions/setup-node@v4 + with: + node-version: '20.x' + - run: npm ci + - run: npm run build --if-present + - run: npm run test:e2e:runner diff --git a/.gitignore b/.gitignore index 28b56ed..7cc1625 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ test/result .env .envrc +.eslintcache test/remote_expected test/remote_sql test/remote_result +node_modules diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..b009dfb --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +lts/* diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..9f8ecd9 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": false, + "singleQuote": true, + "arrowParens": "avoid", + "trailingComma": "none" +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b35a7c1 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Test File w/ ts-node", + "protocol": "inspector", + "runtimeArgs": ["--loader", "ts-node/esm/transpile-only", "--test"], + "args": ["${relativeFile}"], + "outputCapture": "std", + "internalConsoleOptions": "openOnSessionStart", + "envFile": "${workspaceRoot}/.env", + "skipFiles": [ + "${workspaceRoot}/../../node_modules/**/*", + "/**/*" + ], + "windows": { + "skipFiles": ["C:\\**\\node_modules\\**\\*", "/**/*"] + }, + "disableOptimisticBPs": true + } + ] +} diff --git a/README.md b/README.md index ace5837..b4e11fa 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![ci](https://github.com/nearform/temporal_tables/actions/workflows/ci.yml/badge.svg)](https://github.com/nearform/temporal_tables/actions/workflows/ci.yml) +> **⚠️ IMPORTANT:** PostgreSQL 13.21 or higher is required for modern features. Legacy support for PostgreSQL versions below 13 has been dropped. + This rewrite aims to provide a temporal tables solution in PL/pgSQL, targeting AWS RDS, Google Cloud SQL, and Azure Database for PostgreSQL where custom C extensions aren't permitted. The script in `versioning_function.sql` serves as a direct substitute. @@ -12,10 +14,24 @@ Over time, new features have been introduced while maintaining backward compatib - [Ignore updates with no actual changes](#ignore-unchanged-values) - [Include the current version in history](#include-current-version-in-history) -- [Autoincrementing version number support](#autoincrementing-version-number) +- [Static trigger code generation](#static-trigger-code-generation) +- [Automatic trigger re-rendering](#event-trigger-for-automatic-re-rendering) +- [Versioning tables metadata management](#versioning-tables-metadata) +- [Update conflict mitigation](#mitigate-update-conflicts) +- [Auto-incrementing version number support](#autoincrementing-version-number) +- [Migration mode support](#migration-mode) +## PostgreSQL Version Requirements + +**Minimum Required Version: PostgreSQL 13.21** + +- **All functionality** (including static triggers, metadata management, event triggers): Requires PostgreSQL 13.21 or higher +- **Legacy PostgreSQL versions** (below 13): No longer supported + +For full feature compatibility and performance, we recommend using the latest PostgreSQL version available. + ## Usage Create a database and the versioning function: @@ -235,6 +251,40 @@ FOR EACH ROW EXECUTE PROCEDURE versioning( ); ``` + + +### Mitigate Update Conflicts + +By default, when multiple transactions try to update the same row simultaneously, PostgreSQL's versioning system can detect conflicts where the system period start time would be greater than or equal to the current transaction time. This can cause errors in high-concurrency scenarios. + +The `mitigate_update_conflicts` parameter (sixth parameter) automatically adjusts timestamps by adding 1 microsecond when conflicts are detected, allowing operations to proceed smoothly: + +```sql +CREATE TRIGGER versioning_trigger +BEFORE INSERT OR UPDATE OR DELETE ON subscriptions +FOR EACH ROW EXECUTE PROCEDURE versioning( + 'sys_period', 'subscriptions_history', true, false, false, true +); +``` + +**Note:** This feature slightly modifies timestamps to resolve conflicts, which may affect temporal queries that rely on exact timing. + + + +### Migration Mode + +Migration mode (sixth parameter) enables gradual adoption of the `include_current_version_in_history` feature for existing tables without requiring a maintenance window: + +```sql +CREATE TRIGGER versioning_trigger +BEFORE INSERT OR UPDATE OR DELETE ON subscriptions +FOR EACH ROW EXECUTE PROCEDURE versioning( + 'sys_period', 'subscriptions_history', true, false, true, true +); +``` + +When enabled, the trigger automatically populates missing current versions in the history table during UPDATE or DELETE operations. + ### Migrating to include_current_version_in_history @@ -340,13 +390,13 @@ WHERE UPPER(sys_period) IS NULL; When adopting the `include_current_version_in_history` feature for existing tables, you can use the automatic gradual migration mode to seamlessly populate the history table with current records. -The migration mode is enabled by adding a sixth parameter to the versioning trigger: +The migration mode is enabled by adding a seventh parameter to the versioning trigger: ```sql CREATE TRIGGER versioning_trigger BEFORE INSERT OR UPDATE OR DELETE ON your_table FOR EACH ROW EXECUTE PROCEDURE versioning( - 'sys_period', 'your_table_history', true, false, true, true + 'sys_period', 'your_table_history', true, false, true, false, true ); ``` @@ -386,11 +436,181 @@ When migration mode is enabled: **Note:** The automatic migration happens gradually, filling in missing history only when existing records are updated or deleted. As a result, records that rarely change will still require manual migration using the [method described above](#migration-to-include-current-version-in-history). However, since the most active records will be automatically migrated, the risk of missing important data is greatly reduced, eliminating the need for a dedicated maintenance window. - + + +## Versioning Tables Metadata + +The modern functionality includes a metadata table to track all versioned tables and their configuration. This enables automatic trigger re-rendering when table schemas change. + +### Setup + +1. **Install the metadata table:** + ```sh + psql temporal_test < versioning_tables_metadata.sql + ``` + +2. **Register versioned tables:** + ```sql + INSERT INTO versioning_tables_metadata ( + table_name, + table_schema, + history_table, + history_table_schema, + sys_period, + ignore_unchanged_values, + include_current_version_in_history, + mitigate_update_conflicts, + enable_migration_mode + ) VALUES ( + 'subscriptions', + 'public', + 'subscriptions_history', + 'public', + 'sys_period', + false, + false, + false, + false + ); + ``` + +### Benefits + +- **Automatic trigger re-rendering** when table schemas change +- **Centralized configuration** for all versioned tables +- **Schema flexibility** with separate schemas for tables and history tables + + -### Autoincrementing version number +## Static Trigger Code Generation (PostgreSQL 13+) -There is support for autoincrementing a version number whenever values of a row get updated. This may be useful for a few reasons: +The modern static trigger generator creates optimized, table-specific trigger functions with no runtime schema lookups, providing better performance and reliability. + +### Basic Usage + +1. **Install the generator:** + ```sh + psql temporal_test < render_versioning_trigger.sql + ``` + +2. **Generate static trigger:** + ```sql + CALL render_versioning_trigger( + table_name => 'subscriptions', + history_table => 'subscriptions_history', + sys_period => 'sys_period' + ); + ``` + +### Advanced Features + +The static generator supports all modern features: + +```sql +-- Generate trigger with all advanced features enabled +CALL render_versioning_trigger( + table_name => 'subscriptions', + history_table => 'subscriptions_history', + sys_period => 'sys_period', + ignore_unchanged_values => true, + include_current_version_in_history => true, + mitigate_update_conflicts => true, + enable_migration_mode => true, + increment_version => true +); +``` + +### Auto-incrementing Version Number Support + +The static generator fully supports auto-incrementing version numbers: + +```sql +-- First, add version columns to your tables +ALTER TABLE subscriptions ADD COLUMN version integer NOT NULL DEFAULT 1; +ALTER TABLE subscriptions_history ADD COLUMN version integer NOT NULL; + +-- Generate trigger with version increment support +CALL render_versioning_trigger( + table_name => 'subscriptions', + history_table => 'subscriptions_history', + sys_period => 'sys_period', + increment_version => true +); +``` + +This provides the same functionality as the legacy versioning function but with better performance through static generation. + +### Benefits of Static Triggers + +- **Better Performance**: No runtime schema lookups +- **Compile-time Validation**: Errors detected when trigger is created +- **Explicit Dependencies**: Clear relationship between table structure and trigger code +- **Optimized Code**: Generated specifically for your table's current schema + + + +## Event Trigger for Automatic Re-rendering (PostgreSQL 13+) + +Event triggers automatically re-render static versioning triggers when table schemas change, ensuring triggers stay synchronized with table structures. + +### Setup + +1. **Install event trigger:** + ```sh + psql temporal_test < event_trigger_versioning.sql + ``` + +2. **Ensure metadata is populated:** + The event trigger uses the `versioning_tables_metadata` table to determine which tables need trigger re-rendering. + +### How It Works + +- **Automatic Detection**: Event trigger fires on `ALTER TABLE` commands +- **Metadata Lookup**: Checks if the altered table is registered in `versioning_tables_metadata` +- **Trigger Re-rendering**: Automatically calls `render_versioning_trigger` with stored configuration +- **Schema Synchronization**: Ensures triggers always match current table structure + +### Example Workflow + +```sql +-- 1. Register table in metadata +INSERT INTO versioning_tables_metadata ( + table_name, table_schema, history_table, history_table_schema, + sys_period, ignore_unchanged_values, include_current_version_in_history +) VALUES ( + 'subscriptions', 'public', 'subscriptions_history', 'public', + 'sys_period', true, true +); + +-- 2. Generate initial trigger +CALL render_versioning_trigger( + table_name => 'subscriptions', + history_table => 'subscriptions_history', + sys_period => 'sys_period', + ignore_unchanged_values => true, + include_current_version_in_history => true +); + +-- 3. Modify table schema - trigger is automatically re-rendered +ALTER TABLE subscriptions ADD COLUMN plan text; +ALTER TABLE subscriptions_history ADD COLUMN plan text; + +-- The event trigger automatically regenerates the versioning trigger! +``` + +### Benefits + +- **Zero Maintenance**: Triggers stay synchronized automatically +- **Schema Evolution**: Safe to modify table structures +- **Error Prevention**: Eliminates manual trigger update requirements +- **Development Friendly**: Works seamlessly with migration scripts + + + + +### Auto-increment version number + +There is support for auto-incrementing a version number whenever values of a row get updated. This may be useful for a few reasons: * Easier to see how many updates have been made to a row * Adding primary keys to the history table. E.g. if the main table has a primary key 'id', it will allow adding a primary key 'id', 'version' to the history table. A lot of ORMs expect a primary key @@ -429,8 +649,6 @@ UPDATE subscriptions SET state='updated' WHERE name='test1' then the table will reflect incremented version `name=test1, state=updated, version=2`. And correspondingly the history table will have the old version `name=test1, state=inserted, version=1` (or both versions if `include_current_version_in_history` is turned on). - - ## Migrations During the life of an application is may be necessary to change the schema of a table. In order for temporal_tables to continue to work properly the same migrations should be applied to the history table as well. @@ -455,6 +673,75 @@ If the column doesn't accept null values you'll need to modify it to allow for n ## Test +### End-to-End Tests (Enhanced) + +We've enhanced our comprehensive end-to-end tests written in modern TypeScript using Node.js built-in test runner and node-postgres. These tests provide extensive coverage of all temporal table features with improved reliability: + +#### Test Coverage +- **Static Generator Tests**: Full coverage of the static trigger generator functionality +- **Legacy Function Tests**: Backward compatibility testing with the original versioning function +- **Event Trigger Tests**: Automatic trigger re-rendering on schema changes +- **Integration Tests**: Real-world scenarios including e-commerce workflows, schema evolution, and performance testing +- **Error Handling**: Edge cases, transaction rollbacks, and concurrent modifications +- **Version-aware Testing**: Automatically adapts tests based on PostgreSQL version + +#### Test Reliability Improvements +- **Enhanced timestamp handling**: Eliminates flaky tests due to timing issues +- **Robust database state management**: Proper cleanup and isolation between tests +- **Version-specific loading**: Only loads SQL files appropriate for the PostgreSQL version +- **Better error reporting**: Detailed failure messages with timing context + +#### Running E2E Tests + +1. **Prerequisites**: Ensure you have PostgreSQL running (Docker or local installation) + +2. **Install dependencies**: + ```bash + npm install + ``` + +3. **Start database** (if using Docker): + ```bash + npm run db:start + ``` + +4. **Run all E2E tests**: + ```bash + npm run test:e2e + ``` + +5. **Run specific test suites**: + ```bash + # Static generator tests + npm run test:e2e:static + + # Legacy function tests + npm run test:e2e:legacy + + # Integration tests + npm run test:e2e:integration + + # Event trigger tests + npm run test:e2e:event + + # Custom test runner (TypeScript) + npm run test:e2e:runner + ``` + +#### Features of Enhanced E2E Tests +- **Type-safe**: Written in TypeScript with proper type definitions +- **Modern**: Uses Node.js built-in test runner (no external dependencies like Jest) +- **Comprehensive**: Tests all features including advanced options and edge cases +- **Isolated**: Each test starts with a clean database state +- **Real-world scenarios**: Includes practical examples like e-commerce order processing +- **Performance testing**: Bulk operations and concurrent access patterns +- **Version-aware**: Automatically skips tests not supported by current PostgreSQL version +- **Reliable timing**: Enhanced timestamp checking eliminates flaky test failures + +See [test/e2e/README.md](test/e2e/README.md) for detailed documentation. + +### Traditional Tests + Ensure you have a postgres database available. A database container can be started by running: ```sh diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..50b9f11 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,17 @@ +import globals from 'globals' +import js from '@eslint/js' +import prettierRecommended from 'eslint-plugin-prettier/recommended' + +export default [ + js.configs.recommended, + prettierRecommended, + { + languageOptions: { + globals: { + ...globals.node + }, + ecmaVersion: 'latest', + sourceType: 'module' + } + } +] diff --git a/event_trigger_versioning.sql b/event_trigger_versioning.sql new file mode 100644 index 0000000..f58e288 --- /dev/null +++ b/event_trigger_versioning.sql @@ -0,0 +1,52 @@ +-- event_trigger_versioning.sql +-- Event trigger to re-render static versioning trigger on ALTER TABLE + +CREATE OR REPLACE FUNCTION rerender_versioning_trigger() +RETURNS event_trigger AS $$ +DECLARE + obj record; + config record; + sql text; + source_schema text; + source_table text; + history_table text; + sys_period text; +BEGIN + FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands() LOOP + CONTINUE WHEN obj.command_tag <> 'ALTER TABLE'; + source_schema := SPLIT_PART(obj.object_identity, '.', 1); + source_table := SPLIT_PART(obj.object_identity, '.', 2); + -- when the source is history, invert to the actual source table + IF source_table ~ '_history$' THEN + source_table := SUBSTRING(source_table, 1, LENGTH(source_table) - 8); + END IF; + -- when a versioned table is altered, we need to re-render the trigger + SELECT * + INTO config + FROM versioning_tables_metadata + WHERE table_name = source_table + AND table_schema = source_schema; + + IF FOUND THEN + CALL render_versioning_trigger( + table_name => FORMAT('%I.%I', source_schema, source_table), + history_table => FORMAT('%I.%I', config.history_table_schema, config.history_table), + sys_period => config.sys_period, + ignore_unchanged_values => config.ignore_unchanged_values, + include_current_version_in_history => config.include_current_version_in_history, + mitigate_update_conflicts => config.mitigate_update_conflicts, + enable_migration_mode => config.enable_migration_mode, + increment_version => config.increment_version, + version_column_name => config.version_column_name + ); + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +DROP EVENT TRIGGER IF EXISTS rerender_versioning_on_alter; + +CREATE EVENT TRIGGER rerender_versioning_on_alter + ON ddl_command_end + WHEN TAG IN ('ALTER TABLE') + EXECUTE FUNCTION rerender_versioning_trigger(); diff --git a/jest.config.json b/jest.config.json new file mode 100644 index 0000000..cb1c545 --- /dev/null +++ b/jest.config.json @@ -0,0 +1,20 @@ +{ + "collectCoverage": true, + "extensionsToTreatAsEsm": [".ts"], + "moduleFileExtensions": ["js", "ts"], + "moduleNameMapper": { + "^(\\.{1,2}/.*)\\.js$": "$1" + }, + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".spec.ts$", + "transform": { + "^.+\\.ts$": [ + "ts-jest", + { + "useESM": true + } + ] + }, + "verbose": true +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c50c240 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3856 @@ +{ + "name": "temporal_tables", + "version": "1.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "temporal_tables", + "version": "1.2.0", + "license": "ISC", + "devDependencies": { + "@commitlint/cli": "^19.8.1", + "@commitlint/config-conventional": "^19.8.1", + "@eslint/js": "^9.28.0", + "@types/node": "^20.19.1", + "@types/pg": "^8.15.4", + "cross-env": "^7.0.3", + "eslint": "^9.29.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-prettier": "^5.5.0", + "globals": "^16.2.0", + "husky": "^9.1.7", + "lint-staged": "^16.1.0", + "pg": "^8.16.2", + "prettier": "^3.6.0", + "ts-node": "^10.9.2", + "tsx": "^4.20.3", + "typescript": "^5.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@commitlint/cli": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-19.8.1.tgz", + "integrity": "sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/format": "^19.8.1", + "@commitlint/lint": "^19.8.1", + "@commitlint/load": "^19.8.1", + "@commitlint/read": "^19.8.1", + "@commitlint/types": "^19.8.1", + "tinyexec": "^1.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "commitlint": "cli.js" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-conventional": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-19.8.1.tgz", + "integrity": "sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.1", + "conventional-changelog-conventionalcommits": "^7.0.2" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-validator": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-19.8.1.tgz", + "integrity": "sha512-0jvJ4u+eqGPBIzzSdqKNX1rvdbSU1lPNYlfQQRIFnBgLy26BtC0cFnr7c/AyuzExMxWsMOte6MkTi9I3SQ3iGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.1", + "ajv": "^8.11.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/ensure": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-19.8.1.tgz", + "integrity": "sha512-mXDnlJdvDzSObafjYrOSvZBwkD01cqB4gbnnFuVyNpGUM5ijwU/r/6uqUmBXAAOKRfyEjpkGVZxaDsCVnHAgyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.1", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/execute-rule": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-19.8.1.tgz", + "integrity": "sha512-YfJyIqIKWI64Mgvn/sE7FXvVMQER/Cd+s3hZke6cI1xgNT/f6ZAz5heND0QtffH+KbcqAwXDEE1/5niYayYaQA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/format": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-19.8.1.tgz", + "integrity": "sha512-kSJj34Rp10ItP+Eh9oCItiuN/HwGQMXBnIRk69jdOwEW9llW9FlyqcWYbHPSGofmjsqeoxa38UaEA5tsbm2JWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.1", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/is-ignored": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-19.8.1.tgz", + "integrity": "sha512-AceOhEhekBUQ5dzrVhDDsbMaY5LqtN8s1mqSnT2Kz1ERvVZkNihrs3Sfk1Je/rxRNbXYFzKZSHaPsEJJDJV8dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.1", + "semver": "^7.6.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/lint": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-19.8.1.tgz", + "integrity": "sha512-52PFbsl+1EvMuokZXLRlOsdcLHf10isTPlWwoY1FQIidTsTvjKXVXYb7AvtpWkDzRO2ZsqIgPK7bI98x8LRUEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/is-ignored": "^19.8.1", + "@commitlint/parse": "^19.8.1", + "@commitlint/rules": "^19.8.1", + "@commitlint/types": "^19.8.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/load": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-19.8.1.tgz", + "integrity": "sha512-9V99EKG3u7z+FEoe4ikgq7YGRCSukAcvmKQuTtUyiYPnOd9a2/H9Ak1J9nJA1HChRQp9OA/sIKPugGS+FK/k1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^19.8.1", + "@commitlint/execute-rule": "^19.8.1", + "@commitlint/resolve-extends": "^19.8.1", + "@commitlint/types": "^19.8.1", + "chalk": "^5.3.0", + "cosmiconfig": "^9.0.0", + "cosmiconfig-typescript-loader": "^6.1.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/message": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-19.8.1.tgz", + "integrity": "sha512-+PMLQvjRXiU+Ae0Wc+p99EoGEutzSXFVwQfa3jRNUZLNW5odZAyseb92OSBTKCu+9gGZiJASt76Cj3dLTtcTdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/parse": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-19.8.1.tgz", + "integrity": "sha512-mmAHYcMBmAgJDKWdkjIGq50X4yB0pSGpxyOODwYmoexxxiUCy5JJT99t1+PEMK7KtsCtzuWYIAXYAiKR+k+/Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.1", + "conventional-changelog-angular": "^7.0.0", + "conventional-commits-parser": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/read": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-19.8.1.tgz", + "integrity": "sha512-03Jbjb1MqluaVXKHKRuGhcKWtSgh3Jizqy2lJCRbRrnWpcM06MYm8th59Xcns8EqBYvo0Xqb+2DoZFlga97uXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/top-level": "^19.8.1", + "@commitlint/types": "^19.8.1", + "git-raw-commits": "^4.0.0", + "minimist": "^1.2.8", + "tinyexec": "^1.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/resolve-extends": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-19.8.1.tgz", + "integrity": "sha512-GM0mAhFk49I+T/5UCYns5ayGStkTt4XFFrjjf0L4S26xoMTSkdCf9ZRO8en1kuopC4isDFuEm7ZOm/WRVeElVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^19.8.1", + "@commitlint/types": "^19.8.1", + "global-directory": "^4.0.1", + "import-meta-resolve": "^4.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/rules": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-19.8.1.tgz", + "integrity": "sha512-Hnlhd9DyvGiGwjfjfToMi1dsnw1EXKGJNLTcsuGORHz6SS9swRgkBsou33MQ2n51/boIDrbsg4tIBbRpEWK2kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/ensure": "^19.8.1", + "@commitlint/message": "^19.8.1", + "@commitlint/to-lines": "^19.8.1", + "@commitlint/types": "^19.8.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/to-lines": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-19.8.1.tgz", + "integrity": "sha512-98Mm5inzbWTKuZQr2aW4SReY6WUukdWXuZhrqf1QdKPZBCCsXuG87c+iP0bwtD6DBnmVVQjgp4whoHRVixyPBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/top-level": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-19.8.1.tgz", + "integrity": "sha512-Ph8IN1IOHPSDhURCSXBz44+CIu+60duFwRsg6HqaISFHQHbmBtxVw4ZrFNIYUzEP7WwrNPxa2/5qJ//NK1FGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^7.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/types": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-19.8.1.tgz", + "integrity": "sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/conventional-commits-parser": "^5.0.0", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz", + "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz", + "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz", + "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", + "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", + "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/conventional-commits-parser": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.1.tgz", + "integrity": "sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.1.tgz", + "integrity": "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pg": { + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.4.tgz", + "integrity": "sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/conventional-changelog-angular": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", + "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", + "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-commits-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", + "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-text-path": "^2.0.0", + "JSONStream": "^1.3.5", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig-typescript-loader": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.1.0.tgz", + "integrity": "sha512-tJ1w35ZRUiM5FeTzT7DtYWAFFv37ZLqSRkGi2oeCK1gPhvaWjkAtfXvLmvE1pRfxxp9aQo6ba/Pvg1dKj05D4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jiti": "^2.4.1" + }, + "engines": { + "node": ">=v18" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=9", + "typescript": ">=5" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dargs": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz", + "integrity": "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz", + "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.1", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.29.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", + "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz", + "integrity": "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", + "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.2.0", + "path-exists": "^5.0.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/git-raw-commits": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz", + "integrity": "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dargs": "^8.0.0", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "git-raw-commits": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", + "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-text-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz", + "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "text-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lint-staged": { + "version": "16.1.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.2.tgz", + "integrity": "sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0", + "debug": "^4.4.1", + "lilconfig": "^3.1.3", + "listr2": "^8.3.3", + "micromatch": "^4.0.8", + "nano-spawn": "^1.0.2", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/listr2": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nano-spawn": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz", + "integrity": "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pg": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.2.tgz", + "integrity": "sha512-OtLWF0mKLmpxelOt9BqVq83QV6bTfsS0XLegIeAKqKjurRnRKie1Dc1iL89MugmSLhftxw6NNCyZhm1yQFLMEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.2", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.6" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.6.tgz", + "integrity": "sha512-uxmJAnmIgmYgnSFzgOf2cqGQBzwnRYcrEgXuFjJNEkpedEIPBSEzxY7ph4uA9k1mI+l/GR0HjPNS6FKNZe8SBQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.2.tgz", + "integrity": "sha512-Ci7jy8PbaWxfsck2dwZdERcDG2A0MG8JoQILs+uZNjABFuBuItAZCWUNz8sXRDMoui24rJw7WlXqgpMdBSN/vQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz", + "integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", + "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.4" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/text-extensions": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", + "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsx": { + "version": "4.20.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", + "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 57f6a1f..e6630b2 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "temporal_tables", + "type": "module", "version": "1.2.0", "description": "A postgresql temporal_tables extension in PL/pgSQL", "repository": { @@ -9,8 +10,16 @@ "scripts": { "db:start": "docker compose up -d", "db:stop": "docker compose down", - "update-version": "node ./scripts/update-version.js", - "test": "PGHOST=localhost PGPORT=5432 PGUSER=postgres PGPASSWORD=password make run_test" + "format": "prettier --config ./.prettierrc --write \"test/**/*.ts\"", + "test": "PGHOST=localhost PGPORT=5432 PGUSER=postgres PGPASSWORD=password make run_test", + "test:e2e:comparison": "node --env-file-if-exists ./.env --loader ts-node/esm/transpile-only --test test/e2e/test-performance-comparison.ts", + "test:e2e:event": "node --env-file-if-exists ./.env --loader ts-node/esm/transpile-only --test test/e2e/test-event-trigger.ts", + "test:e2e:increment": "node --env-file-if-exists ./.env --loader ts-node/esm/transpile-only --test test/e2e/test-increment-version.ts", + "test:e2e:integration": "node --env-file-if-exists ./.env --loader ts-node/esm/transpile-only --test test/e2e/test-integration.ts", + "test:e2e:legacy": "node --env-file-if-exists ./.env --loader ts-node/esm/transpile-only --test test/e2e/test-legacy.ts", + "test:e2e:runner": "node --env-file-if-exists ./.env --loader ts-node/esm/transpile-only test/e2e/run-tests.ts", + "test:e2e:static": "node --env-file-if-exists ./.env --loader ts-node/esm/transpile-only --test test/e2e/test-static-generator.ts", + "update-version": "node ./scripts/update-version.js" }, "keywords": [], "author": "", @@ -18,5 +27,24 @@ "bugs": { "url": "https://github.com/nearform/temporal_tables/issues" }, - "homepage": "https://github.com/nearform/temporal_tables#readme" + "homepage": "https://github.com/nearform/temporal_tables#readme", + "devDependencies": { + "@commitlint/cli": "^19.8.1", + "@commitlint/config-conventional": "^19.8.1", + "@eslint/js": "^9.28.0", + "@types/node": "^20.19.1", + "@types/pg": "^8.15.4", + "cross-env": "^7.0.3", + "eslint": "^9.29.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-prettier": "^5.5.0", + "globals": "^16.2.0", + "husky": "^9.1.7", + "lint-staged": "^16.1.0", + "pg": "^8.16.2", + "prettier": "^3.6.0", + "ts-node": "^10.9.2", + "tsx": "^4.20.3", + "typescript": "^5.0.0" + } } diff --git a/render_versioning_trigger.sql b/render_versioning_trigger.sql new file mode 100644 index 0000000..ddd8d04 --- /dev/null +++ b/render_versioning_trigger.sql @@ -0,0 +1,299 @@ +DROP PROCEDURE IF EXISTS render_versioning_trigger(text,text,text,boolean,boolean,boolean,boolean,boolean,text); + +CREATE OR REPLACE PROCEDURE render_versioning_trigger( + table_name text, + history_table text, + sys_period text, + ignore_unchanged_values boolean DEFAULT false, + include_current_version_in_history boolean DEFAULT false, + mitigate_update_conflicts boolean DEFAULT false, + enable_migration_mode boolean DEFAULT false, + increment_version boolean DEFAULT false, + version_column_name text DEFAULT 'version' +) +AS $$ +DECLARE + main_table_name text; + main_table_schema text; + hist_table_name text; + hist_table_schema text; + trigger_func_name text; + trigger_name text; + trigger_sql text; + func_sql text; + common_columns text; + sys_period_type text; + history_sys_period_type text; + new_row_compare text; + old_row_compare text; + version_declare_var text := ''; + version_init_logic text := ''; + version_column_insert text := ''; + version_old_value text := ''; + version_new_value text := ''; + version_increment_logic text := ''; +BEGIN + IF POSITION('.' IN table_name) > 0 THEN + main_table_schema := split_part(table_name, '.', 1); + main_table_name := split_part(table_name, '.', 2); + ELSE + main_table_schema := COALESCE(current_schema, 'public'); + main_table_name := table_name; + END IF; + table_name := format('%I.%I', main_table_schema, main_table_name); + + IF POSITION('.' IN history_table) > 0 THEN + hist_table_schema := split_part(history_table, '.', 1); + hist_table_name := split_part(history_table, '.', 2); + ELSE + hist_table_schema := COALESCE(current_schema, 'public'); + hist_table_name := history_table; + END IF; + history_table := format('%I.%I', hist_table_schema, hist_table_name); + + trigger_func_name := main_table_name || '_versioning'; + trigger_name := main_table_name || '_versioning_trigger'; + + -- Get columns common to both source and history tables, excluding sys_period + SELECT string_agg(quote_ident(main.attname), ',') + INTO common_columns + FROM ( + SELECT attname + FROM pg_attribute + WHERE attrelid = table_name::regclass + AND attnum > 0 AND NOT attisdropped + AND attname != sys_period + AND (NOT increment_version OR attname != version_column_name) + ) main + INNER JOIN ( + SELECT attname + FROM pg_attribute + WHERE attrelid = history_table::regclass + AND attnum > 0 AND NOT attisdropped + AND attname != sys_period + AND (NOT increment_version OR attname != version_column_name) + ) hist + ON main.attname = hist.attname; + + -- For row comparison (unchanged values) + SELECT string_agg('NEW.' || quote_ident(main.attname), ',') + INTO new_row_compare + FROM ( + SELECT attname + FROM pg_attribute + WHERE attrelid = table_name::regclass + AND attnum > 0 AND NOT attisdropped + AND attname != sys_period + AND (NOT increment_version OR attname != version_column_name) + ) main + INNER JOIN ( + SELECT attname + FROM pg_attribute + WHERE attrelid = history_table::regclass + AND attnum > 0 AND NOT attisdropped + AND attname != sys_period + AND (NOT increment_version OR attname != version_column_name) + ) hist + ON main.attname = hist.attname; + SELECT string_agg('OLD.' || quote_ident(main.attname), ',') + INTO old_row_compare + FROM ( + SELECT attname + FROM pg_attribute + WHERE attrelid = table_name::regclass + AND attnum > 0 AND NOT attisdropped + AND attname != sys_period + AND (NOT increment_version OR attname != version_column_name) + ) main + INNER JOIN ( + SELECT attname + FROM pg_attribute + WHERE attrelid = history_table::regclass + AND attnum > 0 AND NOT attisdropped + AND attname != sys_period + AND (NOT increment_version OR attname != version_column_name) + ) hist + ON main.attname = hist.attname; + + -- Get sys_period type for validation + SELECT format_type(atttypid, null) INTO sys_period_type + FROM pg_attribute + WHERE attrelid = table_name::regclass AND attname = sys_period AND NOT attisdropped; + SELECT format_type(atttypid, null) INTO history_sys_period_type + FROM pg_attribute + WHERE attrelid = history_table::regclass AND attname = sys_period AND NOT attisdropped; + + -- Check sys_period type at render time + IF COALESCE(sys_period_type, 'invalid') != 'tstzrange' THEN + RAISE 'system period column % does not have type tstzrange', sys_period_type; + END IF; + IF COALESCE(history_sys_period_type, 'invalid') != 'tstzrange' THEN + RAISE 'history system period column % does not have type tstzrange', history_sys_period_type; + END IF; + + -- Check version column if increment_version is enabled + IF increment_version THEN + -- Check if version column exists in main table + IF NOT EXISTS(SELECT FROM pg_attribute WHERE attrelid = table_name::regclass AND attname = version_column_name AND NOT attisdropped) THEN + RAISE 'relation "%" does not contain version column "%"', table_name, version_column_name; + END IF; + + -- Check if version column exists in history table + IF NOT EXISTS(SELECT FROM pg_attribute WHERE attrelid = history_table::regclass AND attname = version_column_name AND NOT attisdropped) THEN + RAISE 'history relation "%" does not contain version column "%"', history_table, version_column_name; + END IF; + + -- Check version column type is integer + IF NOT EXISTS(SELECT FROM pg_attribute WHERE attrelid = table_name::regclass AND attname = version_column_name AND atttypid = 'integer'::regtype AND NOT attisdropped) THEN + RAISE 'version column "%" of relation "%" is not an integer', version_column_name, table_name; + END IF; + + -- Remove version column from common columns to handle it separately + SELECT string_agg(quote_ident(main.attname), ',') + INTO common_columns + FROM ( + SELECT attname + FROM pg_attribute + WHERE attrelid = table_name::regclass + AND attnum > 0 AND NOT attisdropped + AND attname != sys_period + AND attname != version_column_name + ) main + INNER JOIN ( + SELECT attname + FROM pg_attribute + WHERE attrelid = history_table::regclass + AND attnum > 0 AND NOT attisdropped + AND attname != sys_period + AND attname != version_column_name + ) hist + ON main.attname = hist.attname; + END IF; + + -- Prepare version-related variables for the format function + IF increment_version THEN + version_declare_var := E'\n existing_version integer;'; + version_init_logic := format(E' -- Initialize version handling\n IF TG_OP = ''INSERT'' THEN\n existing_version := 0;\n ELSIF TG_OP = ''UPDATE'' OR TG_OP = ''DELETE'' THEN\n existing_version := OLD.%I;\n IF existing_version IS NULL THEN\n RAISE ''version column "%%" of relation "%%" must not be null'', %L, TG_TABLE_NAME;\n END IF;\n END IF;\n', version_column_name, version_column_name); + version_column_insert := ', ' || quote_ident(version_column_name); + version_old_value := ', existing_version'; + version_new_value := ', existing_version + 1'; + version_increment_logic := format(E'\n NEW.%I := existing_version + 1;', version_column_name); + END IF; + + -- Build the main trigger logic conditionally at generation time + DECLARE + unchanged_check_logic text := ''; + conflict_mitigation_logic text := ''; + transaction_check_logic text := ''; + migration_check_logic text := ''; + current_version_update_logic text := ''; + variable_declarations text := E' time_stamp_to_use timestamptz;\n'; + update_delete_logic text := ''; + insert_update_logic text := ''; + BEGIN + -- Generate unchanged values check logic + IF ignore_unchanged_values THEN + unchanged_check_logic := format(E' IF TG_OP = ''UPDATE'' THEN\n IF (%s) IS NOT DISTINCT FROM (%s) THEN\n RETURN OLD;\n END IF;\n END IF;\n\n', new_row_compare, old_row_compare); + END IF; + + -- Generate conflict mitigation logic + IF mitigate_update_conflicts THEN + conflict_mitigation_logic := E' IF range_lower >= time_stamp_to_use THEN\n time_stamp_to_use := range_lower + interval ''1 microseconds'';\n END IF;\n'; + END IF; + + -- Generate transaction check logic (only if include_current_version_in_history is false) + IF NOT include_current_version_in_history THEN + transaction_check_logic := E' -- Ignore rows already modified in the current transaction\n IF OLD.xmin::TEXT = (txid_current() % (2^32)::BIGINT)::TEXT THEN\n IF TG_OP = ''DELETE'' THEN\n RETURN OLD;\n END IF;\n RETURN NEW;\n END IF;\n'; + END IF; + + -- Generate migration check logic + IF enable_migration_mode AND include_current_version_in_history THEN + migration_check_logic := format(E' -- Check if record exists in history table for migration mode\n IF TG_OP = ''UPDATE'' OR TG_OP = ''DELETE'' THEN\n SELECT EXISTS (\n SELECT FROM %s WHERE ROW(%s) IS NOT DISTINCT FROM ROW(%s)\n ) INTO record_exists;\n\n IF NOT record_exists THEN\n -- Insert current record into history table with its original range\n INSERT INTO %s (%s, %I%s) VALUES (%s, tstzrange(range_lower, time_stamp_to_use, ''[)'')%s);\n END IF;\n END IF;\n\n', + history_table, common_columns, old_row_compare, history_table, common_columns, sys_period, version_column_insert, old_row_compare, version_old_value); + END IF; + + -- Generate current version update logic for include_current_version_in_history mode + IF include_current_version_in_history THEN + current_version_update_logic := format(E' IF TG_OP = ''UPDATE'' OR TG_OP = ''DELETE'' THEN\n UPDATE %s SET %I = tstzrange(range_lower, time_stamp_to_use, ''[)'')\n WHERE (%s) = (%s) AND %I = OLD.%I;\n END IF;\n IF TG_OP = ''UPDATE'' OR TG_OP = ''INSERT'' THEN\n INSERT INTO %s (%s, %I%s) VALUES (%s, tstzrange(time_stamp_to_use, NULL, ''[)'')%s);\n END IF;\n', + history_table, sys_period, common_columns, common_columns, sys_period, sys_period, + history_table, common_columns, sys_period, version_column_insert, new_row_compare, version_new_value); + END IF; + + -- Add variables only when needed + IF enable_migration_mode AND include_current_version_in_history THEN + variable_declarations := variable_declarations || E' record_exists bool;\n'; + END IF; + + IF increment_version THEN + variable_declarations := variable_declarations || E' existing_version integer;\n'; + END IF; + + -- Only add range variables if we have UPDATE or DELETE operations + variable_declarations := variable_declarations || E' range_lower timestamptz;\n existing_range tstzrange;'; + + -- Build UPDATE/DELETE logic with integrated history handling + IF include_current_version_in_history THEN + update_delete_logic := format(E' IF TG_OP = ''UPDATE'' OR TG_OP = ''DELETE'' THEN\n existing_range := OLD.%1$I;\n IF existing_range IS NULL THEN\n RAISE ''system period column "%%" must not be null'', %2$L;\n END IF;\n IF isempty(existing_range) OR NOT upper_inf(existing_range) THEN\n RAISE ''system period column "%%" contains invalid value'', %2$L;\n END IF;\n range_lower := lower(existing_range);\n \n%3$s IF range_lower >= time_stamp_to_use THEN\n RAISE ''system period value of relation "%%" cannot be set to a valid period because a row that is attempted to modify was also modified by another transaction'', TG_TABLE_NAME USING\n ERRCODE = ''data_exception'',\n DETAIL = ''the start time of the system period is the greater than or equal to the time of the current transaction '';\n END IF;\n END IF;\n\n IF TG_OP = ''UPDATE'' OR TG_OP = ''DELETE'' OR TG_OP = ''INSERT'' THEN\n%4$s%5$s%6$s\n END IF;', + sys_period, sys_period, conflict_mitigation_logic, + transaction_check_logic, migration_check_logic, current_version_update_logic); + ELSE + update_delete_logic := format(E' IF TG_OP = ''UPDATE'' OR TG_OP = ''DELETE'' THEN\n existing_range := OLD.%1$I;\n IF existing_range IS NULL THEN\n RAISE ''system period column "%%" must not be null'', %2$L;\n END IF;\n IF isempty(existing_range) OR NOT upper_inf(existing_range) THEN\n RAISE ''system period column "%%" contains invalid value'', %2$L;\n END IF;\n range_lower := lower(existing_range);\n \n%3$s IF range_lower >= time_stamp_to_use THEN\n RAISE ''system period value of relation "%%" cannot be set to a valid period because a row that is attempted to modify was also modified by another transaction'', TG_TABLE_NAME USING\n ERRCODE = ''data_exception'',\n DETAIL = ''the start time of the system period is the greater than or equal to the time of the current transaction '';\n END IF;\n\n%4$s\n INSERT INTO %5$s (%6$s, %1$I%7$s) VALUES (%8$s, tstzrange(range_lower, time_stamp_to_use, ''[)'')%9$s);\n END IF;', + sys_period, sys_period, conflict_mitigation_logic, transaction_check_logic, + history_table, common_columns, version_column_insert, old_row_compare, version_old_value); + END IF; + + -- Build INSERT/UPDATE logic + IF include_current_version_in_history THEN + insert_update_logic := format(E' IF TG_OP = ''UPDATE'' OR TG_OP = ''INSERT'' THEN\n NEW.%1$I := tstzrange(time_stamp_to_use, NULL, ''[)'');%2$s\n RETURN NEW;\n END IF;\n\n RETURN OLD;', + sys_period, version_increment_logic); + ELSE + insert_update_logic := format(E' IF TG_OP = ''UPDATE'' OR TG_OP = ''INSERT'' THEN\n NEW.%1$I := tstzrange(time_stamp_to_use, NULL, ''[)'');%2$s\n RETURN NEW;\n END IF;\n\n RETURN OLD;', + sys_period, version_increment_logic); + END IF; + + func_sql := format($outer$ +CREATE OR REPLACE FUNCTION %1$I() +RETURNS TRIGGER AS $func$ +DECLARE +%2$s +BEGIN + -- set custom system time if exists + BEGIN + SELECT current_setting('user_defined.system_time') INTO STRICT time_stamp_to_use; + time_stamp_to_use := TO_TIMESTAMP(time_stamp_to_use::text, 'YYYY-MM-DD HH24:MI:SS.US'); + EXCEPTION WHEN OTHERS THEN + time_stamp_to_use := CURRENT_TIMESTAMP; + END; + +%3$s%4$s + +%5$s + +%6$s +END; +$func$ LANGUAGE plpgsql; +$outer$, + trigger_func_name, -- %1$s + variable_declarations, -- %2$s + unchanged_check_logic, -- %3$s + version_init_logic, -- %4$s + update_delete_logic, -- %5$s + insert_update_logic -- %6$s +); + END; + + trigger_sql := format($t$ +DROP TRIGGER IF EXISTS %1$I ON %2$s; +CREATE TRIGGER %1$I +BEFORE INSERT OR UPDATE OR DELETE ON %2$s +FOR EACH ROW EXECUTE FUNCTION %3$I(); +$t$, + trigger_name, + table_name, + trigger_func_name +); + + EXECUTE func_sql; + EXECUTE trigger_sql; +END; +$$ LANGUAGE plpgsql; diff --git a/test/e2e/README.md b/test/e2e/README.md new file mode 100644 index 0000000..916edb4 --- /dev/null +++ b/test/e2e/README.md @@ -0,0 +1,305 @@ +# End-to-End Tests for Temporal Tables + +This directory contains comprehensive end-to-end tests for the temporal tables PostgreSQL extension, written in modern TypeScript using Node.js built-in test runner and node-postgres. + +## Test Files + +### `db-helper.ts` +Database utility class that provides: +- Connection management +- SQL execution with transaction support +- Test data setup and cleanup +- SQL file loading and execution +- Type-safe query results + +### `test-static-generator.ts` +Tests for the static trigger generator functionality: +- Basic versioning operations (INSERT, UPDATE, DELETE) +- Advanced features (ignore unchanged values, custom system time) +- Error handling and validation +- Schema compatibility testing +- Performance edge cases + +### `test-legacy.ts` +Tests for the legacy versioning function: +- Backward compatibility with existing versioning function +- Parameter variations and options +- Custom system time handling +- Schema compatibility +- Comparison with static generator + +### `test-event-trigger.ts` +Tests for event trigger functionality: +- Automatic trigger re-rendering on schema changes +- Metadata table management +- Migration mode handling +- Complex schema evolution scenarios + +### `test-integration.ts` +Comprehensive integration tests: +- Real-world e-commerce scenarios +- Schema evolution and migration +- Performance and stress testing +- Referential integrity maintenance +- Error recovery and edge cases +- Concurrent modification handling + +### `test-performance-comparison.ts` +Performance benchmarking and comparison tests: +- Side-by-side performance comparison between legacy and static generator implementations +- Detailed timing analysis for INSERT, UPDATE, and DELETE operations +- Performance scaling tests with increasing data volumes +- Comprehensive reporting with formatted tables showing performance differences +- Analysis of performance improvements and optimization opportunities + +## Prerequisites + +1. **PostgreSQL Database**: Either via Docker or local installation +2. **Node.js**: Version 18+ (for built-in test runner) +3. **TypeScript**: For type checking and compilation + +## Database Setup + +### Option 1: Docker (Recommended) +```bash +# Start PostgreSQL database +npm run db:start + +# Stop database when done +npm run db:stop +``` + +### Option 2: Local PostgreSQL +If you have PostgreSQL installed locally: +1. Create a database (default: `postgres`) +2. Ensure the user has appropriate permissions +3. Set environment variables: + ```bash + export PGHOST=localhost + export PGPORT=5432 + export PGUSER=your_username + export PGPASSWORD=your_password + export PGDATABASE=your_database + ``` + +### Option 3: Remote PostgreSQL +You can also run tests against a remote PostgreSQL instance by setting the appropriate environment variables. + +## Running Tests + +### All E2E Tests +```bash +npm run test:e2e +``` + +### Individual Test Suites +```bash +# Static generator tests +npm run test:e2e:static + +# Legacy function tests +npm run test:e2e:legacy + +# Integration tests +npm run test:e2e:integration + +# Performance comparison tests +npm run test:e2e:performance +``` + +### Manual Test Execution +```bash +# Set environment variables +export PGHOST=localhost +export PGPORT=5432 +export PGUSER=postgres +export PGPASSWORD=password + +# Run specific test file +node --test test/e2e/test-static-generator.ts +``` + +## Test Architecture + +### Database Helper Pattern +The tests use a `DatabaseHelper` class that encapsulates: +- Connection management with proper cleanup +- Transaction handling for test isolation +- Type-safe query execution with proper error handling +- Automatic loading of SQL functions and extensions + +### Test Structure +Each test file follows a consistent pattern: +```typescript +describe('Feature Name', () => { + let db: DatabaseHelper + + before(async () => { + db = new DatabaseHelper() + await db.connect() + await db.setupVersioning() + }) + + after(async () => { + await db.cleanup() + await db.disconnect() + }) + + beforeEach(async () => { + // Clean up test tables + }) + + // Test cases... +}) +``` + +### Test Data Management +- Each test starts with a clean database state +- Tables are created and dropped per test to ensure isolation +- Helper functions create consistent test scenarios +- Proper cleanup prevents test interference + +## Test Coverage + +### Functional Testing +- ✅ Basic CRUD operations with versioning +- ✅ Advanced versioning features (ignore unchanged, custom time) +- ✅ Schema compatibility (quoted names, complex types) +- ✅ Error handling and validation +- ✅ Migration scenarios + +### Integration Testing +- ✅ Real-world application scenarios +- ✅ Schema evolution and trigger re-rendering +- ✅ Performance under load +- ✅ Concurrent access patterns +- ✅ Referential integrity maintenance + +### Performance Testing +- ✅ Comparative benchmarking between legacy and static implementations +- ✅ Performance scaling analysis with increasing data volumes +- ✅ Detailed timing measurements for all CRUD operations +- ✅ Performance regression detection and improvement analysis +- ✅ Professional formatted reports with alignment and clear metrics + +### Edge Cases +- ✅ Transaction rollback handling +- ✅ Rapid sequential updates +- ✅ Bulk data operations +- ✅ Complex data types (JSON, arrays, custom types) +- ✅ Migration mode with existing data + +## Performance Testing + +The performance comparison tests provide comprehensive benchmarking capabilities to evaluate the effectiveness of the static trigger generator versus the legacy versioning function. + +### Test Structure +The performance tests include: + +1. **Implementation Comparison**: Side-by-side testing of legacy function vs static generator +2. **Operation Benchmarking**: Detailed timing for INSERT, UPDATE, and DELETE operations +3. **Scaling Analysis**: Performance evaluation with increasing data volumes (100, 500, 1000+ records) +4. **Professional Reporting**: Formatted tables with alignment, borders, and clear metrics + +### Sample Output +``` +┌───────────────────────────────────────────────────────────────────────────────────┐ +│ Performance Comparison Report │ +├─────────────┬──────────────┬──────────────┬─────────────┬─────────────────────────┤ +│ Operation │ Legacy (ms) │ Static (ms) │ Difference │ Improvement │ +├─────────────┼──────────────┼──────────────┼─────────────┼─────────────────────────┤ +│ INSERT │ 45.23 │ 12.67 │ 32.56 │ 72.0% faster │ +│ UPDATE │ 38.91 │ 10.44 │ 28.47 │ 73.2% faster │ +│ DELETE │ 31.78 │ 9.12 │ 22.66 │ 71.3% faster │ +└─────────────┴──────────────┴──────────────┴─────────────┴─────────────────────────┘ +``` + +### Running Performance Tests +```bash +# Run performance comparison +npm run test:e2e:performance + +# Run with detailed output +NODE_ENV=development npm run test:e2e:performance +``` + +## Database Configuration + +The tests expect a PostgreSQL database with: +- Host: `localhost` (configurable via `PGHOST`) +- Port: `5432` (configurable via `PGPORT`) +- User: `postgres` (configurable via `PGUSER`) +- Password: `password` (configurable via `PGPASSWORD`) +- Database: `postgres` (configurable via `PGDATABASE`) + +## TypeScript Configuration + +The tests are written in TypeScript and use: +- Modern ES modules and async/await +- Strict type checking with proper interfaces +- Node.js built-in modules (`node:assert`, `node:test`) +- Type-safe database interactions + +## Troubleshooting + +### Database Connection Issues + +#### Docker Environment +```bash +# Check if database is running +docker ps + +# View database logs +docker logs temporal-tables-test + +# Restart database +npm run db:stop +npm run db:start +``` + +#### Local PostgreSQL +```bash +# Check if PostgreSQL is running +pg_isready -h localhost -p 5432 + +# Connect manually to test +psql -h localhost -p 5432 -U postgres + +# Check PostgreSQL service status (Linux/macOS) +sudo systemctl status postgresql + +# Check PostgreSQL service status (Windows) +net start | findstr postgres +``` + +### Test Failures +- Ensure database is running and accessible +- Check that all SQL files are present in the root directory +- Verify environment variables are set correctly +- Look for port conflicts (PostgreSQL on 5432) +- Ensure the user has CREATE/DROP permissions on the database + +### Performance Issues +- Tests include intentional delays (`pg_sleep`) for timestamp differentiation +- Large datasets in performance tests may take time +- Concurrent tests may be slower due to database locking +- Performance comparison tests may take longer due to multiple benchmark runs +- Ensure the database server has adequate resources for accurate performance measurements + +## Contributing + +When adding new tests: +1. Follow the existing pattern and structure +2. Use descriptive test names and group related tests +3. Ensure proper cleanup in `beforeEach`/`afterEach` +4. Add type annotations for better IDE support +5. Include both positive and negative test cases +6. Test edge cases and error conditions + +## Notes + +- Tests use the Node.js built-in test runner (no Jest dependency) +- All assertions use Node.js built-in `assert` module +- Database operations are properly typed with TypeScript +- Each test suite is independent and can run in isolation +- Tests are designed to be deterministic and repeatable diff --git a/test/e2e/db-helper.ts b/test/e2e/db-helper.ts new file mode 100644 index 0000000..f172a3f --- /dev/null +++ b/test/e2e/db-helper.ts @@ -0,0 +1,208 @@ +import { Client, ClientConfig, QueryResult } from 'pg' +import { readFileSync } from 'fs' +import { join } from 'path' +import * as url from 'url' + +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) + +export interface DatabaseRow { + [key: string]: any +} + +export interface TestResult { + rows: DatabaseRow[] + command: string + rowCount: number +} + +export class DatabaseHelper { + static modernMinimumPostgresVersion = '13.21' as const + + private client: Client + private isConnected = false + + constructor(config?: ClientConfig) { + const defaultConfig: ClientConfig = { + host: process.env.PGHOST || 'localhost', + port: parseInt(process.env.PGPORT || '5432'), + user: process.env.PGUSER || 'postgres', + password: process.env.PGPASSWORD || 'password', + database: process.env.PGDATABASE || 'postgres' + } + + this.client = new Client({ ...defaultConfig, ...config }) + } + + async cleanup(): Promise { + // Drop all test tables and functions + const tables = await this.query(` + SELECT tablename + FROM pg_tables + WHERE schemaname = 'public' + AND (tablename LIKE '%versioning%' OR tablename LIKE '%test%') + `) + + for (const table of tables.rows) + await this.query(`DROP TABLE IF EXISTS ${table.tablename} CASCADE`) + + // Clean up any test functions + const functions = await this.query(` + SELECT proname + FROM pg_proc + WHERE proname LIKE '%test%' OR proname LIKE '%versioning%' + `) + + for (const func of functions.rows) + await this.query(`DROP FUNCTION IF EXISTS ${func.proname}() CASCADE`) + } + + async connect(): Promise { + if (!this.isConnected) { + await this.client.connect() + this.isConnected = true + } + } + + async disconnect(): Promise { + if (this.isConnected) { + await this.client.end() + this.isConnected = false + } + } + + async ensureTimestampGap(): Promise { + // Ensure a small gap between timestamps to avoid timing races + await this.sleep(0.001) // 1 millisecond gap + } + + async executeTransaction(sqlStatements: string[]): Promise { + const results: TestResult[] = [] + + await this.query('BEGIN') + + try { + for (const sql of sqlStatements) + if (sql.trim()) { + const result = await this.query(sql) + results.push(result) + } + await this.query('COMMIT') + } catch (error) { + await this.query('ROLLBACK') + throw error + } + + return results + } + + async getCurrentTimestamp(): Promise { + const result = await this.query('SELECT CURRENT_TIMESTAMP as now') + return result.rows[0].now + } + + async getReliableTimestamp(): Promise { + // Get timestamp and ensure it's unique by adding a small delay + const timestamp = await this.getCurrentTimestamp() + await this.ensureTimestampGap() + return timestamp + } + + async getTableStructure(tableName: string): Promise { + const result = await this.query( + ` + SELECT column_name, data_type, is_nullable, column_default + FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = $1 + ORDER BY ordinal_position + `, + [tableName] + ) + + return result.rows + } + + async isTimestampInRange( + timestamp: Date, + beforeTime: Date, + afterTime: Date, + toleranceMs: number = 1000 // 1 second tolerance by default + ): Promise { + const timestampMs = timestamp.getTime() + const beforeMs = beforeTime.getTime() - toleranceMs + const afterMs = afterTime.getTime() + toleranceMs + + return timestampMs >= beforeMs && timestampMs <= afterMs + } + + async loadAndExecuteSqlFile(filePath: string): Promise { + await this.query(readFileSync(filePath, 'utf-8')) + } + + async query(sql: string, params?: any[]): Promise { + const result: QueryResult = await this.client.query(sql, params) + return { + rows: result.rows, + command: result.command, + rowCount: result.rowCount ?? 0 + } + } + + async setupVersioning(minimumServerVersion: string = '0.0'): Promise { + const rootPath = join(__dirname, '..', '..') + + // Always load legacy functionality (works on any Postgres version) + const legacySqlFiles = [ + 'versioning_function.sql', + 'system_time_function.sql' + ] + + // Modern functionality requires Postgres 13+ + const modernSqlFiles = [ + 'versioning_tables_metadata.sql', + 'render_versioning_trigger.sql', + 'event_trigger_versioning.sql' + ] + + // Verify PostgreSQL version first + if (!(await this.verifyPostgresVersion(minimumServerVersion))) + process.exit(0) + + // Always load legacy files + for (const filename of legacySqlFiles) + await this.loadAndExecuteSqlFile(join(rootPath, filename)) + + // Only load modern files if we meet the minimum version requirement + // (minimum version 13.0 or higher for new functionality) + const [majorVersion] = minimumServerVersion.split('.').map(Number) + if (majorVersion >= 13) + for (const filename of modernSqlFiles) + await this.loadAndExecuteSqlFile(join(rootPath, filename)) + } + + async sleep(seconds: number): Promise { + await this.query(`SELECT pg_sleep($1)`, [seconds]) + } + + async tableExists(tableName: string): Promise { + const result = await this.query( + ` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = $1 + ) + `, + [tableName] + ) + + return result.rows[0].exists + } + + async verifyPostgresVersion(minVersion: string): Promise { + const result = await this.query('SHOW server_version') + const version = result.rows[0].server_version + const [major, minor] = version.split('.').map(Number) // Split version into major and minor parts + const [minMajor, minMinor] = minVersion.split('.').map(Number) // Split minVersion into major and minor parts + return !(major < minMajor || (major === minMajor && minor < minMinor)) + } +} diff --git a/test/e2e/run-tests.ts b/test/e2e/run-tests.ts new file mode 100644 index 0000000..99ff010 --- /dev/null +++ b/test/e2e/run-tests.ts @@ -0,0 +1,179 @@ +#!/usr/bin/env node + +import { spawn, ChildProcess } from 'child_process' +import { fileURLToPath } from 'url' +import { dirname, join } from 'path' +import { existsSync } from 'fs' + +const __filename: string = fileURLToPath(import.meta.url) +const __dirname: string = dirname(__filename) + +interface TestEnvironment { + PGHOST: string + PGPORT: string + PGUSER: string + PGPASSWORD: string + PGDATABASE: string + [key: string]: string | undefined +} + +interface TestResult { + testFile: string + success: boolean + stdout: string + stderr: string + code?: number +} + +interface TestError { + testFile: string + error: Error +} + +const testFiles: string[] = [ + 'test-static-generator.ts', + 'test-legacy.ts', + 'test-event-trigger.ts', + 'test-integration.ts', + 'test-increment-version.ts' +] + +const env: TestEnvironment = { + ...process.env, + PGHOST: process.env.PGHOST || 'localhost', + PGPORT: process.env.PGPORT || '5432', + PGUSER: process.env.PGUSER || 'postgres', + PGPASSWORD: process.env.PGPASSWORD || 'password', + PGDATABASE: process.env.PGDATABASE || 'postgres' +} as TestEnvironment + +console.log('🚀 Running Temporal Tables E2E Tests') +console.log('=====================================') +console.log( + `Database: ${env.PGUSER}@${env.PGHOST}:${env.PGPORT}/${env.PGDATABASE}` +) +console.log('') + +async function runTest(testFile: string): Promise { + return new Promise((resolve, reject) => { + console.log(`📋 Running ${testFile}...`) + + const testPath: string = join(__dirname, testFile) + + // Use the same execution pattern as the working npm scripts + const nodeArgs: string[] = [ + '--env-file-if-exists', + './.env', + '--loader', + 'ts-node/esm/transpile-only', + '--test', + testPath + ] + + const child: ChildProcess = spawn('node', nodeArgs, { + env, + stdio: 'pipe', + shell: process.platform === 'win32', + cwd: join(__dirname, '../..') // Run from project root + }) + + let stdout: string = '' + let stderr: string = '' + + child.stdout?.on('data', (data: Buffer) => { + stdout += data.toString() + }) + + child.stderr?.on('data', (data: Buffer) => { + stderr += data.toString() + }) + + child.on('close', (code: number | null) => { + if (code === 0) { + console.log(`✅ ${testFile} passed`) + console.log(stdout) + resolve({ testFile, success: true, stdout, stderr }) + } else { + console.error(`❌ ${testFile} failed (exit code: ${code})`) + console.error(stderr) + console.error(stdout) + resolve({ + testFile, + success: false, + stdout, + stderr, + code: code || undefined + }) + } + }) + + child.on('error', (error: Error) => { + console.error(`❌ ${testFile} error:`, error) + reject({ testFile, error }) + }) + }) +} + +async function runAllTests(): Promise { + const results: (TestResult | TestError)[] = [] + + for (const testFile of testFiles) { + try { + const result: TestResult = await runTest(testFile) + results.push(result) + console.log('') // Add spacing between tests + } catch (error) { + results.push(error as TestError) + console.error(`Failed to run ${testFile}:`, error) + console.log('') + } + } + + // Summary + console.log('📊 Test Summary') + console.log('===============') + + const passed: number = results.filter( + (r): r is TestResult => 'success' in r && r.success + ).length + const failed: number = results.filter( + (r): r is TestResult => 'success' in r && !r.success + ).length + + console.log(`✅ Passed: ${passed}`) + console.log(`❌ Failed: ${failed}`) + console.log(`📁 Total: ${results.length}`) + + if (failed > 0) { + console.log('') + console.log('Failed tests:') + results + .filter((r): r is TestResult => 'success' in r && !r.success) + .forEach((r: TestResult) => { + console.log(` - ${r.testFile}`) + }) + process.exit(1) + } else { + console.log('') + console.log('🎉 All tests passed!') + process.exit(0) + } +} + +// Check if specific test file was requested +const requestedTest: string | undefined = process.argv[2] +if (requestedTest && testFiles.includes(requestedTest)) { + runTest(requestedTest) + .then((result: TestResult) => { + process.exit(result.success ? 0 : 1) + }) + .catch((error: TestError) => { + console.error('Error running test:', error) + process.exit(1) + }) +} else { + runAllTests().catch((error: Error) => { + console.error('Error running tests:', error) + process.exit(1) + }) +} diff --git a/test/e2e/test-event-trigger.ts b/test/e2e/test-event-trigger.ts new file mode 100644 index 0000000..eed88f3 --- /dev/null +++ b/test/e2e/test-event-trigger.ts @@ -0,0 +1,422 @@ +import { deepStrictEqual, ok, rejects } from 'node:assert' +import { describe, test, before, after, beforeEach } from 'node:test' +import * as url from 'url' +import { DatabaseHelper } from './db-helper.js' + +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) + +describe('Event Trigger Versioning E2E Tests', () => { + let db: DatabaseHelper + + before(async () => { + db = new DatabaseHelper() + await db.connect() + await db.setupVersioning(DatabaseHelper.modernMinimumPostgresVersion) + }) + + after(async () => { + await db.cleanup() + await db.disconnect() + }) + + beforeEach(async () => { + // Clean up any existing test tables and metadata + await db.query('DROP TABLE IF EXISTS subscriptions CASCADE') + await db.query('DROP TABLE IF EXISTS subscriptions_history CASCADE') + await db.query('DROP TABLE IF EXISTS users CASCADE') + await db.query('DROP TABLE IF EXISTS users_history CASCADE') + await db.query( + "DELETE FROM versioning_tables_metadata WHERE table_schema = 'public'" + ) + }) + + describe('Event Trigger Setup and Management', () => { + test('should create versioning metadata table', async () => { + const tableExists = await db.tableExists('versioning_tables_metadata') + ok(tableExists, 'Versioning metadata table should exist') + + // Check table structure + const structure = await db.getTableStructure('versioning_tables_metadata') + const hasTableName = structure.some( + col => col.column_name === 'table_name' + ) + const hasTableSchema = structure.some( + col => col.column_name === 'table_schema' + ) + + ok(hasTableName, 'Should have table_name column') + ok(hasTableSchema, 'Should have table_schema column') + }) + + test('should register tables in metadata for automatic re-rendering', async () => { + // Register a table for versioning + await db.query(` + INSERT INTO versioning_tables_metadata (table_name, table_schema, history_table, history_table_schema, sys_period) + VALUES ('subscriptions', 'public', 'subscriptions_history', 'public', 'sys_period') + `) + + // Verify registration + const result = await db.query(` + SELECT * FROM versioning_tables_metadata + WHERE table_name = 'subscriptions' AND table_schema = 'public' + `) + + deepStrictEqual(result.rows.length, 1) + deepStrictEqual(result.rows[0].table_name, 'subscriptions') + deepStrictEqual(result.rows[0].table_schema, 'public') + }) + + test('should create render_versioning_trigger procedure', async () => { + // Test the procedure + await db.query(` + CREATE TABLE test_table ( + id bigint, + name text, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE test_table_history ( + id bigint, + name text, + sys_period tstzrange + ) + `) + + // Call the procedure + await db.query(` + CALL render_versioning_trigger( + 'test_table', + 'test_table_history', + 'sys_period' + ) + `) + + // Verify trigger was created by testing functionality + await db.query("INSERT INTO test_table (id, name) VALUES (1, 'test')") + + const result = await db.query('SELECT * FROM test_table WHERE id = 1') + deepStrictEqual(result.rows.length, 1) + ok(result.rows[0].sys_period, 'sys_period should be set by trigger') + }) + }) + + describe('Automatic Trigger Re-rendering', () => { + test('should handle table alterations and re-render triggers', async () => { + // Create versioned table + await db.query(` + CREATE TABLE users ( + id bigint, + email text, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE users_history ( + id bigint, + email text, + sys_period tstzrange + ) + `) + + // Register for versioning + await db.query(` + INSERT INTO versioning_tables_metadata (table_name, table_schema, history_table, history_table_schema, sys_period) + VALUES ('users', 'public', 'users_history', 'public', 'sys_period') + `) + + // Create initial trigger + await db.query(` + CALL render_versioning_trigger('users', 'users_history', 'sys_period') + `) + + // Test initial functionality + await db.query( + "INSERT INTO users (id, email) VALUES (1, 'test@example.com')" + ) + + let result = await db.query('SELECT * FROM users WHERE id = 1') + deepStrictEqual(result.rows.length, 1) + + // Alter the table (add column) + await db.query('ALTER TABLE users ADD COLUMN name text') + await db.query('ALTER TABLE users_history ADD COLUMN name text') + + // Test that versioning still works with new column + await db.query( + "INSERT INTO users (id, email, name) VALUES (2, 'test2@example.com', 'Test User')" + ) + + result = await db.query('SELECT * FROM users WHERE id = 2') + deepStrictEqual(result.rows.length, 1) + deepStrictEqual(result.rows[0].name, 'Test User') + + // Test update to create history + await db.sleep(0.1) + await db.query("UPDATE users SET name = 'Updated User' WHERE id = 2") + + const historyResult = await db.query( + 'SELECT * FROM users_history WHERE id = 2' + ) + deepStrictEqual(historyResult.rows.length, 1) + deepStrictEqual(historyResult.rows[0].name, 'Test User') // Original value in history + }) + + test('should handle schema changes gracefully', async () => { + // Create table with initial schema + await db.query(` + CREATE TABLE subscriptions ( + id bigint, + user_id bigint, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE subscriptions_history ( + id bigint, + user_id bigint, + sys_period tstzrange + ) + `) + + // Register for versioning + await db.query(` + INSERT INTO versioning_tables_metadata (table_name, table_schema, history_table, history_table_schema, sys_period) + VALUES ('subscriptions', 'public', 'subscriptions_history', 'public', 'sys_period') + `) + + // Generate initial trigger + await db.query(` + CALL render_versioning_trigger('subscriptions', 'subscriptions_history', 'sys_period') + `) + + // Insert test data + await db.query('INSERT INTO subscriptions (id, user_id) VALUES (1, 100)') + + // Add column to both tables + await db.query( + "ALTER TABLE subscriptions ADD COLUMN plan_type text DEFAULT 'basic'" + ) + await db.query( + 'ALTER TABLE subscriptions_history ADD COLUMN plan_type text' + ) + + // Test that new column is handled correctly + await db.query( + "INSERT INTO subscriptions (id, user_id, plan_type) VALUES (2, 200, 'premium')" + ) + + await db.sleep(0.1) + + await db.query( + "UPDATE subscriptions SET plan_type = 'enterprise' WHERE id = 2" + ) + + // Verify history includes new column + const historyResult = await db.query(` + SELECT id, user_id, plan_type + FROM subscriptions_history + WHERE id = 2 + `) + + deepStrictEqual(historyResult.rows.length, 1) + deepStrictEqual(historyResult.rows[0].plan_type, 'premium') // Original value + }) + }) + + describe('Migration Mode with Event Triggers', () => { + test('should handle migration mode correctly', async () => { + // Create tables + await db.query(` + CREATE TABLE users ( + id bigint, + email text, + created_at timestamp, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE users_history ( + id bigint, + email text, + created_at timestamp, + sys_period tstzrange + ) + `) + + // Generate trigger with migration mode enabled + await db.query(` + CALL render_versioning_trigger( + 'users', + 'users_history', + 'sys_period', + enable_migration_mode => true + ) + `) + + // Insert existing data with historical periods + const oldTime = '2023-01-01 10:00:00+00' + const midTime = '2023-06-01 10:00:00+00' + + await db.query( + ` + INSERT INTO users (id, email, created_at, sys_period) + VALUES (1, 'old@example.com', '2023-01-01', tstzrange($1, $2)) + `, + [oldTime, midTime] + ) + + // Update should handle migration mode + await db.query( + "UPDATE users SET email = 'updated@example.com' WHERE id = 1" + ) + + // Check that history was created appropriately + const historyResult = await db.query( + 'SELECT * FROM users_history WHERE id = 1' + ) + ok( + historyResult.rows.length > 0, + 'History should be created in migration mode' + ) + + const mainResult = await db.query('SELECT * FROM users WHERE id = 1') + deepStrictEqual(mainResult.rows.length, 1) + deepStrictEqual(mainResult.rows[0].email, 'updated@example.com') + }) + }) + + describe('Error Handling in Event Triggers', () => { + test('should handle missing history table gracefully', async () => { + await db.query(` + CREATE TABLE IF NOT EXISTS orphan_table ( + id bigint, + data text, + sys_period tstzrange + ) + `) + + // Attempt to create trigger should fail gracefully + await rejects(async () => { + await db.query(` + CALL render_versioning_trigger( + 'orphan_table', + 'orphan_table_history', + 'sys_period' + ) + `) + }) + }) + + test('should validate system period column exists', async () => { + await db.query(` + DROP TABLE IF EXISTS invalid_period_table CASCADE + `) + + await db.query(` + DROP TABLE IF EXISTS invalid_period_table_history CASCADE + `) + + await db.query(` + CREATE TABLE invalid_period_table ( + id bigint, + data text + -- Missing sys_period column + ) + `) + + await db.query(` + CREATE TABLE invalid_period_table_history ( + id bigint, + data text, + sys_period tstzrange + ) + `) + + // Should fail when trying to generate trigger + await rejects(async () => { + await db.query(` + CALL render_versioning_trigger( + 'invalid_period_table', + 'invalid_period_table_history', + 'sys_period' + ) + `) + }) + }) + }) + + describe('Complex Schema Scenarios', () => { + test('should handle tables with complex data types', async () => { + await db.query(` + DROP TABLE IF EXISTS complex_table CASCADE + `) + + await db.query(` + DROP TABLE IF EXISTS complex_table_history CASCADE + `) + + await db.query(` + CREATE TABLE complex_table ( + id bigint, + metadata jsonb, + tags text[], + coordinates point, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE complex_table_history ( + id bigint, + metadata jsonb, + tags text[], + coordinates point, + sys_period tstzrange + ) + `) + + await db.query(` + CALL render_versioning_trigger( + 'complex_table', + 'complex_table_history', + 'sys_period' + ) + `) + + // Test with complex data + await db.query(` + INSERT INTO complex_table (id, metadata, tags, coordinates) + VALUES ( + 1, + '{"key": "value", "nested": {"array": [1,2,3]}}', + ARRAY['tag1', 'tag2', 'tag3'], + point(1.0, 2.0) + ) + `) + + await db.sleep(0.1) + + await db.query(` + UPDATE complex_table + SET metadata = '{"key": "updated", "new": true}' + WHERE id = 1 + `) + + // Verify complex types are preserved in history + const historyResult = await db.query( + 'SELECT * FROM complex_table_history WHERE id = 1' + ) + deepStrictEqual(historyResult.rows.length, 1) + + const historyRow = historyResult.rows[0] + ok(historyRow.metadata, 'JSON metadata should be preserved') + ok(historyRow.tags, 'Array should be preserved') + ok(historyRow.coordinates, 'Point should be preserved') + }) + }) +}) diff --git a/test/e2e/test-increment-version.ts b/test/e2e/test-increment-version.ts new file mode 100644 index 0000000..1ba97ee --- /dev/null +++ b/test/e2e/test-increment-version.ts @@ -0,0 +1,450 @@ +import { deepStrictEqual, ok } from 'node:assert' +import { describe, test, before, after, beforeEach } from 'node:test' +import { DatabaseHelper } from './db-helper.js' + +describe('Increment Version E2E Tests', () => { + let db: DatabaseHelper + + before(async () => { + db = new DatabaseHelper() + await db.connect() + await db.setupVersioning(DatabaseHelper.modernMinimumPostgresVersion) + }) + + after(async () => { + await db.cleanup() + await db.disconnect() + }) + + beforeEach(async () => { + // Clean up any existing test tables + await db.query('DROP TABLE IF EXISTS increment_version_test CASCADE') + await db.query( + 'DROP TABLE IF EXISTS increment_version_test_history CASCADE' + ) + await db.query( + 'DROP TABLE IF EXISTS increment_version_with_history_test CASCADE' + ) + await db.query( + 'DROP TABLE IF EXISTS increment_version_with_history_test_history CASCADE' + ) + }) + + describe('Basic Increment Version Functionality', () => { + test('should increment version on INSERT, UPDATE, and DELETE', async () => { + // Create tables with version column + await db.query(` + CREATE TABLE increment_version_test ( + id serial primary key, + data text, + version integer DEFAULT 1, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE increment_version_test_history ( + id integer, + data text, + version integer, + sys_period tstzrange + ) + `) + + // Generate trigger with increment_version = true + await db.query(` + CALL render_versioning_trigger( + table_name => 'increment_version_test', + history_table => 'increment_version_test_history', + sys_period => 'sys_period', + increment_version => true + ) + `) + + // Test INSERT + await db.executeTransaction([ + "INSERT INTO increment_version_test (data) VALUES ('initial version')" + ]) + + let result = await db.query( + 'SELECT data, version FROM increment_version_test' + ) + deepStrictEqual(result.rows.length, 1) + deepStrictEqual(result.rows[0].data, 'initial version') + deepStrictEqual(result.rows[0].version, 1) + + // History should be empty for INSERT + result = await db.query( + 'SELECT data, version FROM increment_version_test_history' + ) + deepStrictEqual(result.rows.length, 0) + + // Test UPDATE + await db.executeTransaction([ + "UPDATE increment_version_test SET data = 'second version' WHERE id = 1" + ]) + + result = await db.query( + 'SELECT data, version FROM increment_version_test' + ) + deepStrictEqual(result.rows.length, 1) + deepStrictEqual(result.rows[0].data, 'second version') + deepStrictEqual(result.rows[0].version, 2) + + // History should contain the old version + result = await db.query( + 'SELECT data, version, upper(sys_period) IS NOT NULL as history_ended FROM increment_version_test_history' + ) + deepStrictEqual(result.rows.length, 1) + deepStrictEqual(result.rows[0].data, 'initial version') + deepStrictEqual(result.rows[0].version, 1) + ok(result.rows[0].history_ended) + + // Test another UPDATE + await db.executeTransaction([ + "UPDATE increment_version_test SET data = 'third version' WHERE id = 1" + ]) + + result = await db.query( + 'SELECT data, version FROM increment_version_test' + ) + deepStrictEqual(result.rows.length, 1) + deepStrictEqual(result.rows[0].data, 'third version') + deepStrictEqual(result.rows[0].version, 3) + + // History should contain both old versions + result = await db.query( + 'SELECT data, version, upper(sys_period) IS NOT NULL as history_ended FROM increment_version_test_history ORDER BY version' + ) + deepStrictEqual(result.rows.length, 2) + deepStrictEqual(result.rows[0].data, 'initial version') + deepStrictEqual(result.rows[0].version, 1) + deepStrictEqual(result.rows[1].data, 'second version') + deepStrictEqual(result.rows[1].version, 2) + + // Test DELETE + await db.executeTransaction([ + 'DELETE FROM increment_version_test WHERE id = 1' + ]) + + result = await db.query('SELECT * FROM increment_version_test') + deepStrictEqual(result.rows.length, 0) + + // History should contain all versions + result = await db.query( + 'SELECT data, version, upper(sys_period) IS NOT NULL as history_ended FROM increment_version_test_history ORDER BY version' + ) + deepStrictEqual(result.rows.length, 3) + deepStrictEqual(result.rows[0].data, 'initial version') + deepStrictEqual(result.rows[0].version, 1) + deepStrictEqual(result.rows[1].data, 'second version') + deepStrictEqual(result.rows[1].version, 2) + deepStrictEqual(result.rows[2].data, 'third version') + deepStrictEqual(result.rows[2].version, 3) + }) + + test('should work with include_current_version_in_history', async () => { + // Create tables with version column + await db.query(` + CREATE TABLE increment_version_with_history_test ( + id serial primary key, + data text, + version integer DEFAULT 1, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE increment_version_with_history_test_history ( + id integer, + data text, + version integer, + sys_period tstzrange + ) + `) + + // Generate trigger with increment_version and include_current_version_in_history + await db.query(` + CALL render_versioning_trigger( + table_name => 'increment_version_with_history_test', + history_table => 'increment_version_with_history_test_history', + sys_period => 'sys_period', + include_current_version_in_history => true, + increment_version => true + ) + `) + + // Test INSERT + await db.executeTransaction([ + "INSERT INTO increment_version_with_history_test (data) VALUES ('initial version')" + ]) + + let result = await db.query( + 'SELECT data, version FROM increment_version_with_history_test' + ) + deepStrictEqual(result.rows.length, 1) + deepStrictEqual(result.rows[0].data, 'initial version') + deepStrictEqual(result.rows[0].version, 1) + + // History should contain current version for INSERT + result = await db.query( + 'SELECT data, version FROM increment_version_with_history_test_history' + ) + deepStrictEqual(result.rows.length, 1) + deepStrictEqual(result.rows[0].data, 'initial version') + deepStrictEqual(result.rows[0].version, 1) + + // Test UPDATE + await db.executeTransaction([ + "UPDATE increment_version_with_history_test SET data = 'second version' WHERE id = 1" + ]) + + result = await db.query( + 'SELECT data, version FROM increment_version_with_history_test' + ) + deepStrictEqual(result.rows.length, 1) + deepStrictEqual(result.rows[0].data, 'second version') + deepStrictEqual(result.rows[0].version, 2) + + // History should contain both old and current versions + result = await db.query( + 'SELECT data, version, upper(sys_period) IS NOT NULL as history_ended FROM increment_version_with_history_test_history ORDER BY version' + ) + deepStrictEqual(result.rows.length, 2) + deepStrictEqual(result.rows[0].data, 'initial version') + deepStrictEqual(result.rows[0].version, 1) + ok(result.rows[0].history_ended) + deepStrictEqual(result.rows[1].data, 'second version') + deepStrictEqual(result.rows[1].version, 2) + ok(!result.rows[1].history_ended) // Current version has open period + }) + + test('should handle custom version column name', async () => { + // Create tables with custom version column name + await db.query(` + CREATE TABLE increment_version_test ( + id serial primary key, + data text, + rev_number integer DEFAULT 1, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE increment_version_test_history ( + id integer, + data text, + rev_number integer, + sys_period tstzrange + ) + `) + + // Generate trigger with custom version column name + await db.query(` + CALL render_versioning_trigger( + table_name => 'increment_version_test', + history_table => 'increment_version_test_history', + sys_period => 'sys_period', + increment_version => true, + version_column_name => 'rev_number' + ) + `) + + // Test functionality + await db.executeTransaction([ + "INSERT INTO increment_version_test (data) VALUES ('test data')" + ]) + + await db.executeTransaction([ + "UPDATE increment_version_test SET data = 'updated data' WHERE id = 1" + ]) + + const result = await db.query( + 'SELECT data, rev_number FROM increment_version_test' + ) + deepStrictEqual(result.rows.length, 1) + deepStrictEqual(result.rows[0].data, 'updated data') + deepStrictEqual(result.rows[0].rev_number, 2) + }) + + test('should validate version column exists and is integer', async () => { + // Create table without version column + await db.query(` + CREATE TABLE increment_version_test ( + id serial primary key, + data text, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE increment_version_test_history ( + id integer, + data text, + sys_period tstzrange + ) + `) + + // Should fail when trying to generate trigger without version column + try { + await db.query(` + CALL render_versioning_trigger( + table_name => 'increment_version_test', + history_table => 'increment_version_test_history', + sys_period => 'sys_period', + increment_version => true + ) + `) + throw new Error('Should have failed') + } catch (error: any) { + ok( + error.message.includes('does not contain version column'), + 'Should fail with missing version column error' + ) + } + }) + + test('should work with render_versioning_trigger procedure', async () => { + // Create tables with version column + await db.query(` + CREATE TABLE increment_version_test ( + id serial primary key, + data text, + version integer DEFAULT 1, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE increment_version_test_history ( + id integer, + data text, + version integer, + sys_period tstzrange + ) + `) + + // Use render procedure with increment_version + await db.query(` + CALL render_versioning_trigger( + table_name => 'increment_version_test', + history_table => 'increment_version_test_history', + sys_period => 'sys_period', + increment_version => true + ) + `) + + // Test functionality + await db.executeTransaction([ + "INSERT INTO increment_version_test (data) VALUES ('test data')" + ]) + + await db.executeTransaction([ + "UPDATE increment_version_test SET data = 'updated data' WHERE id = 1" + ]) + + const result = await db.query( + 'SELECT data, version FROM increment_version_test' + ) + deepStrictEqual(result.rows.length, 1) + deepStrictEqual(result.rows[0].data, 'updated data') + deepStrictEqual(result.rows[0].version, 2) + }) + }) + + describe('Integration with Metadata and Event Triggers', () => { + test('should work with versioning metadata table', async () => { + // Create tables with version column + await db.query(` + CREATE TABLE increment_version_test ( + id serial primary key, + data text, + version integer DEFAULT 1, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE increment_version_test_history ( + id integer, + data text, + version integer, + sys_period tstzrange + ) + `) + + // Register in metadata table with increment_version + await db.query(` + INSERT INTO versioning_tables_metadata ( + table_name, + table_schema, + history_table, + history_table_schema, + sys_period, + ignore_unchanged_values, + include_current_version_in_history, + mitigate_update_conflicts, + enable_migration_mode, + increment_version, + version_column_name + ) VALUES ( + 'increment_version_test', + 'public', + 'increment_version_test_history', + 'public', + 'sys_period', + false, + false, + false, + false, + true, + 'version' + ) + `) + + // Generate initial trigger + await db.query(` + CALL render_versioning_trigger( + table_name => 'increment_version_test', + history_table => 'increment_version_test_history', + sys_period => 'sys_period', + increment_version => true + ) + `) + + // Test functionality + await db.executeTransaction([ + "INSERT INTO increment_version_test (data) VALUES ('test data')" + ]) + + // Add a column to trigger re-rendering (event trigger should handle this) + await db.query( + 'ALTER TABLE increment_version_test ADD COLUMN description text' + ) + await db.query( + 'ALTER TABLE increment_version_test_history ADD COLUMN description text' + ) + + // Test that versioning still works after schema change + await db.executeTransaction([ + "UPDATE increment_version_test SET data = 'updated data', description = 'test desc' WHERE id = 1" + ]) + + const result = await db.query( + 'SELECT data, description, version FROM increment_version_test' + ) + deepStrictEqual(result.rows.length, 1) + deepStrictEqual(result.rows[0].data, 'updated data') + deepStrictEqual(result.rows[0].description, 'test desc') + deepStrictEqual(result.rows[0].version, 2) + + // Check history contains old version + const historyResult = await db.query( + 'SELECT data, version FROM increment_version_test_history' + ) + deepStrictEqual(historyResult.rows.length, 1) + deepStrictEqual(historyResult.rows[0].data, 'test data') + deepStrictEqual(historyResult.rows[0].version, 1) + }) + }) +}) diff --git a/test/e2e/test-integration.ts b/test/e2e/test-integration.ts new file mode 100644 index 0000000..dc4c3d1 --- /dev/null +++ b/test/e2e/test-integration.ts @@ -0,0 +1,718 @@ +import { deepStrictEqual, ok, rejects } from 'node:assert' +import { describe, test, before, after, beforeEach } from 'node:test' +import * as url from 'url' +import { DatabaseHelper } from './db-helper.js' + +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) + +describe('Integration Tests - All Features', () => { + let db: DatabaseHelper + + before(async () => { + db = new DatabaseHelper() + await db.connect() + await db.setupVersioning(DatabaseHelper.modernMinimumPostgresVersion) + }) + + after(async () => { + await db.cleanup() + await db.disconnect() + }) + + beforeEach(async () => { + // Clean up all test tables + const tables = [ + 'users', + 'users_history', + 'orders', + 'orders_history', + 'products', + 'products_history', + 'complex_scenario', + 'complex_scenario_history', + 'migration_test', + 'migration_test_history', + 'performance_test', + 'performance_test_history' + ] + + for (const table of tables) + await db.query(`DROP TABLE IF EXISTS ${table} CASCADE`) + }) + + describe('Real-world Scenario Testing', () => { + test('should handle e-commerce order system with versioning', async () => { + // Create users table + await db.query(` + CREATE TABLE users ( + id bigint PRIMARY KEY, + email text UNIQUE NOT NULL, + name text, + created_at timestamp DEFAULT CURRENT_TIMESTAMP, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE users_history ( + id bigint, + email text, + name text, + created_at timestamp, + sys_period tstzrange + ) + `) + + // Create orders table + await db.query(` + CREATE TABLE orders ( + id bigint PRIMARY KEY, + user_id bigint REFERENCES users(id), + total_amount decimal(10,2), + status text DEFAULT 'pending', + created_at timestamp DEFAULT CURRENT_TIMESTAMP, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE orders_history ( + id bigint, + user_id bigint, + total_amount decimal(10,2), + status text, + created_at timestamp, + sys_period tstzrange + ) + `) + + // Set up versioning for both tables + await db.query(` + CALL render_versioning_trigger( + table_name => 'users', + history_table => 'users_history', + sys_period => 'sys_period', + ignore_unchanged_values => true + ) + `) + + await db.query(` + CALL render_versioning_trigger( + table_name => 'orders', + history_table => 'orders_history', + sys_period => 'sys_period', + ignore_unchanged_values => true + ) + `) + + // Simulate user registration + await db.executeTransaction([ + "INSERT INTO users (id, email, name) VALUES (1, 'john@example.com', 'John Doe')" + ]) + + // Simulate order creation + await db.executeTransaction([ + "INSERT INTO orders (id, user_id, total_amount, status) VALUES (100, 1, 299.99, 'pending')" + ]) + + await db.sleep(0.1) + + // Update user profile + await db.executeTransaction([ + "UPDATE users SET name = 'John Smith' WHERE id = 1" + ]) + + // Process order through various states + await db.sleep(0.05) + await db.executeTransaction([ + "UPDATE orders SET status = 'processing' WHERE id = 100" + ]) + + await db.sleep(0.05) + await db.executeTransaction([ + "UPDATE orders SET status = 'shipped' WHERE id = 100" + ]) + + await db.sleep(0.05) + await db.executeTransaction([ + "UPDATE orders SET status = 'delivered' WHERE id = 100" + ]) + + // Verify current state + const currentUser = await db.query('SELECT * FROM users WHERE id = 1') + deepStrictEqual(currentUser.rows[0].name, 'John Smith') + + const currentOrder = await db.query('SELECT * FROM orders WHERE id = 100') + deepStrictEqual(currentOrder.rows[0].status, 'delivered') + + // Verify history tracking + const userHistory = await db.query( + 'SELECT * FROM users_history WHERE id = 1 ORDER BY sys_period' + ) + deepStrictEqual(userHistory.rows.length, 1) + deepStrictEqual(userHistory.rows[0].name, 'John Doe') // Original name + + const orderHistory = await db.query( + 'SELECT * FROM orders_history WHERE id = 100 ORDER BY sys_period' + ) + ok(orderHistory.rows.length >= 3, 'Should have history of status changes') + + // Verify we can track order status progression + const statusProgression = orderHistory.rows.map(row => row.status) + ok(statusProgression.includes('pending'), 'Should include pending status') + ok( + statusProgression.includes('processing'), + 'Should include processing status' + ) + ok(statusProgression.includes('shipped'), 'Should include shipped status') + }) + + test('should handle complex schema evolution scenario', async () => { + // Start with basic product table + await db.query(` + CREATE TABLE products ( + id bigint PRIMARY KEY, + name text, + price decimal(10,2), + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE products_history ( + id bigint, + name text, + price decimal(10,2), + sys_period tstzrange + ) + `) + + // Initial versioning setup + await db.query(` + CALL render_versioning_trigger( + table_name => 'products', + history_table => 'products_history', + sys_period => 'sys_period' + ) + `) + + // Insert initial product data + await db.executeTransaction([ + "INSERT INTO products (id, name, price) VALUES (1, 'Widget A', 19.99)", + "INSERT INTO products (id, name, price) VALUES (2, 'Gadget B', 29.99)" + ]) + + await db.sleep(0.1) + + // First schema evolution: add category + await db.query( + "ALTER TABLE products ADD COLUMN category text DEFAULT 'uncategorized'" + ) + await db.query('ALTER TABLE products_history ADD COLUMN category text') + + // Regenerate trigger for new schema + await db.query(` + CALL render_versioning_trigger( + table_name => 'products', + history_table => 'products_history', + sys_period => 'sys_period' + ) + `) + + // Update with new column + await db.executeTransaction([ + "UPDATE products SET category = 'electronics', price = 24.99 WHERE id = 1" + ]) + + // Second schema evolution: add description and remove default + await db.query('ALTER TABLE products ADD COLUMN description text') + await db.query('ALTER TABLE products_history ADD COLUMN description text') + await db.query('ALTER TABLE products ALTER COLUMN category DROP DEFAULT') + + // Regenerate trigger again + await db.query(` + CALL render_versioning_trigger( + table_name => 'products', + history_table => 'products_history', + sys_period => 'sys_period' + ) + `) + + // Test with full schema + await db.executeTransaction([ + "INSERT INTO products (id, name, price, category, description) VALUES (3, 'Super Widget', 39.99, 'premium', 'Advanced widget with extra features')" + ]) + + await db.sleep(0.1) + + await db.executeTransaction([ + "UPDATE products SET description = 'Ultimate widget with premium features', price = 44.99 WHERE id = 3" + ]) + + // Verify current state includes all columns + const currentProducts = await db.query( + 'SELECT * FROM products ORDER BY id' + ) + deepStrictEqual(currentProducts.rows.length, 3) + ok( + currentProducts.rows[2].description.includes('Ultimate'), + 'Should have updated description' + ) + + // Verify history preservation across schema changes + const productHistory = await db.query( + 'SELECT * FROM products_history ORDER BY id, sys_period' + ) + ok(productHistory.rows.length >= 2, 'Should have history records') + + // Verify we can query historical data even after schema changes + const originalProduct1 = productHistory.rows.find( + row => row.id === '1' && parseFloat(row.price) === 19.99 + ) + ok(originalProduct1, 'Should preserve original price in history') + }) + }) + + describe('Performance and Stress Testing', () => { + test('should handle bulk operations efficiently', async () => { + await db.query(` + CREATE TABLE performance_test ( + id bigint PRIMARY KEY, + data text, + counter int DEFAULT 0, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE performance_test_history ( + id bigint, + data text, + counter int, + sys_period tstzrange + ) + `) + + await db.query(` + CALL render_versioning_trigger( + table_name => 'performance_test', + history_table => 'performance_test_history', + sys_period => 'sys_period' + ) + `) + + const startTime = Date.now() + + // Bulk insert + const insertPromises = [] + for (let i = 1; i <= 100; i++) { + insertPromises.push( + db.executeTransaction([ + `INSERT INTO performance_test (id, data) VALUES (${i}, 'data-${i}')` + ]) + ) + } + + await Promise.all(insertPromises) + + const insertTime = Date.now() - startTime + console.log(`Bulk insert time: ${insertTime}ms`) + + // Verify all records inserted + const insertResult = await db.query( + 'SELECT COUNT(*) as count FROM performance_test' + ) + deepStrictEqual(parseInt(insertResult.rows[0].count), 100) + + await db.sleep(0.1) + + // Bulk update + const updateStartTime = Date.now() + + const updatePromises = [] + for (let i = 1; i <= 100; i++) { + updatePromises.push( + db.executeTransaction([ + `UPDATE performance_test SET counter = ${i}, data = 'updated-data-${i}' WHERE id = ${i}` + ]) + ) + } + + await Promise.all(updatePromises) + + const updateTime = Date.now() - updateStartTime + console.log(`Bulk update time: ${updateTime}ms`) + + // Verify history was created + const historyResult = await db.query( + 'SELECT COUNT(*) as count FROM performance_test_history' + ) + deepStrictEqual(parseInt(historyResult.rows[0].count), 100) + + // Performance assertion (should complete within reasonable time) + ok(insertTime < 10000, 'Bulk insert should complete within 10 seconds') + ok(updateTime < 15000, 'Bulk update should complete within 15 seconds') + }) + + test('should handle rapid sequential updates correctly', async () => { + await db.query(` + CREATE TABLE rapid_test ( + id bigint PRIMARY KEY, + value int, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE rapid_test_history ( + id bigint, + value int, + sys_period tstzrange + ) + `) + + await db.query(` + CALL render_versioning_trigger( + table_name => 'rapid_test', + history_table => 'rapid_test_history', + sys_period => 'sys_period' + ) + `) + + // Insert initial record + await db.executeTransaction([ + 'INSERT INTO rapid_test (id, value) VALUES (1, 0)' + ]) + + // Perform rapid sequential updates + for (let i = 1; i <= 50; i++) { + await db.executeTransaction([ + `UPDATE rapid_test SET value = ${i} WHERE id = 1` + ]) + await db.sleep(0.001) // Very small delay to ensure timestamp progression + } + + // Verify final state + const finalResult = await db.query( + 'SELECT * FROM rapid_test WHERE id = 1' + ) + deepStrictEqual(parseInt(finalResult.rows[0].value), 50) + + // Verify all intermediate states were captured + const historyResult = await db.query( + 'SELECT COUNT(*) as count FROM rapid_test_history WHERE id = 1' + ) + deepStrictEqual(parseInt(historyResult.rows[0].count), 50) // All 50 updates should create history + + // Verify history contains sequential values + const historyValues = await db.query(` + SELECT value + FROM rapid_test_history + WHERE id = 1 + ORDER BY sys_period + `) + + for (let i = 0; i < 50; i++) + deepStrictEqual(parseInt(historyValues.rows[i].value), i) + }) + }) + + describe('Migration and Data Integrity', () => { + test('should handle migration mode with existing data', async () => { + // Create table with existing historical data + await db.query(` + CREATE TABLE migration_test ( + id bigint PRIMARY KEY, + status text, + updated_at timestamp, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE migration_test_history ( + id bigint, + status text, + updated_at timestamp, + sys_period tstzrange + ) + `) + + // Insert historical data with specific time ranges + const baseTime = '2023-01-01 10:00:00+00' + const midTime = '2023-06-01 10:00:00+00' + const currentTime = '2023-12-01 10:00:00+00' + + await db.query( + ` + INSERT INTO migration_test (id, status, updated_at, sys_period) + VALUES (1, 'active', '2023-01-01', tstzrange($1, NULL)) + `, + [currentTime] + ) + + // Insert existing history + await db.query( + ` + INSERT INTO migration_test_history (id, status, updated_at, sys_period) + VALUES + (1, 'pending', '2023-01-01', tstzrange($1, $2)), + (1, 'processing', '2023-03-01', tstzrange($2, $3)) + `, + [baseTime, midTime, currentTime] + ) + + // Set up versioning with migration mode + await db.query(` + CALL render_versioning_trigger( + table_name => 'migration_test', + history_table => 'migration_test_history', + sys_period => 'sys_period', + enable_migration_mode => true + ) + `) + + // Update should work correctly with existing history + await db.executeTransaction([ + "UPDATE migration_test SET status = 'completed' WHERE id = 1" + ]) + + // Verify current state + const currentResult = await db.query( + 'SELECT * FROM migration_test WHERE id = 1' + ) + deepStrictEqual(currentResult.rows[0].status, 'completed') + + // Verify history preservation + const historyResult = await db.query(` + SELECT status, sys_period + FROM migration_test_history + WHERE id = 1 + ORDER BY sys_period + `) + + ok( + historyResult.rows.length >= 3, + 'Should preserve existing history and add new' + ) + + const statuses = historyResult.rows.map(row => row.status) + ok(statuses.includes('pending'), 'Should preserve original history') + ok(statuses.includes('processing'), 'Should preserve original history') + ok( + statuses.includes('active'), + 'Should add previous current state to history' + ) + }) + + test('should maintain referential integrity during versioning', async () => { + // Create related tables with foreign keys + await db.query(` + CREATE TABLE users ( + id bigint PRIMARY KEY, + name text, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE users_history ( + id bigint, + name text, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE orders ( + id bigint PRIMARY KEY, + user_id bigint REFERENCES users(id), + amount decimal(10,2), + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE orders_history ( + id bigint, + user_id bigint, + amount decimal(10,2), + sys_period tstzrange + ) + `) + + // Set up versioning for both tables + await db.query(` + CALL render_versioning_trigger( + table_name => 'users', + history_table => 'users_history', + sys_period => 'sys_period' + ) + `) + + await db.query(` + CALL render_versioning_trigger( + table_name => 'orders', + history_table => 'orders_history', + sys_period => 'sys_period' + ) + `) + + // Create user and order + await db.executeTransaction([ + "INSERT INTO users (id, name) VALUES (1, 'Test User')", + 'INSERT INTO orders (id, user_id, amount) VALUES (100, 1, 50.00)' + ]) + + await db.sleep(0.1) + + // Update both related records + await db.executeTransaction([ + "UPDATE users SET name = 'Updated User' WHERE id = 1", + 'UPDATE orders SET amount = 75.00 WHERE id = 100' + ]) + + // Verify referential integrity is maintained + const currentOrder = await db.query(` + SELECT o.*, u.name as user_name + FROM orders o + JOIN users u ON o.user_id = u.id + WHERE o.id = 100 + `) + + deepStrictEqual(currentOrder.rows.length, 1) + deepStrictEqual(currentOrder.rows[0].user_name, 'Updated User') + deepStrictEqual(parseFloat(currentOrder.rows[0].amount), 75.0) + + // Verify history maintains referential relationships + const historyJoin = await db.query(` + SELECT oh.amount as old_amount, uh.name as old_user_name + FROM orders_history oh + JOIN users_history uh ON oh.user_id = uh.id + WHERE oh.id = 100 + `) + + ok(historyJoin.rows.length > 0, 'Should be able to join historical data') + }) + }) + + describe('Error Recovery and Edge Cases', () => { + test('should handle transaction rollbacks correctly', async () => { + await db.query(` + CREATE TABLE rollback_test ( + id bigint PRIMARY KEY, + value text, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE rollback_test_history ( + id bigint, + value text, + sys_period tstzrange + ) + `) + + await db.query(` + CALL render_versioning_trigger( + table_name => 'rollback_test', + history_table => 'rollback_test_history', + sys_period => 'sys_period' + ) + `) + + // Insert initial data + await db.executeTransaction([ + "INSERT INTO rollback_test (id, value) VALUES (1, 'original')" + ]) + + // Attempt transaction that will fail + await db.query('BEGIN') + + try { + await db.query( + "UPDATE rollback_test SET value = 'updated' WHERE id = 1" + ) + // Force an error + await db.query( + "INSERT INTO rollback_test (id, value) VALUES (1, 'duplicate')" + ) // Will fail due to PK constraint + await db.query('COMMIT') + } catch (error) { + await db.query('ROLLBACK') + } + + // Verify original state is preserved + const result = await db.query('SELECT * FROM rollback_test WHERE id = 1') + deepStrictEqual(result.rows[0].value, 'original') + + // Verify no spurious history was created + const historyResult = await db.query( + 'SELECT * FROM rollback_test_history' + ) + deepStrictEqual(historyResult.rows.length, 0) + }) + + test('should handle concurrent modifications gracefully', async () => { + await db.query(` + CREATE TABLE concurrent_test ( + id bigint PRIMARY KEY, + counter int DEFAULT 0, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE concurrent_test_history ( + id bigint, + counter int, + sys_period tstzrange + ) + `) + + await db.query(` + CALL render_versioning_trigger( + table_name => 'concurrent_test', + history_table => 'concurrent_test_history', + sys_period => 'sys_period', + mitigate_update_conflicts => true + ) + `) + + // Insert initial record + await db.executeTransaction([ + 'INSERT INTO concurrent_test (id, counter) VALUES (1, 0)' + ]) + + // Simulate concurrent updates (sequential for testing) + const updates = [] + for (let i = 1; i <= 10; i++) { + updates.push( + db.executeTransaction([ + `UPDATE concurrent_test SET counter = counter + 1 WHERE id = 1` + ]) + ) + } + + await Promise.all(updates) + + // Verify final state (some updates might have been lost due to concurrency, but that's expected) + const finalResult = await db.query( + 'SELECT * FROM concurrent_test WHERE id = 1' + ) + const finalCounter = parseInt(finalResult.rows[0].counter) + ok( + finalCounter > 0 && finalCounter <= 10, + 'Counter should be between 1 and 10' + ) + + // Verify history was created for successful updates + const historyResult = await db.query( + 'SELECT COUNT(*) as count FROM concurrent_test_history WHERE id = 1' + ) + const historyCount = parseInt(historyResult.rows[0].count) + ok(historyCount > 0, 'Should have some history records') + }) + }) +}) diff --git a/test/e2e/test-legacy.ts b/test/e2e/test-legacy.ts new file mode 100644 index 0000000..7ff5394 --- /dev/null +++ b/test/e2e/test-legacy.ts @@ -0,0 +1,307 @@ +import { deepStrictEqual, ok, rejects } from 'node:assert' +import { describe, test, before, after, beforeEach } from 'node:test' +import * as url from 'url' +import { DatabaseHelper } from './db-helper.js' + +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) + +describe('Legacy Versioning Function E2E Tests', () => { + let db: DatabaseHelper + + before(async () => { + db = new DatabaseHelper() + await db.connect() + await db.setupVersioning() + }) + + after(async () => { + await db.cleanup() + await db.disconnect() + }) + + beforeEach(async () => { + // Clean up any existing test tables + await db.query('DROP TABLE IF EXISTS versioning CASCADE') + await db.query('DROP TABLE IF EXISTS versioning_history CASCADE') + await db.query('DROP TABLE IF EXISTS structure CASCADE') + await db.query('DROP TABLE IF EXISTS structure_history CASCADE') + await db.query('DROP TABLE IF EXISTS test_table CASCADE') + await db.query('DROP TABLE IF EXISTS test_table_history CASCADE') + await db.query('DROP TABLE IF EXISTS legacy_test CASCADE') + await db.query('DROP TABLE IF EXISTS legacy_test_history CASCADE') + await db.query('DROP TABLE IF EXISTS static_test CASCADE') + await db.query('DROP TABLE IF EXISTS static_test_history CASCADE') + }) + + describe('Basic Legacy Versioning', () => { + test('should work with legacy versioning function syntax', async () => { + await db.query(` + CREATE TABLE versioning ( + a bigint, + "b b" date, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE versioning_history ( + a bigint, + c date, + sys_period tstzrange + ) + `) + + // Create trigger using legacy versioning function + await db.query(` + CREATE TRIGGER versioning_trigger + BEFORE INSERT OR UPDATE OR DELETE ON versioning + FOR EACH ROW EXECUTE PROCEDURE versioning('sys_period', 'versioning_history', false) + `) + + // Insert some data before versioning is fully active + await db.query(` + INSERT INTO versioning (a, sys_period) VALUES (1, tstzrange('-infinity', NULL)) + `) + + await db.query(` + INSERT INTO versioning (a, sys_period) VALUES (2, tstzrange('2000-01-01', NULL)) + `) + + // Test INSERT + await db.executeTransaction(['INSERT INTO versioning (a) VALUES (3)']) + + const insertResult = await db.query(` + SELECT a, "b b", lower(sys_period) = CURRENT_TIMESTAMP as is_current + FROM versioning + WHERE a = 3 + ORDER BY a, sys_period + `) + + deepStrictEqual(insertResult.rows.length, 1) + deepStrictEqual(insertResult.rows[0].a, '3') + + // History should be empty for inserts + const historyAfterInsert = await db.query( + 'SELECT * FROM versioning_history ORDER BY a, sys_period' + ) + deepStrictEqual(historyAfterInsert.rows.length, 0) + + await db.sleep(0.1) + + // Test UPDATE + await db.executeTransaction(['UPDATE versioning SET a = 4 WHERE a = 3']) + + const updateResult = await db.query(` + SELECT a, "b b", lower(sys_period) = CURRENT_TIMESTAMP as is_current + FROM versioning + ORDER BY a, sys_period + `) + + const updatedRow = updateResult.rows.find(row => row.a === '4') + ok(updatedRow, 'Updated row should exist') + + // History should contain old value + const historyAfterUpdate = await db.query(` + SELECT a, c, upper(sys_period) = CURRENT_TIMESTAMP as is_recent + FROM versioning_history + ORDER BY a, sys_period + `) + + deepStrictEqual(historyAfterUpdate.rows.length, 1) + deepStrictEqual(historyAfterUpdate.rows[0].a, '3') + + await db.sleep(0.1) + + // Test DELETE + await db.executeTransaction(['DELETE FROM versioning WHERE a = 4']) + + const mainAfterDelete = await db.query( + 'SELECT * FROM versioning WHERE a = 4' + ) + deepStrictEqual(mainAfterDelete.rows.length, 0) + + const historyAfterDelete = await db.query(` + SELECT a, c, upper(sys_period) = CURRENT_TIMESTAMP as is_recent + FROM versioning_history + WHERE a = 4 + ORDER BY a, sys_period + `) + + ok(historyAfterDelete.rows.length > 0, 'Deleted row should be in history') + }) + + test('should handle unchanged values option', async () => { + await db.query(` + CREATE TABLE versioning ( + a bigint, + b bigint, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE versioning_history ( + a bigint, + b bigint, + sys_period tstzrange + ) + `) + + // Create trigger with unchanged values detection enabled + await db.query(` + CREATE TRIGGER versioning_trigger + BEFORE INSERT OR UPDATE OR DELETE ON versioning + FOR EACH ROW EXECUTE PROCEDURE versioning('sys_period', 'versioning_history', false, true) + `) + + // Insert initial data + await db.query(` + INSERT INTO versioning (a, b, sys_period) VALUES (1, 1, tstzrange('-infinity', NULL)) + `) + + await db.query(` + INSERT INTO versioning (a, b, sys_period) VALUES (2, 2, tstzrange('2000-01-01', NULL)) + `) + + // Update with no actual changes - should be ignored + await db.executeTransaction(['UPDATE versioning SET b = 2 WHERE a = 2']) + + const historyAfterNoChange = await db.query( + 'SELECT * FROM versioning_history ORDER BY a, sys_period' + ) + deepStrictEqual( + historyAfterNoChange.rows.length, + 0, + 'No history should be created for unchanged values' + ) + + await db.sleep(0.1) + + // Update with actual changes + await db.executeTransaction(['UPDATE versioning SET b = 3 WHERE a = 2']) + + const historyAfterChange = await db.query( + 'SELECT * FROM versioning_history ORDER BY a, sys_period' + ) + ok( + historyAfterChange.rows.length > 0, + 'History should be created for actual changes' + ) + }) + }) + + describe('Custom System Time', () => { + test('should respect custom system time setting', async () => { + await setupLegacyVersioningTable(db) + + // Set custom system time + const customTime = '2023-01-15 14:30:00.123456' + await db.query(`SET user_defined.system_time = '${customTime}'`) + + await db.executeTransaction(['INSERT INTO versioning (a) VALUES (100)']) + + const result = await db.query(` + SELECT a, lower(sys_period) as start_time + FROM versioning + WHERE a = 100 + `) + + deepStrictEqual(result.rows.length, 1) + + // Verify custom timestamp was used (allowing for small parsing differences) + const startTime = new Date(result.rows[0].start_time) + const expectedTime = new Date(customTime) + const timeDiff = Math.abs(startTime.getTime() - expectedTime.getTime()) + ok(timeDiff < 10000, 'Custom system time should be used') + + // Reset system time + await db.query('RESET user_defined.system_time') + }) + }) + + describe('Schema Compatibility', () => { + test('should work with quoted column names', async () => { + await db.query(` + CREATE TABLE structure ( + a bigint, + "b b" date, + d text, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE structure_history ( + a bigint, + "b b" date, + d text, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TRIGGER versioning_trigger + BEFORE INSERT OR UPDATE OR DELETE ON structure + FOR EACH ROW EXECUTE PROCEDURE versioning('sys_period', 'structure_history', false) + `) + + // Test with quoted column names + await db.executeTransaction([ + "INSERT INTO structure (a, \"b b\", d) VALUES (1, '2000-01-01', 'test')" + ]) + + await db.sleep(0.1) + + await db.executeTransaction([ + "UPDATE structure SET d = 'updated' WHERE a = 1" + ]) + + const historyResult = await db.query( + 'SELECT * FROM structure_history ORDER BY a, sys_period' + ) + deepStrictEqual(historyResult.rows.length, 1) + deepStrictEqual(historyResult.rows[0].d, 'test') + + const mainResult = await db.query( + 'SELECT * FROM structure ORDER BY a, sys_period' + ) + deepStrictEqual(mainResult.rows.length, 1) + deepStrictEqual(mainResult.rows[0].d, 'updated') + }) + }) +}) + +// Helper function to set up basic legacy versioning table +async function setupLegacyVersioningTable(db: DatabaseHelper): Promise { + await db.query(` + CREATE TABLE versioning ( + a bigint, + "b b" date, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE versioning_history ( + a bigint, + c date, + sys_period tstzrange + ) + `) + + // Insert some initial data + await db.query(` + INSERT INTO versioning (a, sys_period) VALUES (1, tstzrange('-infinity', NULL)) + `) + + await db.query(` + INSERT INTO versioning (a, sys_period) VALUES (2, tstzrange('2000-01-01', NULL)) + `) + + // Create legacy trigger + await db.query(` + CREATE TRIGGER versioning_trigger + BEFORE INSERT OR UPDATE OR DELETE ON versioning + FOR EACH ROW EXECUTE PROCEDURE versioning('sys_period', 'versioning_history', false) + `) +} diff --git a/test/e2e/test-performance-comparison.ts b/test/e2e/test-performance-comparison.ts new file mode 100644 index 0000000..9b9cd4c --- /dev/null +++ b/test/e2e/test-performance-comparison.ts @@ -0,0 +1,634 @@ +import { deepStrictEqual, ok } from 'node:assert' +import { describe, test, before, after, beforeEach } from 'node:test' +import { DatabaseHelper } from './db-helper.js' + +interface PerformanceMetrics { + setupTime: number + insertTime: number + updateTime: number + deleteTime: number + totalTime: number + operationCount: number +} + +interface TestResults { + mainTableRows: any[] + historyTableRows: any[] + totalMainCount: number + totalHistoryCount: number +} + +describe('Legacy vs Modern Implementation Performance Comparison', () => { + let db: DatabaseHelper + + before(async () => { + db = new DatabaseHelper() + await db.connect() + await db.setupVersioning(DatabaseHelper.modernMinimumPostgresVersion) + }) + + after(async () => { + await db.cleanup() + await db.disconnect() + }) + + beforeEach(async () => { + // Clean up any existing test tables + await db.query('DROP TABLE IF EXISTS legacy_perf_test CASCADE') + await db.query('DROP TABLE IF EXISTS legacy_perf_test_history CASCADE') + await db.query('DROP TABLE IF EXISTS modern_perf_test CASCADE') + await db.query('DROP TABLE IF EXISTS modern_perf_test_history CASCADE') + }) + + async function setupLegacyTable(): Promise { + // Create main table + await db.query(` + CREATE TABLE legacy_perf_test ( + id SERIAL PRIMARY KEY, + name VARCHAR(100), + value INTEGER, + description TEXT, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + sys_period tstzrange NOT NULL DEFAULT tstzrange(CURRENT_TIMESTAMP, NULL) + ) + `) + + // Create history table + await db.query(` + CREATE TABLE legacy_perf_test_history ( + id INTEGER, + name VARCHAR(100), + value INTEGER, + description TEXT, + created_at TIMESTAMPTZ, + sys_period tstzrange NOT NULL + ) + `) + + // Apply legacy versioning function + await db.query(` + CREATE TRIGGER versioning_trigger + BEFORE INSERT OR UPDATE OR DELETE ON legacy_perf_test + FOR EACH ROW EXECUTE FUNCTION versioning( + 'sys_period', 'legacy_perf_test_history', true + ) + `) + } + + async function setupModernTable(): Promise { + // Create main table + await db.query(` + CREATE TABLE modern_perf_test ( + id SERIAL PRIMARY KEY, + name VARCHAR(100), + value INTEGER, + description TEXT, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + sys_period tstzrange NOT NULL DEFAULT tstzrange(CURRENT_TIMESTAMP, NULL) + ) + `) + + // Create history table + await db.query(` + CREATE TABLE modern_perf_test_history ( + id INTEGER, + name VARCHAR(100), + value INTEGER, + description TEXT, + created_at TIMESTAMPTZ, + sys_period tstzrange NOT NULL + ) + `) + + // Use render_versioning_trigger procedure with named arguments + await db.query(` + CALL render_versioning_trigger( + table_name => 'modern_perf_test', + history_table => 'modern_perf_test_history', + sys_period => 'sys_period', + ignore_unchanged_values => true + ) + `) + } + + async function performOperations( + tableName: string, + historyTableName: string, + testDataSize: number + ): Promise { + const metrics: PerformanceMetrics = { + setupTime: 0, + insertTime: 0, + updateTime: 0, + deleteTime: 0, + totalTime: 0, + operationCount: testDataSize + } + + const startTotal = Date.now() + + // INSERT operations + const insertStart = Date.now() + for (let i = 1; i <= testDataSize; i++) { + await db.query( + ` + INSERT INTO ${tableName} (name, value, description) + VALUES ($1, $2, $3) + `, + [`Test Item ${i}`, i * 10, `Description for item ${i}`] + ) + } + metrics.insertTime = Date.now() - insertStart + + // UPDATE operations (update half of the records) + const updateStart = Date.now() + for (let i = 1; i <= Math.floor(testDataSize / 2); i++) { + await db.query( + ` + UPDATE ${tableName} + SET value = $1, description = $2 + WHERE id = $3 + `, + [i * 20, `Updated description for item ${i}`, i] + ) + } + metrics.updateTime = Date.now() - updateStart + + // DELETE operations (delete quarter of the records) + const deleteStart = Date.now() + for (let i = 1; i <= Math.floor(testDataSize / 4); i++) { + await db.query(`DELETE FROM ${tableName} WHERE id = $1`, [i]) + } + metrics.deleteTime = Date.now() - deleteStart + + metrics.totalTime = Date.now() - startTotal + + return metrics + } + + async function getTableResults( + tableName: string, + historyTableName: string + ): Promise { + const mainResult = await db.query(` + SELECT id, name, value, description, sys_period + FROM ${tableName} + ORDER BY id + `) + + const historyResult = await db.query(` + SELECT id, name, value, description, sys_period + FROM ${historyTableName} + ORDER BY id, sys_period + `) + + const mainCountResult = await db.query( + `SELECT COUNT(*) as count FROM ${tableName}` + ) + const historyCountResult = await db.query( + `SELECT COUNT(*) as count FROM ${historyTableName}` + ) + + return { + mainTableRows: mainResult.rows, + historyTableRows: historyResult.rows, + totalMainCount: parseInt(mainCountResult.rows[0].count), + totalHistoryCount: parseInt(historyCountResult.rows[0].count) + } + } + + function formatPerformanceReport( + legacyMetrics: PerformanceMetrics, + modernMetrics: PerformanceMetrics + ): string { + // Collect all data for dynamic width calculation + const operations = ['INSERT', 'UPDATE', 'DELETE', 'TOTAL'] + const times = [ + [legacyMetrics.insertTime, modernMetrics.insertTime], + [legacyMetrics.updateTime, modernMetrics.updateTime], + [legacyMetrics.deleteTime, modernMetrics.deleteTime], + [legacyMetrics.totalTime, modernMetrics.totalTime] + ] + + // Calculate dynamic column widths (ensure column headers fit) + const operationWidth = Math.max( + 'Operation'.length, + ...operations.map(op => op.length) + ) + const legacyWidth = Math.max( + 'Legacy'.length, + ...times.map(([legacy]) => `${legacy}ms`.length) + ) + const modernWidth = Math.max( + 'Modern'.length, + ...times.map(([, modern]) => `${modern}ms`.length) + ) + const diffWidth = Math.max( + 'Difference'.length, + ...times.map( + ([legacy, modern]) => `${Math.abs(legacy - modern)}ms`.length + ) + ) + const improvWidth = Math.max('Improvement'.length, 7) // Fixed width for percentage + const statusWidth = Math.max('✓'.length, 1) // Fixed width for checkmark/X + + // Calculate the total width by summing all components explicitly + const columnWidths = [ + operationWidth + 2, + legacyWidth + 2, + modernWidth + 2, + diffWidth + 2, + improvWidth + 2, + statusWidth + 2 + ] + const separatorCount = columnWidths.length - 1 // n columns = n-1 separators + const totalWidth = + columnWidths.reduce((sum, width) => sum + width, 0) + separatorCount + + const createRow = ( + operation: string, + legacyTime: number, + modernTime: number, + isTotal: boolean = false + ) => { + const diff = legacyTime - modernTime + const percentage = + legacyTime > 0 ? ((diff / legacyTime) * 100).toFixed(1) : '0.0' + const symbol = diff > 0 ? '✓' : diff < 0 ? '✗' : '≈' + const diffDisplay = diff > 0 ? `+${diff}` : diff.toString() + + return `│ ${operation.padEnd(operationWidth)} │ ${`${legacyTime}ms`.padStart(legacyWidth)} │ ${`${modernTime}ms`.padStart(modernWidth)} │ ${`${diffDisplay}ms`.padStart(diffWidth)} │ ${`${percentage}%`.padStart(improvWidth)} │ ${symbol.padStart(statusWidth)} │` + } + + const titleText = 'PERFORMANCE COMPARISON REPORT' + const titlePadding = Math.max( + 0, + Math.floor((totalWidth - titleText.length) / 2) + ) + const title = + ' '.repeat(titlePadding) + + titleText + + ' '.repeat(totalWidth - titleText.length - titlePadding) + + return ` +┌${'─'.repeat(totalWidth)}┐ +│${title}│ +├${'─'.repeat(operationWidth + 2)}┬${'─'.repeat(legacyWidth + 2)}┬${'─'.repeat(modernWidth + 2)}┬${'─'.repeat(diffWidth + 2)}┬${'─'.repeat(improvWidth + 2)}┬${'─'.repeat(statusWidth + 2)}┤ +│ ${'Operation'.padEnd(operationWidth)} │ ${'Legacy'.padStart(legacyWidth)} │ ${'Modern'.padStart(modernWidth)} │ ${'Difference'.padStart(diffWidth)} │ ${'Improvement'.padStart(improvWidth)} │ ${'✓'.padStart(statusWidth)} │ +├${'─'.repeat(operationWidth + 2)}┼${'─'.repeat(legacyWidth + 2)}┼${'─'.repeat(modernWidth + 2)}┼${'─'.repeat(diffWidth + 2)}┼${'─'.repeat(improvWidth + 2)}┼${'─'.repeat(statusWidth + 2)}┤ +${createRow('INSERT', legacyMetrics.insertTime, modernMetrics.insertTime)} +${createRow('UPDATE', legacyMetrics.updateTime, modernMetrics.updateTime)} +${createRow('DELETE', legacyMetrics.deleteTime, modernMetrics.deleteTime)} +├${'─'.repeat(operationWidth + 2)}┼${'─'.repeat(legacyWidth + 2)}┼${'─'.repeat(modernWidth + 2)}┼${'─'.repeat(diffWidth + 2)}┼${'─'.repeat(improvWidth + 2)}┼${'─'.repeat(statusWidth + 2)}┤ +${createRow('TOTAL', legacyMetrics.totalTime, modernMetrics.totalTime, true)} +└${'─'.repeat(operationWidth + 2)}┴${'─'.repeat(legacyWidth + 2)}┴${'─'.repeat(modernWidth + 2)}┴${'─'.repeat(diffWidth + 2)}┴${'─'.repeat(improvWidth + 2)}┴${'─'.repeat(statusWidth + 2)}┘ + +📊 Test Data: + • Operations performed: ${legacyMetrics.operationCount.toLocaleString()} + • Insert operations: ${legacyMetrics.operationCount} + • Update operations: ${Math.floor(legacyMetrics.operationCount / 2)} + • Delete operations: ${Math.floor(legacyMetrics.operationCount / 4)} + +🎯 Performance Summary: + • Modern implementation is ${(((legacyMetrics.totalTime - modernMetrics.totalTime) / legacyMetrics.totalTime) * 100).toFixed(1)}% ${modernMetrics.totalTime < legacyMetrics.totalTime ? 'faster' : 'slower'} overall + • Legacy total time: ${legacyMetrics.totalTime.toLocaleString()}ms + • Modern total time: ${modernMetrics.totalTime.toLocaleString()}ms + • Time difference: ${Math.abs(legacyMetrics.totalTime - modernMetrics.totalTime).toLocaleString()}ms +` + } + + function validateResultsMatch( + legacyResults: TestResults, + modernResults: TestResults + ): void { + // Both should have the same number of records in main tables + deepStrictEqual( + legacyResults.totalMainCount, + modernResults.totalMainCount, + `Main table counts differ: Legacy=${legacyResults.totalMainCount}, Modern=${modernResults.totalMainCount}` + ) + + // Both should have the same number of records in history tables + deepStrictEqual( + legacyResults.totalHistoryCount, + modernResults.totalHistoryCount, + `History table counts differ: Legacy=${legacyResults.totalHistoryCount}, Modern=${modernResults.totalHistoryCount}` + ) + + // Compare the data in main tables (excluding sys_period which may have timing differences) + for (let i = 0; i < legacyResults.mainTableRows.length; i++) { + const legacyRow = legacyResults.mainTableRows[i] + const modernRow = modernResults.mainTableRows[i] + + deepStrictEqual( + legacyRow.id, + modernRow.id, + `Main table row ${i} ID mismatch` + ) + deepStrictEqual( + legacyRow.name, + modernRow.name, + `Main table row ${i} name mismatch` + ) + deepStrictEqual( + legacyRow.value, + modernRow.value, + `Main table row ${i} value mismatch` + ) + deepStrictEqual( + legacyRow.description, + modernRow.description, + `Main table row ${i} description mismatch` + ) + } + + console.log( + '✅ Data consistency validation passed: Both implementations produce identical results!' + ) + } + + // Create individual tests for each data size + const testDataSizes = [100, 500, 1000, 5000] + + test('should compare performance with multiple data sizes and validate result consistency', async () => { + console.log('\n🚀 Starting Performance Comparison Tests...\n') + + for (const testDataSize of testDataSizes) { + console.log( + `\n🚀 Testing with ${testDataSize.toLocaleString()} operations...\n` + ) + + // Setup and test legacy implementation + console.log('📊 Testing Legacy Implementation...') + await setupLegacyTable() + const legacyMetrics = await performOperations( + 'legacy_perf_test', + 'legacy_perf_test_history', + testDataSize + ) + const legacyResults = await getTableResults( + 'legacy_perf_test', + 'legacy_perf_test_history' + ) + + // Clean up before setting up modern table + await db.query('DROP TABLE IF EXISTS legacy_perf_test CASCADE') + await db.query('DROP TABLE IF EXISTS legacy_perf_test_history CASCADE') + + // Setup and test modern implementation + console.log('📊 Testing Modern Implementation...') + await setupModernTable() + const modernMetrics = await performOperations( + 'modern_perf_test', + 'modern_perf_test_history', + testDataSize + ) + const modernResults = await getTableResults( + 'modern_perf_test', + 'modern_perf_test_history' + ) + + // Validate that both implementations produce the same results + validateResultsMatch(legacyResults, modernResults) + + // Display performance comparison report + console.log(formatPerformanceReport(legacyMetrics, modernMetrics)) + + // Assert that modern implementation performs reasonably (not more than 50% slower) + const performanceRatio = modernMetrics.totalTime / legacyMetrics.totalTime + ok( + performanceRatio <= 1.5, + `Modern implementation is significantly slower (${(performanceRatio * 100).toFixed(1)}% of legacy time). ` + + `Expected ratio <= 150%, got ${(performanceRatio * 100).toFixed(1)}%` + ) + + // Verify that we have the expected number of operations + ok( + legacyResults.totalMainCount > 0, + 'Legacy implementation should have main table records' + ) + ok( + legacyResults.totalHistoryCount > 0, + 'Legacy implementation should have history table records' + ) + ok( + modernResults.totalMainCount > 0, + 'Modern implementation should have main table records' + ) + ok( + modernResults.totalHistoryCount > 0, + 'Modern implementation should have history table records' + ) + + console.log('\n✅ Performance comparison test completed successfully!') + + // Clean up after test + await db.query('DROP TABLE IF EXISTS modern_perf_test CASCADE') + await db.query('DROP TABLE IF EXISTS modern_perf_test_history CASCADE') + } + }) + + test('should provide performance scaling summary across all data sizes', async () => { + console.log('\n📈 Running Performance Scaling Analysis...\n') + + interface ScalingResult { + dataSize: number + legacyTime: number + modernTime: number + ratio: number + } + + const scalingResults: ScalingResult[] = [] + const testDataSizes = [100, 500, 1000, 5000] + + for (const testDataSize of testDataSizes) { + console.log( + `\n⚡ Testing with ${testDataSize.toLocaleString()} operations...` + ) + + // Test legacy implementation + await setupLegacyTable() + const legacyMetrics = await performOperations( + 'legacy_perf_test', + 'legacy_perf_test_history', + testDataSize + ) + await db.query('DROP TABLE IF EXISTS legacy_perf_test CASCADE') + await db.query('DROP TABLE IF EXISTS legacy_perf_test_history CASCADE') + + // Test modern implementation + await setupModernTable() + const modernMetrics = await performOperations( + 'modern_perf_test', + 'modern_perf_test_history', + testDataSize + ) + await db.query('DROP TABLE IF EXISTS modern_perf_test CASCADE') + await db.query('DROP TABLE IF EXISTS modern_perf_test_history CASCADE') + + const ratio = modernMetrics.totalTime / legacyMetrics.totalTime + scalingResults.push({ + dataSize: testDataSize, + legacyTime: legacyMetrics.totalTime, + modernTime: modernMetrics.totalTime, + ratio + }) + + console.log( + ` Legacy: ${legacyMetrics.totalTime}ms | Modern: ${modernMetrics.totalTime}ms | Ratio: ${ratio.toFixed(2)}x` + ) + } + + // Display scaling summary + console.log('\n' + formatScalingReport(scalingResults)) + + // Assert that performance doesn't degrade significantly at higher scales + const maxRatio = Math.max(...scalingResults.map(r => r.ratio)) + ok( + maxRatio <= 2.0, + `Performance ratio should not exceed 2.0x at any scale, but got ${maxRatio.toFixed(2)}x` + ) + + console.log('\n✅ Performance scaling analysis completed!') + }) + + function formatScalingReport( + results: { + dataSize: number + legacyTime: number + modernTime: number + ratio: number + }[] + ): string { + // Calculate dynamic column widths (ensure column headers fit) + const dataSizeWidth = Math.max( + 'Data Size'.length, + ...results.map(r => r.dataSize.toLocaleString().length) + ) + const legacyWidth = Math.max( + 'Legacy'.length, + ...results.map(r => `${r.legacyTime}ms`.length) + ) + const modernWidth = Math.max( + 'Modern'.length, + ...results.map(r => `${r.modernTime}ms`.length) + ) + const ratioWidth = Math.max('Ratio'.length, 8) + const throughputWidth = Math.max('Throughput'.length, 12) + + // Calculate total width precisely: sum of all column widths + padding (2 per column) + separators (1 per separator) + const totalWidth = + dataSizeWidth + + 2 + + (legacyWidth + 2) + + (modernWidth + 2) + + (ratioWidth + 2) + + (throughputWidth + 2) + + 4 // 4 separators (|) + + const titleText = 'PERFORMANCE SCALING REPORT' + const titlePadding = Math.max( + 0, + Math.floor((totalWidth - titleText.length) / 2) + ) + const title = + ' '.repeat(titlePadding) + + titleText + + ' '.repeat(totalWidth - titleText.length - titlePadding) + + let report = ` +┌${'─'.repeat(totalWidth)}┐ +│${title}│ +├${'─'.repeat(dataSizeWidth + 2)}┬${'─'.repeat(legacyWidth + 2)}┬${'─'.repeat(modernWidth + 2)}┬${'─'.repeat(ratioWidth + 2)}┬${'─'.repeat(throughputWidth + 2)}┤ +│ ${'Data Size'.padEnd(dataSizeWidth)} │ ${'Legacy'.padStart(legacyWidth)} │ ${'Modern'.padStart(modernWidth)} │ ${'Ratio'.padStart(ratioWidth)} │ ${'Throughput'.padStart(throughputWidth)} │ +├${'─'.repeat(dataSizeWidth + 2)}┼${'─'.repeat(legacyWidth + 2)}┼${'─'.repeat(modernWidth + 2)}┼${'─'.repeat(ratioWidth + 2)}┼${'─'.repeat(throughputWidth + 2)}┤` + + for (const result of results) { + const throughput = Math.round( + result.dataSize / (result.modernTime / 1000) + ) // ops per second + const ratioDisplay = `${result.ratio.toFixed(2)}x` + const throughputDisplay = `${throughput.toLocaleString()}/s` + + report += ` +│ ${result.dataSize.toLocaleString().padEnd(dataSizeWidth)} │ ${`${result.legacyTime}ms`.padStart(legacyWidth)} │ ${`${result.modernTime}ms`.padStart(modernWidth)} │ ${ratioDisplay.padStart(ratioWidth)} │ ${throughputDisplay.padStart(throughputWidth)} │` + } + + report += ` +└${'─'.repeat(dataSizeWidth + 2)}┴${'─'.repeat(legacyWidth + 2)}┴${'─'.repeat(modernWidth + 2)}┴${'─'.repeat(ratioWidth + 2)}┴${'─'.repeat(throughputWidth + 2)}┘ + +📊 Scaling Analysis: + • Best performance ratio: ${Math.min(...results.map(r => r.ratio)).toFixed(2)}x (at ${results.find(r => r.ratio === Math.min(...results.map(r => r.ratio)))?.dataSize.toLocaleString()} operations) + • Worst performance ratio: ${Math.max(...results.map(r => r.ratio)).toFixed(2)}x (at ${results.find(r => r.ratio === Math.max(...results.map(r => r.ratio)))?.dataSize.toLocaleString()} operations) + • Average performance ratio: ${(results.reduce((sum, r) => sum + r.ratio, 0) / results.length).toFixed(2)}x + • Modern implementation efficiency improves with scale: ${results[results.length - 1].ratio < results[0].ratio ? '✓ Yes' : '✗ No'}` + + return report + } + + test('should demonstrate advanced modern features work correctly', async () => { + console.log('\n🔬 Testing Advanced Modern Features...') + + // Test with ignore_unchanged_values enabled + await db.query(` + CREATE TABLE modern_advanced_test ( + id SERIAL PRIMARY KEY, + name VARCHAR(100), + value INTEGER, + sys_period tstzrange NOT NULL DEFAULT tstzrange(CURRENT_TIMESTAMP, NULL) + ) + `) + + await db.query(` + CREATE TABLE modern_advanced_test_history ( + id INTEGER, + name VARCHAR(100), + value INTEGER, + sys_period tstzrange NOT NULL + ) + `) + + // Use render_versioning_trigger with ignore_unchanged_values enabled + await db.query(` + CALL render_versioning_trigger( + table_name => 'modern_advanced_test', + history_table => 'modern_advanced_test_history', + sys_period => 'sys_period', + ignore_unchanged_values => true + ) + `) + + // Insert a record + await db.query(` + INSERT INTO modern_advanced_test (name, value) + VALUES ('test', 100) + `) + + // Update with the same values (should be ignored) + await db.query(` + UPDATE modern_advanced_test + SET name = 'test', value = 100 + WHERE id = 1 + `) + + // Update with different values (should create history) + await db.query(` + UPDATE modern_advanced_test + SET name = 'updated', value = 200 + WHERE id = 1 + `) + + // Check history count - should only have one entry (from the real update) + const historyCount = await db.query(` + SELECT COUNT(*) as count FROM modern_advanced_test_history + `) + + deepStrictEqual( + parseInt(historyCount.rows[0].count), + 1, + 'Should have only one history entry due to ignore_unchanged_values' + ) + + console.log('✅ Advanced features test passed!') + }) +}) diff --git a/test/e2e/test-static-generator.ts b/test/e2e/test-static-generator.ts new file mode 100644 index 0000000..3ecfac0 --- /dev/null +++ b/test/e2e/test-static-generator.ts @@ -0,0 +1,587 @@ +import { deepStrictEqual, ok, rejects } from 'node:assert' +import { describe, test, before, after, beforeEach } from 'node:test' +import { DatabaseHelper } from './db-helper.js' + +describe('Static Generator E2E Tests', () => { + let db: DatabaseHelper + + before(async () => { + db = new DatabaseHelper() + await db.connect() + await db.setupVersioning(DatabaseHelper.modernMinimumPostgresVersion) + }) + + after(async () => { + await db.cleanup() + await db.disconnect() + }) + + beforeEach(async () => { + // Clean up any existing test tables + await db.query('DROP TABLE IF EXISTS versioning CASCADE') + await db.query('DROP TABLE IF EXISTS versioning_history CASCADE') + await db.query('DROP TABLE IF EXISTS structure CASCADE') + await db.query('DROP TABLE IF EXISTS structure_history CASCADE') + await db.query('DROP TABLE IF EXISTS test_table CASCADE') + await db.query('DROP TABLE IF EXISTS test_table_history CASCADE') + }) + + describe('Basic Versioning Functionality', () => { + test('should create versioned table with static trigger', async () => { + // Create main table + await db.query(` + CREATE TABLE versioning ( + a bigint, + "b b" date, + sys_period tstzrange + ) + `) + + // Create history table + await db.query(` + CREATE TABLE versioning_history ( + a bigint, + c date, + sys_period tstzrange + ) + `) + + // Use static generator to create trigger + await db.query(` + CALL render_versioning_trigger( + table_name => 'versioning', + history_table => 'versioning_history', + sys_period => 'sys_period' + ) + `) + + // Verify table exists + const tableExists = await db.tableExists('versioning') + ok(tableExists) + }) + + test('should handle INSERT operations correctly', async () => { + await setupBasicVersioningTable(db) + + const beforeTimestamp = await db.getCurrentTimestamp() + await db.sleep(0.01) // Small delay to ensure timestamp difference + + // Insert data + await db.executeTransaction(['INSERT INTO versioning (a) VALUES (3)']) + + const afterTimestamp = await db.getCurrentTimestamp() + + // Check main table + const mainResult = await db.query( + ` + SELECT a, "b b", lower(sys_period) >= $1 AND lower(sys_period) <= $2 as timestamp_ok + FROM versioning + WHERE a = 3 + ORDER BY a, sys_period + `, + [beforeTimestamp, afterTimestamp] + ) + + deepStrictEqual(mainResult.rows.length, 1) + deepStrictEqual(mainResult.rows[0].a, '3') + ok(mainResult.rows[0].timestamp_ok) + + // History table should be empty for INSERT + const historyResult = await db.query( + 'SELECT * FROM versioning_history ORDER BY a, sys_period' + ) + deepStrictEqual(historyResult.rows.length, 0) + }) + + test('should handle UPDATE operations correctly', async () => { + await setupBasicVersioningTable(db) + + // Insert initial data + await db.executeTransaction(['INSERT INTO versioning (a) VALUES (3)']) + + await db.sleep(0.1) // Ensure timestamp difference + + const beforeUpdateTimestamp = await db.getCurrentTimestamp() + + // Update data + await db.executeTransaction(['UPDATE versioning SET a = 4 WHERE a = 3']) + + const afterUpdateTimestamp = await db.getCurrentTimestamp() + + // Check main table has updated value + const mainResult = await db.query( + ` + SELECT a, "b b", lower(sys_period) >= $1 AND lower(sys_period) <= $2 as timestamp_ok + FROM versioning + ORDER BY a, sys_period + `, + [beforeUpdateTimestamp, afterUpdateTimestamp] + ) + + const currentRow = mainResult.rows.find(row => row.a === '4') + ok(currentRow, 'Updated row should exist in main table') + ok(currentRow.timestamp_ok, 'Timestamp should be recent') + + // Check history table has old value + const historyResult = await db.query( + ` + SELECT a, c, upper(sys_period) >= $1 AND upper(sys_period) <= $2 as timestamp_ok + FROM versioning_history + ORDER BY a, sys_period + `, + [beforeUpdateTimestamp, afterUpdateTimestamp] + ) + + deepStrictEqual(historyResult.rows.length, 1) + deepStrictEqual(historyResult.rows[0].a, '3') + ok( + historyResult.rows[0].timestamp_ok, + 'History timestamp should be recent' + ) + }) + + test('should handle DELETE operations correctly', async () => { + await setupBasicVersioningTable(db) + + // Insert and update to create some history + await db.executeTransaction(['INSERT INTO versioning (a) VALUES (3)']) + + await db.sleep(0.1) + + await db.executeTransaction(['UPDATE versioning SET a = 4 WHERE a = 3']) + + await db.sleep(0.1) + + const beforeDeleteTimestamp = await db.getReliableTimestamp() + + // Delete data + await db.executeTransaction(['DELETE FROM versioning WHERE a = 4']) + + const afterDeleteTimestamp = await db.getReliableTimestamp() + + // Main table should be empty (or not contain deleted row) + const mainResult = await db.query('SELECT * FROM versioning WHERE a = 4') + deepStrictEqual(mainResult.rows.length, 0) + + // History table should contain the deleted row with more robust timestamp checking + const historyResult = await db.query( + ` + SELECT a, c, upper(sys_period) as delete_timestamp + FROM versioning_history + WHERE a = 4 + ORDER BY a, sys_period + ` + ) + + ok(historyResult.rows.length > 0, 'Deleted row should be in history') + + // Use more robust timestamp checking with tolerance + const deletedRow = historyResult.rows[historyResult.rows.length - 1] // Get the latest row + const deleteTimestamp = new Date(deletedRow.delete_timestamp) + const isInRange = await db.isTimestampInRange( + deleteTimestamp, + beforeDeleteTimestamp, + afterDeleteTimestamp, + 2000 // 2 second tolerance for timing issues + ) + + ok( + isInRange, + `Delete timestamp ${deleteTimestamp} should be between ${beforeDeleteTimestamp} and ${afterDeleteTimestamp}` + ) + }) + }) + + describe('Advanced Versioning Features', () => { + test('should ignore unchanged values when configured', async () => { + // Create table with ignore_unchanged_values = true + await db.query(` + CREATE TABLE versioning ( + a bigint, + b bigint, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE versioning_history ( + a bigint, + b bigint, + sys_period tstzrange + ) + `) + + // Generate trigger with ignore_unchanged_values = true + await db.query(` + CALL render_versioning_trigger( + table_name => 'versioning', + history_table => 'versioning_history', + sys_period => 'sys_period', + ignore_unchanged_values => true + ) + `) + + // Insert initial data + await db.executeTransaction([ + "INSERT INTO versioning (a, b, sys_period) VALUES (1, 1, tstzrange('-infinity', NULL))", + "INSERT INTO versioning (a, b, sys_period) VALUES (2, 2, tstzrange('2000-01-01', NULL))" + ]) + + // Update with no actual changes + await db.executeTransaction(['UPDATE versioning SET b = 2 WHERE a = 2']) + + // History should be empty since no real changes occurred + const historyResult = await db.query( + 'SELECT * FROM versioning_history ORDER BY a, sys_period' + ) + deepStrictEqual( + historyResult.rows.length, + 0, + 'No history should be created for unchanged values' + ) + + await db.sleep(0.1) + + // Update with actual changes + await db.executeTransaction(['UPDATE versioning SET b = 3 WHERE a = 2']) + + // History should now contain the change + const historyAfterChange = await db.query( + 'SELECT * FROM versioning_history ORDER BY a, sys_period' + ) + ok( + historyAfterChange.rows.length > 0, + 'History should be created for actual changes' + ) + }) + + test('should include current version in history when configured', async () => { + await db.query(` + CREATE TABLE versioning ( + a bigint, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE versioning_history ( + a bigint, + sys_period tstzrange + ) + `) + + // Generate trigger with include_current_version_in_history = true + await db.query(` + CALL render_versioning_trigger( + table_name => 'versioning', + history_table => 'versioning_history', + sys_period => 'sys_period', + include_current_version_in_history => true + ) + `) + + // Insert data + await db.executeTransaction(['INSERT INTO versioning (a) VALUES (1)']) + + // Check that current version is also in history + const historyResult = await db.query( + 'SELECT * FROM versioning_history ORDER BY a, sys_period' + ) + ok( + historyResult.rows.length > 0, + 'Current version should be in history table' + ) + + const mainResult = await db.query( + 'SELECT * FROM versioning ORDER BY a, sys_period' + ) + deepStrictEqual( + mainResult.rows.length, + 1, + 'Main table should have current version' + ) + }) + + test('should handle custom system time', async () => { + await setupBasicVersioningTable(db) + + // Set custom system time + const customTime = '2023-01-01 12:00:00.000000' + await db.query(`SET user_defined.system_time = '${customTime}'`) + + // Insert data + await db.executeTransaction(['INSERT INTO versioning (a) VALUES (100)']) + + // Check that the custom timestamp was used + const result = await db.query(` + SELECT a, lower(sys_period) as start_time + FROM versioning + WHERE a = 100 + `) + + deepStrictEqual(result.rows.length, 1) + const startTime = new Date(result.rows[0].start_time) + const expectedTime = new Date(customTime) + + // Allow for small differences due to parsing + const timeDiff = Math.abs(startTime.getTime() - expectedTime.getTime()) + ok(timeDiff < 1000, 'Custom system time should be used') + + // Reset system time + await db.query(`RESET user_defined.system_time`) + }) + }) + + describe('Error Handling', () => { + test('should reject invalid system period types', async () => { + await db.query(` + DROP TABLE IF EXISTS invalid_table CASCADE; + + CREATE TABLE invalid_table ( + a bigint, + sys_period text -- Wrong type! + ) + `) + + await db.query(` + DROP TABLE IF EXISTS invalid_table_history CASCADE; + + CREATE TABLE invalid_table_history ( + a bigint, + sys_period tstzrange + ) + `) + + // Should throw error when generating trigger + await rejects(async () => { + await db.query(` + CALL render_versioning_trigger( + table_name => 'invalid_table', + history_table => 'invalid_table_history', + sys_period => 'sys_period' + ) + `) + }) + }) + + test('should reject operations on missing history table', async () => { + await db.query(` + CREATE TABLE versioning ( + a bigint, + sys_period tstzrange + ) + `) + + // No history table created + + // Should throw error when generating trigger + await rejects(async () => { + await db.query(` + CALL render_versioning_trigger( + table_name => 'versioning', + history_table => 'nonexistent_history', + sys_period => 'sys_period' + ) + `) + }) + }) + + test('should reject invalid system period values', async () => { + await setupBasicVersioningTable(db) + + // Try to insert invalid system period + await rejects(async () => { + await db.executeTransaction([ + "INSERT INTO versioning (a, sys_period) VALUES (1, tstzrange('2023-01-01', '2022-01-01'))" // Invalid range + ]) + }) + }) + }) + + describe('Schema Compatibility', () => { + test('should work with different column names and types', async () => { + await db.query(` + CREATE TABLE structure ( + a bigint, + "b b" date, + d text, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE structure_history ( + a bigint, + "b b" date, + d text, + sys_period tstzrange + ) + `) + + await db.query(` + CALL render_versioning_trigger( + table_name => 'structure', + history_table => 'structure_history', + sys_period => 'sys_period' + ) + `) + + // Test with various data types + await db.executeTransaction([ + "INSERT INTO structure (a, \"b b\", d) VALUES (1, '2000-01-01', 'test')" + ]) + + await db.sleep(0.1) + + await db.executeTransaction([ + "UPDATE structure SET d = 'updated' WHERE a = 1" + ]) + + const historyResult = await db.query( + 'SELECT * FROM structure_history ORDER BY a, sys_period' + ) + deepStrictEqual(historyResult.rows.length, 1) + deepStrictEqual(historyResult.rows[0].d, 'test') + + const mainResult = await db.query( + 'SELECT * FROM structure ORDER BY a, sys_period' + ) + deepStrictEqual(mainResult.rows.length, 1) + deepStrictEqual(mainResult.rows[0].d, 'updated') + }) + + test('should handle tables with different schemas', async () => { + await db.query('DROP SCHEMA IF EXISTS test_schema CASCADE') + + await db.query('CREATE SCHEMA test_schema') + + await db.query(` + CREATE TABLE test_schema.versioning ( + a bigint, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE test_schema.versioning_history ( + a bigint, + sys_period tstzrange + ) + `) + + await db.query(` + CALL render_versioning_trigger( + table_name => 'test_schema.versioning', + history_table => 'test_schema.versioning_history', + sys_period => 'sys_period' + ) + `) + + // Test operations + await db.executeTransaction([ + 'INSERT INTO test_schema.versioning (a) VALUES (1)' + ]) + + const result = await db.query('SELECT * FROM test_schema.versioning') + deepStrictEqual(result.rows.length, 1) + + // Cleanup + await db.query('DROP SCHEMA test_schema CASCADE') + }) + }) + + describe('Performance and Edge Cases', () => { + test('should handle multiple rapid updates', async () => { + await setupBasicVersioningTable(db, false) + + // Insert initial data + await db.executeTransaction(['INSERT INTO versioning (a) VALUES (1)']) + + // Perform multiple rapid updates + for (let i = 2; i <= 10; i++) { + await db.executeTransaction([ + `UPDATE versioning SET a = ${i} WHERE a = ${i - 1}` + ]) + await db.sleep(0.01) // Small delay to ensure timestamp progression + } + + // Check final state + const mainResult = await db.query('SELECT * FROM versioning ORDER BY a') + deepStrictEqual(mainResult.rows.length, 1) + deepStrictEqual(mainResult.rows[0].a, '10') + + // Check history contains all intermediate values + const historyResult = await db.query( + 'SELECT * FROM versioning_history ORDER BY a, sys_period' + ) + deepStrictEqual(historyResult.rows.length, 9) // 9 updates = 9 history records + }) + + test('should handle concurrent transaction simulation', async () => { + await setupBasicVersioningTable(db, false) + + // Insert initial data + await db.executeTransaction([ + 'INSERT INTO versioning (a) VALUES (1)', + 'INSERT INTO versioning (a) VALUES (2)' + ]) + + // Simulate concurrent updates (sequential for testing) + await db.sleep(0.1) + + await db.executeTransaction(['UPDATE versioning SET a = 10 WHERE a = 1']) + + await db.executeTransaction(['UPDATE versioning SET a = 20 WHERE a = 2']) + + // Verify both updates worked + const mainResult = await db.query('SELECT * FROM versioning ORDER BY a') + deepStrictEqual(mainResult.rows.length, 2) + + const historyResult = await db.query( + 'SELECT * FROM versioning_history ORDER BY a, sys_period' + ) + deepStrictEqual(historyResult.rows.length, 2) + }) + }) +}) + +// Helper function to set up basic versioning table +async function setupBasicVersioningTable( + db: DatabaseHelper, + withInitialData: boolean = true +): Promise { + await db.query(` + CREATE TABLE versioning ( + a bigint, + "b b" date, + sys_period tstzrange + ) + `) + + await db.query(` + CREATE TABLE versioning_history ( + a bigint, + c date, + sys_period tstzrange + ) + `) + + // Insert some initial data + if (withInitialData) { + await db.query(` + INSERT INTO versioning (a, "b b", sys_period) VALUES (1, '2000-01-01', tstzrange('-infinity', NULL)) + `) + await db.query(` + INSERT INTO versioning (a, "b b", sys_period) VALUES (2, '2000-01-02', tstzrange('2000-01-01', NULL)) + `) + } + + // Generate and execute static trigger + await db.query(` + CALL render_versioning_trigger( + table_name => 'versioning', + history_table => 'versioning_history', + sys_period => 'sys_period' + ) + `) +} diff --git a/test/e2e/tsconfig.json b/test/e2e/tsconfig.json new file mode 100644 index 0000000..a93920d --- /dev/null +++ b/test/e2e/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "target": "ES2022", + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": false, + "noEmit": true, + "types": ["node", "pg"] + }, + "include": [ + "*.ts", + "**/*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/test/runTest.sh b/test/runTest.sh old mode 100755 new mode 100644 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..dafff50 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "lib": ["ES2022"], + "module": "NodeNext", + "moduleResolution": "nodenext", + "outDir": "./dist", + "sourceMap": true, + "strict": true, + "target": "ES2022" + }, + "exclude": ["node_modules/**/*"], + "include": ["test/**/*.ts", "./package.json"] +} diff --git a/versioning_function.sql b/versioning_function.sql index 51285e1..65dd2a0 100644 --- a/versioning_function.sql +++ b/versioning_function.sql @@ -33,7 +33,7 @@ BEGIN ELSE SELECT TO_TIMESTAMP( user_defined_system_time, - 'YYYY-MM-DD HH24:MI:SS.MS.US' + 'YYYY-MM-DD HH24:MI:SS.US' ) INTO time_stamp_to_use; END IF; EXCEPTION WHEN OTHERS THEN diff --git a/versioning_function_nochecks.sql b/versioning_function_nochecks.sql index 32a4955..603b98e 100644 --- a/versioning_function_nochecks.sql +++ b/versioning_function_nochecks.sql @@ -27,7 +27,7 @@ BEGIN ELSE SELECT TO_TIMESTAMP( user_defined_system_time, - 'YYYY-MM-DD HH24:MI:SS.MS.US' + 'YYYY-MM-DD HH24:MI:SS.US' ) INTO time_stamp_to_use; END IF; EXCEPTION WHEN OTHERS THEN diff --git a/versioning_tables_metadata.sql b/versioning_tables_metadata.sql new file mode 100644 index 0000000..e2e3a53 --- /dev/null +++ b/versioning_tables_metadata.sql @@ -0,0 +1,35 @@ +-- this metadata table tracks all the tables the system will automatically +-- create versioning triggers for, so that we can re-render the trigger +-- when the table is altered. +CREATE TABLE IF NOT EXISTS versioning_tables_metadata ( + table_name text NOT NULL, + table_schema text NOT NULL, + history_table text NOT NULL, + history_table_schema text NOT NULL, + sys_period text NOT NULL DEFAULT 'sys_period', + ignore_unchanged_values boolean NOT NULL DEFAULT false, + include_current_version_in_history boolean NOT NULL DEFAULT false, + mitigate_update_conflicts boolean NOT NULL DEFAULT false, + enable_migration_mode boolean NOT NULL DEFAULT false, + increment_version boolean NOT NULL DEFAULT false, + version_column_name text NOT NULL DEFAULT 'version', + PRIMARY KEY (table_name, table_schema) +); + +-- Example INSERT statements with all parameters: +-- INSERT INTO versioning_tables_metadata ( +-- table_name, +-- table_schema, +-- history_table, +-- history_table_schema, +-- sys_period, +-- ignore_unchanged_values, +-- include_current_version_in_history, +-- mitigate_update_conflicts, +-- enable_migration_mode, +-- increment_version, +-- version_column_name +-- ) +-- VALUES +-- ('subscriptions', 'public', 'subscriptions_history', 'history', 'sys_period', false, false, false, false, false, 'version'), +-- ('users', 'public', 'users_history', 'public', 'system_time', true, true, false, false, true, 'version');