openvibers/openvibe.community Commits
The SQLite era is gone (on PostgreSQL since 2026-09-28; no compatibility kept): the one-time migrate-to-postgres script, *_DB_PATH settings (config, .env.example, systemd unit, test helpers), the unused better-sqlite3 dependency and its lockfile entries, and docs, docstrings and STATUS lines about SQLite or its rollback window. Reviewed independently before shipping. npm test 46/46 test files passed.
Changes
@@@ -58,15 +58,13 @@ OV_CHAT_URL=https://openvibe.chat# With 'community' the OAuth client above also needs identity.subject.resolve (author names).PASTES_AUTHORITY=live# PostgreSQL (ADR-035), written by OpenVibe.Host roles/data/add-service.sh community: the runtime role through--# PgBouncer, the owner on a direct connection (migrations and the one-time import), and Valkey (shared limit--# counters). Comments, the forum, Pulse and the relay's bookkeeping live there in every mode; pastes too with--# 'community'. Without DATABASE_URL, development uses an embedded PGlite database in data/pglite.++# PgBouncer, the owner on a direct connection (migrations), and Valkey (shared limit counters). Comments, the++# forum, Pulse and the relay's bookkeeping live there in every mode; pastes too with 'community'. Without++# DATABASE_URL, development uses an embedded PGlite database in data/pglite.DATABASE_URL=DATABASE_DIRECT_URL=VALKEY_URL=VALKEY_PREFIX=--# The SQLite file of releases before PostgreSQL: read once by scripts/migrate-to-postgres.js.--COMMUNITY_DB_PATH=./data/community.db# Optional salt for hashed visitor ids in view counting (default: derived from the client secret)#VIEW_HASH_SECRET=@@@ -587,7 +587,6 @@ Copy `.env.example` to `.env` (production: `/etc/openvibe/community.env`, mode 0| `PASTES_AUTHORITY` | `live` | `live` = proxy to Live; `community` = this site's database is the authority || `DATABASE_URL`, `DATABASE_DIRECT_URL` | unset (development: embedded PGlite in `data/pglite`) | PostgreSQL through PgBouncer, and the owner's direct connection for migrations (written by OpenVibe.Host `roles/data/add-service.sh community`) || `VALKEY_URL`, `VALKEY_PREFIX` | unset | per-actor limit counters shared across processes |--| `COMMUNITY_DB_PATH` | `./data/community.db` | the SQLite file of releases before PostgreSQL, read once by `scripts/migrate-to-postgres.js` || `API_CORS_ORIGINS` | Live, Media, Network, Tools, Games origins | Browser origins that may call `/api/v1/comments` and `/api/v1/pulse` with a Bearer JWT || `DISCORD_RELAY_ENABLED` | off | `true` turns the Discord relay on ([docs/discord-relay.md](docs/discord-relay.md)) || `DISCORD_RELAY_POLL_MS` / `DISCORD_RELAY_BACKOFF_MS` / `DISCORD_RELAY_MAX_ATTEMPTS` | `30000` / `30000` / `6` | Relay sender cadence, first retry delay, attempts before `failed` (the dead letter) |@@@ -616,12 +615,11 @@ npm run n-1:record # after a deploy: the N-1 fixtures from the deployed commit (````test/n-1.test.js` (roadmap WS-P task 11, in `npm test`) runs the previous release's clients against--this one and its SQL against this schema, from `test/fixtures/n-1/`: every call `community.js`, the--templates' forms and the openvibe-sdk community client make, and every link, script and form of the--pages that release served, each answered compatibly (status, JSON, the response fields the client--reads); then every statement the previous release runs must still prepare after this release opened--(and migrated) a database the previous one created. After each deploy, record the release now in--production as the next N-1 and commit `test/fixtures/n-1/`.++this one, from `test/fixtures/n-1/`: every call `community.js`, the templates' forms and the++openvibe-sdk community client make, and every link, script and form of the pages that release served,++each answered compatibly (status, JSON, the response fields the client reads); then this release must++keep every migration that release ran byte for byte and only add. After each deploy, record the++release now in production as the next N-1 and commit `test/fixtures/n-1/`.## Acceptance@@@ -665,10 +663,7 @@ Production deploys with `sudo ovhost deploy community` on the host (strategy `gifast-forward `/opt/openvibe.community`, install on a lockfile change, restart, wait for `/api/ready`).The unit is `openvibe-community.service` on `127.0.0.1:4200`, the env file `/etc/openvibe/community.env`. The database is`ov_community` on the host's data role (`sudo /opt/openvibe.host/roles/data/add-service.sh community` writes its settings); the--release migrates it at boot. The one-time move from SQLite is `scripts/migrate-to-postgres.js` (openvibe-sdk--`runSqliteMigration`, with a `--pglite` rehearsal mode), run while the service is stopped; the old--`/var/lib/openvibe-community/community.db` stays read-only for 7 days as the rollback. After a--deploy, record the N-1 fixtures (`npm run n-1:record`).++release migrates it at boot. After a deploy, record the N-1 fixtures (`npm run n-1:record`).Rollback: ovhost puts the previous sha back by itself when `/api/ready` does not answer 2xx after therestart; afterwards `sudo ovhost rollback community --to <sha>`. Migrations only add tables and columns.@@@ -732,7 +727,6 @@ server/public/ css/community.css, js/community.js, favicon.svg, og-default.png(openvibe-shared is the pinned OpenVibe.Shared v1.25.0 release, installed by npm)deploy/ systemd unit, nginx vhost--scripts/migrate-to-postgres.js the one-time SQLite → PostgreSQL import (runSqliteMigration; --pglite rehearsal)test/ run.js + *.test.js (mock Live, Network and Media with a real RS256 key)docs/capabilities-proposal/ Wave 5 capability manifests (released in openvibe-contracts v0.7.0)docs/discord-relay.md the Discord relay: how it works, owner steps, staff API, limits@@@ -21,10 +21,8 @@ NoNewPrivileges=truePrivateTmp=trueProtectSystem=strictProtectHome=true--# The code tree stays read-only. Community's database is PostgreSQL (ov_community, ADR-035); /var/lib/openvibe-community--# keeps the pre-PostgreSQL SQLite file (read-only) for the rollback window and the one-time import.++# The code tree stays read-only. Community's database is PostgreSQL (ov_community, ADR-035).StateDirectory=openvibe-community--Environment=COMMUNITY_DB_PATH=/var/lib/openvibe-community/community.db[Install]WantedBy=multi-user.target@@@ -9,7 +9,6 @@"version": "1.0.0","license": "MIT","dependencies": {-- "better-sqlite3": "^11.10.0","cookie-parser": "^1.4.7","dotenv": "^16.4.7","express": "^4.21.2",@@@ -119,7 +118,9 @@"url": "https://feross.org/support"}],-- "license": "MIT"++ "license": "MIT",++ "optional": true,++ "peer": true},"node_modules/better-sqlite3": {"version": "11.10.0",@@@ -127,6 +128,8 @@"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==","hasInstallScript": true,"license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"bindings": "^1.5.0","prebuild-install": "^7.1.1"@@@ -137,6 +140,8 @@"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz","integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"file-uri-to-path": "1.0.0"}@@@ -146,6 +151,8 @@"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz","integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"buffer": "^5.5.0","inherits": "^2.0.4",@@@ -195,6 +202,8 @@}],"license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"base64-js": "^1.3.1","ieee754": "^1.1.13"@@@ -259,7 +268,9 @@"version": "1.1.4","resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz","integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",-- "license": "ISC"++ "license": "ISC",++ "optional": true,++ "peer": true},"node_modules/cluster-key-slot": {"version": "1.1.2",@@@ -333,6 +344,8 @@"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz","integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"mimic-response": "^3.1.0"},@@@ -348,6 +361,8 @@"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz","integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==","license": "MIT",++ "optional": true,++ "peer": true,"engines": {"node": ">=4.0.0"}@@@ -385,6 +400,8 @@"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz","integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==","license": "Apache-2.0",++ "optional": true,++ "peer": true,"engines": {"node": ">=8"}@@@ -444,6 +461,8 @@"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz","integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"once": "^1.4.0"}@@@ -498,6 +517,8 @@"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz","integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==","license": "(MIT OR WTFPL)",++ "optional": true,++ "peer": true,"engines": {"node": ">=6"}@@@ -589,7 +610,9 @@"version": "1.0.0","resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz","integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",-- "license": "MIT"++ "license": "MIT",++ "optional": true,++ "peer": true},"node_modules/finalhandler": {"version": "1.3.2",@@@ -631,7 +654,9 @@"version": "1.0.0","resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz","integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",-- "license": "MIT"++ "license": "MIT",++ "optional": true,++ "peer": true},"node_modules/function-bind": {"version": "1.1.2",@@@ -683,7 +708,9 @@"version": "0.0.0","resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz","integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",-- "license": "MIT"++ "license": "MIT",++ "optional": true,++ "peer": true},"node_modules/gopd": {"version": "1.2.0",@@@ -792,7 +819,9 @@"url": "https://feross.org/support"}],-- "license": "BSD-3-Clause"++ "license": "BSD-3-Clause",++ "optional": true,++ "peer": true},"node_modules/inherits": {"version": "2.0.4",@@@ -804,7 +833,9 @@"version": "1.3.8","resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz","integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",-- "license": "ISC"++ "license": "ISC",++ "optional": true,++ "peer": true},"node_modules/iovalkey": {"version": "0.4.0",@@@ -1041,6 +1072,8 @@"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz","integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==","license": "MIT",++ "optional": true,++ "peer": true,"engines": {"node": ">=10"},@@@ -1053,6 +1086,8 @@"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz","integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==","license": "MIT",++ "optional": true,++ "peer": true,"funding": {"url": "https://github.com/sponsors/ljharb"}@@@ -1061,7 +1096,9 @@"version": "0.5.3","resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz","integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",-- "license": "MIT"++ "license": "MIT",++ "optional": true,++ "peer": true},"node_modules/ms": {"version": "2.0.0",@@@ -1091,7 +1128,9 @@"version": "2.0.0","resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz","integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",-- "license": "MIT"++ "license": "MIT",++ "optional": true,++ "peer": true},"node_modules/negotiator": {"version": "0.6.3",@@@ -1107,6 +1146,8 @@"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz","integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"semver": "^7.3.5"},@@@ -1143,6 +1184,8 @@"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz","integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==","license": "ISC",++ "optional": true,++ "peer": true,"dependencies": {"wrappy": "1"}@@@ -1350,6 +1393,8 @@"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==","deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"detect-libc": "^2.0.0","expand-template": "^2.0.3",@@@ -1393,6 +1438,8 @@"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz","integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"end-of-stream": "^1.1.0","once": "^1.3.1"@@@ -1443,6 +1490,8 @@"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz","integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==","license": "(BSD-2-Clause OR MIT OR Apache-2.0)",++ "optional": true,++ "peer": true,"dependencies": {"deep-extend": "^0.6.0","ini": "~1.3.0",@@@ -1458,6 +1507,8 @@"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz","integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"inherits": "^2.0.3","string_decoder": "^1.1.1",@@@ -1676,7 +1727,9 @@"url": "https://feross.org/support"}],-- "license": "MIT"++ "license": "MIT",++ "optional": true,++ "peer": true},"node_modules/simple-get": {"version": "4.0.1",@@@ -1697,6 +1750,8 @@}],"license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"decompress-response": "^6.0.0","once": "^1.3.1",@@@ -1740,6 +1795,8 @@"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz","integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"safe-buffer": "~5.2.0"}@@@ -1749,6 +1806,8 @@"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz","integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==","license": "MIT",++ "optional": true,++ "peer": true,"engines": {"node": ">=0.10.0"}@@@ -1758,6 +1817,8 @@"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz","integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"chownr": "^1.1.1","mkdirp-classic": "^0.5.2",@@@ -1770,6 +1831,8 @@"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz","integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"bl": "^4.0.3","end-of-stream": "^1.4.1",@@@ -1795,6 +1858,8 @@"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz","integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==","license": "Apache-2.0",++ "optional": true,++ "peer": true,"dependencies": {"safe-buffer": "^5.0.1"},@@@ -1828,7 +1893,9 @@"version": "1.0.2","resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz","integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",-- "license": "MIT"++ "license": "MIT",++ "optional": true,++ "peer": true},"node_modules/utils-merge": {"version": "1.0.1",@@@ -1852,7 +1919,9 @@"version": "1.0.2","resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz","integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",-- "license": "ISC"++ "license": "ISC",++ "optional": true,++ "peer": true},"node_modules/xtend": {"version": "4.0.2",@@@ -20,7 +20,6 @@"author": "OpenVibe Community","license": "MIT","dependencies": {-- "better-sqlite3": "^11.10.0","cookie-parser": "^1.4.7","dotenv": "^16.4.7","express": "^4.21.2",ddeleted file mode 100755@@@ -1,26 +0,0 @@--#!/usr/bin/env node--'use strict';--/**-- * The one-time move of Community's SQLite database (COMMUNITY_DB_PATH) into its PostgreSQL schema (ADR-035; the-- * procedure is openvibe-sdk docs/migrating-to-postgresql.md, section 6, carried out by openvibe-sdk/db-- * runSqliteMigration).-- *-- * node scripts/migrate-to-postgres.js [--sqlite <file>] [--pglite] [--json]-- *-- * Applies migrations/ as the owner (DATABASE_DIRECT_URL), copies every table into emptied tables, verifies row counts-- * and checksums, and exits 1 unless everything verified. The SQLite file is opened read-only. Every table keeps its-- * name and columns.-- */--require('dotenv').config();--const { runSqliteMigration } = require('openvibe-sdk/db');--const config = require('../server/config');--const { MIGRATIONS } = require('../server/db');----const TABLES = {};----if (require.main === module) {-- runSqliteMigration({ service: 'community', sqlite: config.dbPath, directUrl: config.db.directUrl, migrations: MIGRATIONS, tables: TABLES })-- .then((code) => process.exit(code), (err) => { console.error(`migrate-to-postgres failed: ${err.message}`); process.exit(1); });--}----module.exports = { TABLES };@@@ -1,8 +1,8 @@#!/usr/bin/env node'use strict';/**-- * Records the N-1 fixtures (roadmap WS-P task 11) from a release: what its client calls and reads,-- * and what SQL it runs on which schema. test/n-1.test.js replays them against this checkout.++ * Records the N-1 fixtures (roadmap WS-P task 11) from a release: what its client calls and reads, and++ * the migrations it ran. test/n-1.test.js replays them against this checkout.** npm run n-1:record # from HEAD: run it right after a deploy, from the deployed commit,* # so the fixture is N-1 for the next release@@@ -14,9 +14,7 @@* Writes test/fixtures/n-1/client.json and worker.json; commit them.*/const fs = require('fs');--const os = require('os');const path = require('path');--const Database = require('better-sqlite3');const h = require('../test/n-1/harness');const svc = require('../test/n-1/service');@@@ -26,12 +24,6 @@ const OUT = path.join(ROOT, 'test', 'fixtures', 'n-1');(async () => {const ref = process.argv[2] || 'HEAD';const wt = h.worktree(ROOT, ref);-- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-n-1-record-'));-- const dbPath = path.join(tmp, 'db', `${svc.service}.db`);-- const dataDir = path.join(tmp, 'data');-- const sqlOut = path.join(tmp, 'sql.json');-- fs.mkdirSync(path.dirname(dbPath), { recursive: true });-- fs.mkdirSync(dataDir, { recursive: true });let server = null;try {console.log(`n-1:record: ${svc.service} at ${wt.sha.slice(0, 12)} (${ref})`);@@@ -43,8 +35,8 @@ const OUT = path.join(ROOT, 'test', 'fixtures', 'n-1');console.log(` client: ${files.length} files, ${calls.length} call sites with a path (${dynamic} with a computed one, not replayed)`);// 2. The release, seeded and booted; every call replayed.-- await svc.seed({ dir: wt.dir, dbPath, dataDir });-- server = await svc.boot({ dir: wt.dir, dbPath, dataDir, sqlOut });++ await svc.seed({ dir: wt.dir });++ server = await svc.boot({ dir: wt.dir });const manifest = await (await fetch(`${server.url}/release.json`)).json();const probe = await h.notFoundProbe(server.url);const requests = h.requestsFor(calls, svc.samples, { signedIn: svc.signedIn !== false });@@@ -98,44 +90,20 @@ const OUT = path.join(ROOT, 'test', 'fixtures', 'n-1');await server.close();server = null;-- // 3. The schema N-1 left, and the SQL it runs on it. A release on PostgreSQL (ADR-035) records its migration files-- // instead: N must keep every one of them unchanged and only add (the database's own ledger refuses an edit).++ // 3. The migrations N-1 ran. N must keep every one of them unchanged and only add (the database's own++ // ledger refuses an edit).const head = { service: svc.service, release: wt.sha, recorded_at: new Date().toISOString(), note: 'N-1 fixture: written by `npm run n-1:record`, replayed by test/n-1.test.js' };const clients = svc.clientReleases ? svc.clientReleases() : null;const migDir = path.join(wt.dir, 'migrations');-- if (fs.existsSync(migDir)) {-- const migrations = fs.readdirSync(migDir).filter((f) => f.endsWith('.sql')).sort()-- .map((name) => ({ name, sha256: require('crypto').createHash('sha256').update(fs.readFileSync(path.join(migDir, name))).digest('hex') }));-- fs.mkdirSync(OUT, { recursive: true });-- fs.writeFileSync(path.join(OUT, 'client.json'), `${JSON.stringify({ ...head, ...(clients ? { clients } : {}), manifest, dynamic_calls: dynamic, calls: recorded, ...(ws ? { ws } : {}) }, null, 1)}\n`);-- fs.writeFileSync(path.join(OUT, 'worker.json'), `${JSON.stringify({ ...head, engine: 'postgresql', migrations, statements: [] }, null, 1)}\n`);-- console.log(` postgresql: ${migrations.length} migration file(s); wrote ${path.relative(ROOT, OUT)}/client.json and worker.json`);-- return;-- }-- const db = new Database(dbPath);-- const schema = h.schemaDDL(db);-- const ledger = {};-- for (const t of svc.ledgerTables || []) {-- try { ledger[t] = db.prepare(`SELECT * FROM "${t}"`).all(); } catch { /* not there */ }-- }-- const userVersion = db.pragma('user_version', { simple: true });-- const serverFiles = h.readTree(wt.dir, svc.sqlDirs);-- // What N-1 creates itself on first use or at a full boot: in production it exists before N deploys.-- const lazy = h.lazyDDL(serverFiles, db);-- const ran = fs.existsSync(sqlOut) ? JSON.parse(fs.readFileSync(sqlOut, 'utf8')) : [];-- const candidates = [...new Set([...ran.map(h.normalizeSql).filter((s) => /^\s*(SELECT|INSERT|UPDATE|DELETE|REPLACE|WITH)\b/i.test(s)), ...h.sqlLiterals(serverFiles)])].sort();-- const failing = new Set(h.prepareProblems(db, candidates).map((p) => p.sql));-- const statements = candidates.filter((s) => !failing.has(s));-- db.close();-- console.log(` sql: ${statements.length} statements (${ran.length} ran, ${failing.size} candidates that do not prepare on N-1's own schema left out), ${schema.length} schema objects, ${lazy.length} created on first use`);--++ if (!fs.existsSync(migDir)) throw new Error(`${svc.service} at ${wt.sha.slice(0, 12)} has no migrations directory: it predates PostgreSQL`);++ const migrations = fs.readdirSync(migDir).filter((f) => f.endsWith('.sql')).sort()++ .map((name) => ({ name, sha256: require('crypto').createHash('sha256').update(fs.readFileSync(path.join(migDir, name))).digest('hex') }));fs.mkdirSync(OUT, { recursive: true });fs.writeFileSync(path.join(OUT, 'client.json'), `${JSON.stringify({ ...head, ...(clients ? { clients } : {}), manifest, dynamic_calls: dynamic, calls: recorded, ...(ws ? { ws } : {}) }, null, 1)}\n`);-- fs.writeFileSync(path.join(OUT, 'worker.json'), `${JSON.stringify({ ...head, user_version: userVersion, schema, ledger, lazy, statements }, null, 1)}\n`);-- console.log(` wrote ${path.relative(ROOT, OUT)}/client.json and worker.json`);++ fs.writeFileSync(path.join(OUT, 'worker.json'), `${JSON.stringify({ ...head, engine: 'postgresql', migrations }, null, 1)}\n`);++ console.log(` postgresql: ${migrations.length} migration file(s); wrote ${path.relative(ROOT, OUT)}/client.json and worker.json`);} finally {if (server) await server.close();wt.remove();-- fs.rmSync(tmp, { recursive: true, force: true });}})().catch((err) => { console.error(err.stack || err.message); process.exit(1); });@@@ -66,8 +66,6 @@ module.exports = {db: { url: process.env.DATABASE_URL || '', directUrl: process.env.DATABASE_DIRECT_URL || '' },// Valkey (ADR-035): per-actor limit counters shared across processes; without it they count in this process.valkey: { url: process.env.VALKEY_URL || '', prefix: process.env.VALKEY_PREFIX || 'ov:community:' },-- // The SQLite file of releases before PostgreSQL: read once by scripts/migrate-to-postgres.js.-- dbPath: process.env.COMMUNITY_DB_PATH || './data/community.db',// Browser origins allowed to call the embeddable APIs (/api/v1/comments, /api/v1/pulse)// with a Bearer Network JWT. No cookies cross origins.@@@ -9,8 +9,8 @@* (usr_… / gst_…), never by a service-local integer; subject_projection is only a display cache* of what the Network says about them.*-- * Timestamps stay SQLite's text ('YYYY-MM-DD HH:MM:SS', UTC): the migration defines ov_now(), datetime() and-- * julianday() with SQLite's behaviour, and ov_hot() for the forum's hot rank.++ * Timestamps are text ('YYYY-MM-DD HH:MM:SS', UTC): the schema defines ov_now(), datetime() and julianday()++ * with those semantics, and ov_hot() for the forum's hot rank.*/const crypto = require('crypto');const fs = require('fs');@@@ -229,7 +229,7 @@ async function softDeleteThread(db, id) {return (await db.prepare('UPDATE threads SET deleted_at = ov_now() WHERE id = ? AND deleted_at IS NULL').run(id)).changes;}--/** Threads a subject opened since `sinceSql` (SQLite modifier, e.g. '-1 day'); deleted ones count. */++/** Threads a subject opened since `sinceSql` (a datetime modifier, e.g. '-1 day'); deleted ones count. */async function countThreadsSince(db, subject, sinceSql) {return (await db.prepare("SELECT COUNT(*) AS c FROM threads WHERE author_subject = ? AND created_at > datetime('now', ?)").get(subject, sinceSql)).c;}@@@ -100,7 +100,7 @@ function cors(origins) {const intIn = (v, def, min, max) => Math.min(Math.max(parseInt(v, 10) || def, min), max);--/** SQLite 'YYYY-MM-DD HH:MM:SS' (UTC) ↔ ISO. */++/** Text timestamps 'YYYY-MM-DD HH:MM:SS' (UTC) ↔ ISO. */function sqlTime(date = new Date()) { return new Date(date).toISOString().replace('T', ' ').slice(0, 19); }function isoTime(v) {if (!v) return null;@@@ -26,7 +26,7 @@ const { createApp } = require('./app');const server = app.listen(config.port, config.host, () => {console.log(`[Community] ${config.nodeEnv} on http://${config.host}:${config.port} → ${config.baseUrl}`);console.log(config.pastesAuthority === 'community'-- ? `[Community] pastes: this site is the authority (${config.dbPath}), identity via ${config.networkUrl}`++ ? `[Community] pastes: this site is the authority (PostgreSQL), identity via ${config.networkUrl}`: `[Community] pastes via ${config.liveInternalUrl}/api/pastes, identity via ${config.networkUrl}`);});server.keepAliveTimeout = 65_000;@@@ -2,7 +2,7 @@/*** Track O: truthful readiness for GET /api/ready (openvibe-shared/ready).*-- * db required a real query on Community's SQLite (the schema is there and answers)++ * db required a real query on Community's PostgreSQL database (the schema is there and answers)* network_jwks optional the Network signing key has loaded. Without it public pages and reads* still work, but nobody can sign in or write as a signed-in viewer or* service (those answer 503), so it degrades rather than fails@@@ -235,7 +235,7 @@ async function listPastes(db, opts = {}) {return { rows, total };}--/** Pastes created by a subject since `sinceSql` (an SQLite datetime modifier, e.g. '-1 day'). */++/** Pastes created by a subject since `sinceSql` (a datetime modifier, e.g. '-1 day'). */async function countOwnerSince(db, subject, sinceSql) {return (await db.prepare("SELECT COUNT(*) AS c FROM pastes WHERE owner_subject = ? AND created_at > datetime('now', ?)").get(subject, sinceSql)).c;}@@@ -6,8 +6,8 @@* Race safety: a vote is a single UPSERT (or DELETE for 0) followed, in the same transaction, by* recomputing the target's score from the vote rows. The score is never incremented or decremented,* so interleaved votes from any number of connections or processes cannot drift it. The transaction-- * first locks the target row (SELECT … FOR UPDATE, PostgreSQL's stand-in for SQLite's IMMEDIATE), so-- * votes on one target queue and each recount sees every vote committed before it.++ * first locks the target row (SELECT … FOR UPDATE), so votes on one target queue and each recount++ * sees every vote committed before it.*/const TARGETS = {comment: { votes: 'comment_votes', key: 'comment_id', table: 'comments', counts: true },@@@ -9,6 +9,5 @@"name": "0001_initial.sql","sha256": "508d5c4bdc753ad04bd17ed252dc5e8301fe6853d26bab6dcd41bd906dfc4629"}-- ],-- "statements": []++ ]}@@@ -1,8 +1,7 @@'use strict';/*** N-1 (roadmap WS-P task 11, ADR-016): during a deploy, and for the 24-hour mixed-version window after-- * it, open tabs run the previous release's client against this release's server, and a previous-release-- * process can still be working on the database this release migrated. Both must keep working.++ * it, open tabs run the previous release's client against this release's server. Both must keep working.** - An N-1 client against the N server: test/fixtures/n-1/client.json is every call the previous* release's client code makes (found statically, made concrete with seeded values) with what that@@@ -11,11 +10,9 @@* for a 2xx, the same status otherwise (a 5xx only needs the route), JSON where there was JSON,* every read field still there with a type it had. The run sits inside openvibe-shared's* mixed-version matrix (release-compat assertMixedVersion) with both releases' manifests.-- * - N-1 SQL against the N schema: test/fixtures/n-1/worker.json holds the previous release's schema-- * (and migration ledger, and what it creates on first use) and every statement it ran or has as a-- * literal. The database this test boots on is created from that schema, so this release's-- * migrations run over it; afterwards every N-1 statement must still prepare (no table or column it-- * names is gone), and no N-1 INSERT may miss a column this release made NOT NULL without a default.++ * - N-1 migrations on the N schema: test/fixtures/n-1/worker.json holds the migration files the++ * previous release ran. This release must keep every one of them byte for byte and only add++ * (expand first, ADR-028).** The fixtures come from the release in production: `npm run n-1:record` after each deploy (see* scripts/n-1-record.js). This test needs no git history and no network.@@@ -24,9 +21,7 @@*/const assert = require('assert');const fs = require('fs');--const os = require('os');const path = require('path');--const Database = require('better-sqlite3');const compat = require('openvibe-shared/release-compat');const h = require('./n-1/harness');const svc = require('./n-1/service');@@@ -43,7 +38,7 @@ async function check(name, fn) {(async () => {const age = Math.round((Date.now() - Date.parse(client.recorded_at)) / 86400000);-- console.log(`n-1: ${svc.service} N-1 = ${client.release.slice(0, 12)}, recorded ${client.recorded_at.slice(0, 10)} (${age} d ago); ${client.calls.length} calls, ${worker.statements.length} statements`);++ console.log(`n-1: ${svc.service} N-1 = ${client.release.slice(0, 12)}, recorded ${client.recorded_at.slice(0, 10)} (${age} d ago); ${client.calls.length} calls, ${worker.migrations.length} migration file(s)`);console.log('n-1: the harness');await check('finds call sites, their method and template, and skips computed paths', () => {@@@ -63,57 +58,17 @@ async function check(name, fn) {assert.deepStrictEqual(h.readProblems(reads, { streams: [{ title: 'b', viewer_count: '2' }] }), ['streams[].viewer_count was number, now string', 'total is gone']);assert.ok(h.statusCompatible(200, 201) && h.statusCompatible(502, 503) && !h.statusCompatible(401, 403) && !h.statusCompatible(502, 404) && !h.statusCompatible(200, 404));});-- await check('SQL: a dropped column fails the prepare; a new NOT NULL column without a default fails an old INSERT', () => {-- const d = new Database(':memory:');-- d.exec('CREATE TABLE t (id INTEGER PRIMARY KEY, a TEXT, b TEXT NOT NULL, c TEXT NOT NULL DEFAULT \'x\')');-- assert.deepStrictEqual(h.prepareProblems(d, ['SELECT a, gone FROM t']).map((p) => p.error), ['no such column: gone']);-- assert.deepStrictEqual(h.insertProblems(d, ['INSERT INTO t (a, b) VALUES (?, ?)', 'INSERT INTO t (a) VALUES (?)']).map((p) => p.error), ['t.b is NOT NULL without a default, and N-1 does not set it']);-- d.close();-- });-- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-n-1-test-'));-- const dbPath = path.join(tmp, 'db', `${svc.service}.db`);-- const dataDir = path.join(tmp, 'data');-- fs.mkdirSync(path.dirname(dbPath), { recursive: true });-- fs.mkdirSync(dataDir, { recursive: true });let server = null;-- // This release on PostgreSQL (ADR-035) and N-1 on SQLite: they never share a database (the switch stops the-- // service, imports, then starts this release), so N-1's SQL has no schema of this release to run on. The client-- // half still runs, against a fresh migrated database.-- const pg = !!require('../server/db').initDb;-- const sqlSkip = pg && worker.engine !== 'postgresql' ? 'N-1 SQL: skipped (N-1 runs on SQLite and this release on PostgreSQL: no shared database, so no N-1 statement runs on this schema; scripts/migrate-to-postgres.js moves the data and was rehearsed)' : null;-- // Both on PostgreSQL: N keeps every migration N-1 ran, byte for byte, and only adds (expand first, ADR-028).-- if (pg && worker.engine === 'postgresql') {-- await check(`N-1's ${worker.migrations.length} migration file(s) are all here, unchanged`, () => {-- const dir = path.join(ROOT, 'migrations');-- const hash = (f) => require('crypto').createHash('sha256').update(fs.readFileSync(path.join(dir, f))).digest('hex');-- const bad = worker.migrations.filter((m) => !fs.existsSync(path.join(dir, m.name)) || hash(m.name) !== m.sha256).map((m) => m.name);-- assert.deepStrictEqual(bad, [], 'a migration N-1 ran was removed or edited: write a new one instead');-- });-- }++ // N keeps every migration N-1 ran, byte for byte, and only adds (expand first, ADR-028).++ await check(`N-1's ${worker.migrations.length} migration file(s) are all here, unchanged`, () => {++ const dir = path.join(ROOT, 'migrations');++ const hash = (f) => require('crypto').createHash('sha256').update(fs.readFileSync(path.join(dir, f))).digest('hex');++ const bad = worker.migrations.filter((m) => !fs.existsSync(path.join(dir, m.name)) || hash(m.name) !== m.sha256).map((m) => m.name);++ assert.deepStrictEqual(bad, [], 'a migration N-1 ran was removed or edited: write a new one instead');++ });try {-- if (pg) {-- server = await svc.boot({ dir: ROOT, dbPath, dataDir });-- } else {-- console.log(`n-1: this release on a database N-1 created`);-- await check('N-1\'s schema, ledger and first-use tables load, and this release migrates and seeds it', () => {-- const d = new Database(dbPath);-- d.transaction(() => {-- for (const ddl of worker.schema) d.exec(ddl);-- for (const [table, rows] of Object.entries(worker.ledger || {})) {-- for (const row of rows) {-- const cols = Object.keys(row);-- d.prepare(`INSERT INTO "${table}" (${cols.map((c) => `"${c}"`).join(', ')}) VALUES (${cols.map(() => '?').join(', ')})`).run(...cols.map((c) => row[c]));-- }-- }-- for (const ddl of worker.lazy || []) d.exec(ddl);-- })();-- d.pragma(`user_version = ${Number(worker.user_version) || 0}`);-- d.close();-- svc.seed({ dir: ROOT, dbPath, dataDir });-- });-- server = await svc.boot({ dir: ROOT, dbPath, dataDir });-- }++ server = await svc.boot({ dir: ROOT });const manifest = await (await fetch(`${server.url}/release.json`)).json();console.log('n-1: the N-1 client against this server');@@@ -141,20 +96,8 @@ async function check(name, fn) {});await server.close();server = null;---- if (sqlSkip) console.log(sqlSkip);-- else if (!pg) {-- console.log('n-1: N-1\'s SQL on the schema this release migrated');-- await check(`every statement N-1 runs still prepares (${worker.statements.length}), and none of its INSERTs misses a new required column`, () => {-- const d = new Database(dbPath, { readonly: true });-- const problems = [...h.prepareProblems(d, worker.statements), ...h.insertProblems(d, worker.statements)];-- d.close();-- assert.deepStrictEqual(problems.map((p) => `${p.error}: ${p.sql.slice(0, 160)}`), [], 'N-1 SQL that breaks on this schema (expand first, contract a release later: ADR-028)');-- });-- }} finally {if (server) { console.log(server.log().slice(-1500)); await server.close(); }-- fs.rmSync(tmp, { recursive: true, force: true });}if (failures) { console.log(`\nn-1: ${failures} check(s) failed`); process.exit(1); }@@@ -2,7 +2,7 @@/*** Runs inside a Community release (its directory is the cwd): starts that release's own Live, Network* and Media mocks (test/helpers/mock-*.js), then the app as production runs it (pastes authority-- * community, the shared database at COMMUNITY_DB_PATH = N1_DB), seeds a thread, a paste and a comment++ * community, a migrated PostgreSQL test database of its own), seeds a thread, a paste and a comment* through the API, and prints one line `{"n1": { url, token, ids }}`. SIGTERM stops it.*/const http = require('http');@@@ -32,16 +32,16 @@ const path = require('path');TRUST_PROXY: '1',OV_MEDIA_INTERNAL_URL: mediaSrv.url,PASTES_AUTHORITY: 'community',-- COMMUNITY_DB_PATH: process.env.N1_DB,});const { createApp } = require(path.join(dir, 'server', 'app'));-- // A release on PostgreSQL (ADR-035; server/db.js initDb) gets a migrated test database of its own; a SQLite-- // release opens COMMUNITY_DB_PATH itself.++ // Every release this test boots is on PostgreSQL (ADR-035; server/db.js initDb): give it a migrated++ // test database of its own.const dbMod = require(path.join(dir, 'server', 'db'));-- const pgDb = dbMod.initDb ? await require(path.join(dir, 'test', 'helpers', 'db')).testDb() : null;-- if (pgDb) dbMod.setDb(pgDb);++ const pgDb = await require(path.join(dir, 'test', 'helpers', 'db')).testDb();++ dbMod.setDb(pgDb);const app = await createApp({-- ...(pgDb ? { db: pgDb, valkey: null } : {}),++ db: pgDb,++ valkey: null,pasteLimits: { cooldownSeconds: 0 },forumLimits: { threads: { cooldownSec: 0, perMinute: 1000 }, posts: { cooldownSec: 0, perMinute: 1000 }, threadsPerDay: 1000 },commentLimits: { comments: { cooldownSec: 0, perMinute: 1000 } },@@@ -64,6 +64,6 @@ const path = require('path');const commentThread = (await call('/api/v1/comments/threads/resolve', { method: 'POST', json: { ref } })).thread;if (commentThread) await call(`/api/v1/comments/threads/${commentThread.id}/comments`, { method: 'POST', json: { body: 'A comment from N-1' } });-- process.on('SIGTERM', async () => { server.close(); try { if (pgDb) await pgDb.close(); else require(path.join(dir, 'server', 'db')).closeDb(); } catch { /* */ } process.exit(0); });++ process.on('SIGTERM', async () => { server.close(); try { await pgDb.close(); } catch { /* */ } process.exit(0); });process.stdout.write(`${JSON.stringify({ n1: { url, token, ids: { thread: thread && thread.id, thread_slug: thread && thread.slug, paste: paste && paste.slug, comment_thread: commentThread && commentThread.id, comment_access: commentThread && (commentThread.access_id || commentThread.id) } } })}\n`);})().catch((err) => { process.stderr.write(`${err.stack || err.message}\n`); process.exit(1); });@@@ -1,15 +1,14 @@'use strict';/*** N-1 harness (roadmap WS-P task 11, ADR-016's mixed-version window): the previous release's client-- * against this release's server, and the previous release's SQL against this release's schema.-- * The same file in Live, Chat, Media and Community; each repository's test/n-1/service.js says what-- * its clients are, how to boot and seed it, and where its SQL lives.++ * against this release's server. The same file in Live, Chat, Media and Community; each repository's++ * test/n-1/service.js says what its clients are and how a release boots and seeds.** scripts/n-1-record.js <ref> at release time: checks out <ref> (the release in production, which* is N-1 for the next deploy) into a temporary worktree, boots it, and* writes test/fixtures/n-1/{client,worker}.json-- * test/n-1.test.js in npm test and CI: boots this checkout on a database created with-- * N-1's schema and replays the fixture; no git history, no network++ * test/n-1.test.js in npm test and CI: boots this checkout, replays the client fixture++ * and checks the migrations N-1 ran; no git history, no network** Client calls are found statically in the N-1 client code (the call sites of `api(...)`, `fetch(...)`* and the service's other wrappers, with a literal or template path), made concrete with sample values,@@@ -17,9 +16,6 @@* fixture keeps per call is what an old client depends on: the status, whether the answer is JSON,* and the response fields it reads (the JSON key paths whose every key the N-1 client code names, with* their types).-- *-- * SQL is every statement N-1 prepared while it served those calls, plus every literal SQL string in its-- * server code that prepares on its own schema. The N-1 schema is its sqlite_master after the replay.*/const fs = require('fs');const os = require('os');@@@ -668,87 +664,6 @@ function wsProblems(recorded, received) {return problems;}--// ── SQL ──────────────────────────────────────────────────────----const SQL_START = /^\s*(SELECT|INSERT|UPDATE|DELETE|REPLACE|WITH)\b/i;----/** Every plain JS string literal in the files (a template only without ${}), comments skipped. */--function stringLiterals(files) {-- const out = [];-- for (const { text } of files) {-- for (let i = 0; i < text.length; i++) {-- const c = text[i];-- if (c === '/' && text[i + 1] === '/') { const nl = text.indexOf('\n', i); i = nl < 0 ? text.length : nl; continue; }-- if (c === '/' && text[i + 1] === '*') { const e = text.indexOf('*/', i + 2); i = e < 0 ? text.length : e + 1; continue; }-- if (c !== '\'' && c !== '"' && c !== '`') continue;-- const lit = readLiteral(text, i);-- if (!lit) continue;-- i = lit.end - 1;-- if (lit.parts.length === 1 && lit.parts[0].text != null) out.push(lit.parts[0].text);-- }-- }-- return out;--}----/** The literals that read like one SQL statement (DML). */--function sqlLiterals(files) {-- const out = new Set();-- for (const t of stringLiterals(files)) {-- const sql = normalizeSql(t);-- if (SQL_START.test(sql) && /\b(FROM|INTO|SET|VALUES)\b/i.test(sql)) out.add(sql);-- }-- return [...out];--}----/** SQL without its comments (-- to the end of a line, and block comments), outside quoted text. */--function stripSqlComments(sql) {-- let out = '';-- const s = String(sql);-- for (let i = 0; i < s.length; i++) {-- const c = s[i];-- if (c === '\'' || c === '"') {-- const j = s.indexOf(c, i + 1);-- const end = j < 0 ? s.length : j + 1;-- out += s.slice(i, end); i = end - 1; continue;-- }-- if (c === '-' && s[i + 1] === '-') { const nl = s.indexOf('\n', i); i = nl < 0 ? s.length : nl - 1; continue; }-- if (c === '/' && s[i + 1] === '*') { const e = s.indexOf('*/', i + 2); i = e < 0 ? s.length : e + 1; out += ' '; continue; }-- out += c;-- }-- return out;--}----function normalizeSql(sql) {-- return stripSqlComments(sql).replace(/\s+/g, ' ').trim();--}----/**-- * The DDL N-1 runs itself when it first needs a table or column (a module's CREATE TABLE IF NOT EXISTS,-- * a boot-time ALTER TABLE … ADD COLUMN) that `db` does not have yet. Each statement is applied to `db`-- * as it is found to apply; → the applied statements, tables first, in order.-- */--function lazyDDL(files, db) {-- const tables = () => new Set(db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map((r) => r.name));-- const columns = (t) => { try { return new Set(db.prepare(`PRAGMA table_xinfo("${t}")`).all().map((c) => c.name.toLowerCase())); } catch { return new Set(); } };-- const lits = stringLiterals(files).map(stripSqlComments);-- const applied = [];-- const apply = (ddl) => { try { db.exec(ddl); applied.push(ddl); return true; } catch { return false; } };-- for (const text of lits) {-- for (const m of text.matchAll(/CREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\s+["`]?(\w+)["`]?\s*\(/gi)) {-- // A rebuild's scratch table (x_new) is the migration's own business, not a table N-1 uses.-- if (tables().has(m[1]) || /_(new|old|tmp|backup)$/i.test(m[1])) continue;-- const end = skipBalanced(text, m.index + m[0].length - 1, '(', ')');-- if (end >= 0) apply(normalizeSql(text.slice(m.index, end + 1)));-- }-- }-- for (const text of lits) {-- const m = /^\s*ALTER\s+TABLE\s+["`]?(\w+)["`]?\s+ADD\s+(?:COLUMN\s+)?["`]?(\w+)["`]?[^;]*$/i.exec(text.trim().replace(/;$/, ''));-- if (!m || !tables().has(m[1]) || columns(m[1]).has(m[2].toLowerCase())) continue;-- apply(normalizeSql(text).replace(/;$/, ''));-- }-- return applied;--}--/** Every file under dir with one of the extensions, as { name (relative to root), text }. */function readTree(root, dirs, exts = ['.js']) {const out = [];@@@ -768,50 +683,6 @@ function readTree(root, dirs, exts = ['.js']) {return out.sort((a, b) => (a.name < b.name ? -1 : 1));}--/** The schema as DDL, tables first (no internal or FTS shadow tables), then indexes, triggers and views. */--function schemaDDL(db) {-- const shadow = new Set(db.prepare('PRAGMA table_list').all().filter((t) => t.type === 'shadow').map((t) => t.name));-- const rows = db.prepare("SELECT type, name, sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%'").all()-- .filter((r) => !shadow.has(r.name));-- const order = { table: 0, index: 1, view: 2, trigger: 3 };-- return rows.sort((a, b) => order[a.type] - order[b.type] || (a.name < b.name ? -1 : 1)).map((r) => r.sql);--}----/** Prepares every statement on db (a missing table or column fails the prepare). → [{ sql, error }] */--function prepareProblems(db, statements) {-- const problems = [];-- for (const sql of statements) {-- try { db.prepare(sql); } catch (e) { problems.push({ sql, error: e.message }); }-- }-- return problems;--}----/**-- * INSERTs of N-1 that N would refuse: a column N made NOT NULL without a default that the INSERT does-- * not name. → [{ sql, error }]-- */--function insertProblems(db, statements) {-- const problems = [];-- const cols = new Map();-- const required = (table) => {-- if (!cols.has(table)) {-- let info = [];-- try { info = db.prepare(`PRAGMA table_xinfo("${table.replace(/"/g, '""')}")`).all(); } catch { info = []; }-- const pkInt = info.filter((c) => c.pk).length === 1 && info.find((c) => c.pk && /^INTEGER$/i.test(c.type));-- cols.set(table, info.filter((c) => c.notnull && c.dflt_value == null && !c.hidden && !(pkInt && c.pk)).map((c) => c.name));-- }-- return cols.get(table);-- };-- for (const sql of statements) {-- const m = /^\s*(?:INSERT|REPLACE)(?:\s+OR\s+\w+)?\s+INTO\s+["`]?(\w+)["`]?\s*\(([^)]*)\)\s*(VALUES|SELECT)/i.exec(sql);-- if (!m) continue;-- const named = new Set(m[2].split(',').map((c) => c.trim().replace(/^["`]|["`]$/g, '').toLowerCase()));-- const missing = required(m[1]).filter((c) => !named.has(c.toLowerCase()));-- if (missing.length) problems.push({ sql, error: `${m[1]}.${missing.join(', ')} is NOT NULL without a default, and N-1 does not set it` });-- }-- return problems;--}--// ── Git ──────────────────────────────────────────────────────/** A detached worktree of `ref` in a temp directory sharing root's node_modules → { dir, sha, remove() }. */@@@ -850,6 +721,6 @@ module.exports = {flatten, readsOf, readProblems, statusCompatible,send, requestsFor, notFoundProbe, isNoRoute, callProblems,wsSends, wsHandled, wsSession, wsMessages, wsReads, wsProblems,-- stringLiterals, sqlLiterals, stripSqlComments, normalizeSql, lazyDDL, readTree, schemaDDL, prepareProblems, insertProblems,++ readTree,worktree, gitFiles, summarize,};ddeleted file mode 100644@@@ -1,23 +0,0 @@--'use strict';--/**-- * Loaded with `node -r` into a release booted by test/n-1/service.js (N-1 at record time, N in the-- * test), from that release's directory. With N1_SQL_OUT set, every SQL text the process prepares or-- * executes on the community database (COMMUNITY_DB_PATH, read when the statement runs) is written-- * there as JSON when it exits.-- */--const fs = require('fs');--const path = require('path');----const out = process.env.N1_SQL_OUT;--if (out) {-- const Database = require(path.join(process.cwd(), 'node_modules', 'better-sqlite3'));-- const seen = new Set();-- const note = (db, sql) => {-- try { if (process.env.COMMUNITY_DB_PATH && path.resolve(db.name) === path.resolve(process.env.COMMUNITY_DB_PATH)) seen.add(String(sql)); } catch { /* */ }-- };-- const prepare = Database.prototype.prepare;-- Database.prototype.prepare = function (sql, ...rest) { note(this, sql); return prepare.call(this, sql, ...rest); };-- const exec = Database.prototype.exec;-- Database.prototype.exec = function (sql, ...rest) { note(this, sql); return exec.call(this, sql, ...rest); };-- process.on('exit', () => { try { fs.writeFileSync(out, JSON.stringify([...seen])); } catch { /* */ } });--}@@@ -1,22 +1,21 @@'use strict';/*** Community's side of the N-1 harness (test/n-1/harness.js): its clients, how a release boots and is-- * seeded, where its SQL lives. Used by scripts/n-1-record.js (on N-1, in a temporary worktree) and by-- * test/n-1.test.js (on this checkout), so both boot and seed the same way.++ * seeded. Used by scripts/n-1-record.js (on N-1, in a temporary worktree) and by test/n-1.test.js (on++ * this checkout), so both boot and seed the same way.** Community's clients are its pages: public/js/community.js, the forms and scripts the templates in* server/render write (an open tab posts the form it was served), the links, scripts and forms of the* pages N-1 served (crawled), and the openvibe-sdk community client (pastes) as that release installed it.** A release boots in a child process (test/n-1/boot-child.js) with its own Live, Network and Media-- * mocks, the pastes authority on, and its database at COMMUNITY_DB_PATH; sign-in is the ov_token-- * cookie the Network mock signs.++ * mocks, the pastes authority on, and a migrated PostgreSQL test database of its own; sign-in is the++ * ov_token cookie the Network mock signs.*/const path = require('path');const { spawn } = require('child_process');const { readTree } = require('./harness');--const PRELOAD = path.join(__dirname, 'preload.js');const CHILD = path.join(__dirname, 'boot-child.js');function baseEnv(extra) {@@@ -50,17 +49,14 @@ module.exports = {[/id\)*$/i, '1'],],-- sqlDirs: ['server'],-- ledgerTables: [],--/** Community migrates when it opens its database (server/db.js openDb); nothing to do first. */seed() {},-- /** Boots the release in `dir` on dbPath → { url, ids, headers(auth), close() }. */-- async boot({ dir, dbPath, sqlOut = '' }) {-- const child = spawn(process.execPath, ['-r', PRELOAD, CHILD], {++ /** Boots the release in `dir` on a migrated PostgreSQL test database → { url, ids, headers(auth), close() }. */++ async boot({ dir }) {++ const child = spawn(process.execPath, [CHILD], {cwd: dir,-- env: baseEnv({ N1_DB: dbPath, N1_SQL_OUT: sqlOut }),++ env: baseEnv({}),stdio: ['ignore', 'pipe', 'pipe'],});let log = '';@@@ -4,8 +4,6 @@// measurement; raising one is a decision to state in the commit.// node test/perf-budget.test.jsconst assert = require('assert');--const fs = require('fs');--const os = require('os');const net = require('net');const path = require('path');const { spawn } = require('child_process');@@@ -26,11 +24,10 @@ const BUDGETS = {const freePort = () => new Promise((resolve) => { const s = net.createServer(); s.listen(0, '127.0.0.1', () => { const { port } = s.address(); s.close(() => resolve(port)); }); });(async () => {-- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-budget-'));const port = await freePort();const child = spawn(process.execPath, [path.join(__dirname, '..', 'server', 'index.js')], {cwd: path.join(__dirname, '..'),-- env: { ...process.env, PORT: String(port), HOST: '127.0.0.1', NODE_ENV: 'test', COMMUNITY_DB_PATH: path.join(dir, 'community.db') },++ env: { ...process.env, PORT: String(port), HOST: '127.0.0.1', NODE_ENV: 'test' },stdio: ['ignore', 'ignore', 'pipe'],});let stderr = '';@@@ -50,6 +47,5 @@ const freePort = () => new Promise((resolve) => { const s = net.createServer();console.log('perf budget: all checks passed');} finally {child.kill();-- fs.rmSync(dir, { recursive: true, force: true });}})().catch((err) => { console.error(err); process.exitCode = 1; });
@@ -58,15 +58,13 @@ OV_CHAT_URL=https://openvibe.chat
@@ -58,15 +58,13 @@ OV_CHAT_URL=https://openvibe.chat
# With 'community' the OAuth client above also needs identity.subject.resolve (author names).
# With 'community' the OAuth client above also needs identity.subject.resolve (author names).
PASTES_AUTHORITY=live
PASTES_AUTHORITY=live
# PostgreSQL (ADR-035), written by OpenVibe.Host roles/data/add-service.sh community: the runtime role through
# PostgreSQL (ADR-035), written by OpenVibe.Host roles/data/add-service.sh community: the runtime role through
-# PgBouncer, the owner on a direct connection (migrations and the one-time import), and Valkey (shared limit
-# counters). Comments, the forum, Pulse and the relay's bookkeeping live there in every mode; pastes too with
-# 'community'. Without DATABASE_URL, development uses an embedded PGlite database in data/pglite.
+# PgBouncer, the owner on a direct connection (migrations), and Valkey (shared limit counters). Comments, the
+# forum, Pulse and the relay's bookkeeping live there in every mode; pastes too with 'community'. Without
+# DATABASE_URL, development uses an embedded PGlite database in data/pglite.
DATABASE_URL=
DATABASE_URL=
DATABASE_DIRECT_URL=
DATABASE_DIRECT_URL=
VALKEY_URL=
VALKEY_URL=
VALKEY_PREFIX=
VALKEY_PREFIX=
-# The SQLite file of releases before PostgreSQL: read once by scripts/migrate-to-postgres.js.
-COMMUNITY_DB_PATH=./data/community.db
# Optional salt for hashed visitor ids in view counting (default: derived from the client secret)
# Optional salt for hashed visitor ids in view counting (default: derived from the client secret)
#VIEW_HASH_SECRET=
#VIEW_HASH_SECRET=
@@ -587,7 +587,6 @@ Copy `.env.example` to `.env` (production: `/etc/openvibe/community.env`, mode 0
@@ -587,7 +587,6 @@ Copy `.env.example` to `.env` (production: `/etc/openvibe/community.env`, mode 0
| `PASTES_AUTHORITY` | `live` | `live` = proxy to Live; `community` = this site's database is the authority |
| `PASTES_AUTHORITY` | `live` | `live` = proxy to Live; `community` = this site's database is the authority |
| `DATABASE_URL`, `DATABASE_DIRECT_URL` | unset (development: embedded PGlite in `data/pglite`) | PostgreSQL through PgBouncer, and the owner's direct connection for migrations (written by OpenVibe.Host `roles/data/add-service.sh community`) |
| `DATABASE_URL`, `DATABASE_DIRECT_URL` | unset (development: embedded PGlite in `data/pglite`) | PostgreSQL through PgBouncer, and the owner's direct connection for migrations (written by OpenVibe.Host `roles/data/add-service.sh community`) |
| `VALKEY_URL`, `VALKEY_PREFIX` | unset | per-actor limit counters shared across processes |
| `VALKEY_URL`, `VALKEY_PREFIX` | unset | per-actor limit counters shared across processes |
-| `COMMUNITY_DB_PATH` | `./data/community.db` | the SQLite file of releases before PostgreSQL, read once by `scripts/migrate-to-postgres.js` |
| `API_CORS_ORIGINS` | Live, Media, Network, Tools, Games origins | Browser origins that may call `/api/v1/comments` and `/api/v1/pulse` with a Bearer JWT |
| `API_CORS_ORIGINS` | Live, Media, Network, Tools, Games origins | Browser origins that may call `/api/v1/comments` and `/api/v1/pulse` with a Bearer JWT |
| `DISCORD_RELAY_ENABLED` | off | `true` turns the Discord relay on ([docs/discord-relay.md](docs/discord-relay.md)) |
| `DISCORD_RELAY_ENABLED` | off | `true` turns the Discord relay on ([docs/discord-relay.md](docs/discord-relay.md)) |
| `DISCORD_RELAY_POLL_MS` / `DISCORD_RELAY_BACKOFF_MS` / `DISCORD_RELAY_MAX_ATTEMPTS` | `30000` / `30000` / `6` | Relay sender cadence, first retry delay, attempts before `failed` (the dead letter) |
| `DISCORD_RELAY_POLL_MS` / `DISCORD_RELAY_BACKOFF_MS` / `DISCORD_RELAY_MAX_ATTEMPTS` | `30000` / `30000` / `6` | Relay sender cadence, first retry delay, attempts before `failed` (the dead letter) |
@@ -616,12 +615,11 @@ npm run n-1:record # after a deploy: the N-1 fixtures from the deployed commit (
@@ -616,12 +615,11 @@ npm run n-1:record # after a deploy: the N-1 fixtures from the deployed commit (
```
```
`test/n-1.test.js` (roadmap WS-P task 11, in `npm test`) runs the previous release's clients against
`test/n-1.test.js` (roadmap WS-P task 11, in `npm test`) runs the previous release's clients against
-this one and its SQL against this schema, from `test/fixtures/n-1/`: every call `community.js`, the
-templates' forms and the openvibe-sdk community client make, and every link, script and form of the
-pages that release served, each answered compatibly (status, JSON, the response fields the client
-reads); then every statement the previous release runs must still prepare after this release opened
-(and migrated) a database the previous one created. After each deploy, record the release now in
-production as the next N-1 and commit `test/fixtures/n-1/`.
+this one, from `test/fixtures/n-1/`: every call `community.js`, the templates' forms and the
+openvibe-sdk community client make, and every link, script and form of the pages that release served,
+each answered compatibly (status, JSON, the response fields the client reads); then this release must
+keep every migration that release ran byte for byte and only add. After each deploy, record the
+release now in production as the next N-1 and commit `test/fixtures/n-1/`.
## Acceptance
## Acceptance
@@ -665,10 +663,7 @@ Production deploys with `sudo ovhost deploy community` on the host (strategy `gi
@@ -665,10 +663,7 @@ Production deploys with `sudo ovhost deploy community` on the host (strategy `gi
fast-forward `/opt/openvibe.community`, install on a lockfile change, restart, wait for `/api/ready`).
fast-forward `/opt/openvibe.community`, install on a lockfile change, restart, wait for `/api/ready`).
The unit is `openvibe-community.service` on `127.0.0.1:4200`, the env file `/etc/openvibe/community.env`. The database is
The unit is `openvibe-community.service` on `127.0.0.1:4200`, the env file `/etc/openvibe/community.env`. The database is
`ov_community` on the host's data role (`sudo /opt/openvibe.host/roles/data/add-service.sh community` writes its settings); the
`ov_community` on the host's data role (`sudo /opt/openvibe.host/roles/data/add-service.sh community` writes its settings); the
-release migrates it at boot. The one-time move from SQLite is `scripts/migrate-to-postgres.js` (openvibe-sdk
-`runSqliteMigration`, with a `--pglite` rehearsal mode), run while the service is stopped; the old
-`/var/lib/openvibe-community/community.db` stays read-only for 7 days as the rollback. After a
-deploy, record the N-1 fixtures (`npm run n-1:record`).
+release migrates it at boot. After a deploy, record the N-1 fixtures (`npm run n-1:record`).
Rollback: ovhost puts the previous sha back by itself when `/api/ready` does not answer 2xx after the
Rollback: ovhost puts the previous sha back by itself when `/api/ready` does not answer 2xx after the
restart; afterwards `sudo ovhost rollback community --to <sha>`. Migrations only add tables and columns.
restart; afterwards `sudo ovhost rollback community --to <sha>`. Migrations only add tables and columns.
@@ -732,7 +727,6 @@ server/
@@ -732,7 +727,6 @@ server/
public/ css/community.css, js/community.js, favicon.svg, og-default.png
public/ css/community.css, js/community.js, favicon.svg, og-default.png
(openvibe-shared is the pinned OpenVibe.Shared v1.25.0 release, installed by npm)
(openvibe-shared is the pinned OpenVibe.Shared v1.25.0 release, installed by npm)
deploy/ systemd unit, nginx vhost
deploy/ systemd unit, nginx vhost
-scripts/migrate-to-postgres.js the one-time SQLite → PostgreSQL import (runSqliteMigration; --pglite rehearsal)
test/ run.js + *.test.js (mock Live, Network and Media with a real RS256 key)
test/ run.js + *.test.js (mock Live, Network and Media with a real RS256 key)
docs/capabilities-proposal/ Wave 5 capability manifests (released in openvibe-contracts v0.7.0)
docs/capabilities-proposal/ Wave 5 capability manifests (released in openvibe-contracts v0.7.0)
docs/discord-relay.md the Discord relay: how it works, owner steps, staff API, limits
docs/discord-relay.md the Discord relay: how it works, owner steps, staff API, limits
@@ -21,10 +21,8 @@ NoNewPrivileges=true
@@ -21,10 +21,8 @@ NoNewPrivileges=true
PrivateTmp=true
PrivateTmp=true
ProtectSystem=strict
ProtectSystem=strict
ProtectHome=true
ProtectHome=true
-# The code tree stays read-only. Community's database is PostgreSQL (ov_community, ADR-035); /var/lib/openvibe-community
-# keeps the pre-PostgreSQL SQLite file (read-only) for the rollback window and the one-time import.
+# The code tree stays read-only. Community's database is PostgreSQL (ov_community, ADR-035).
StateDirectory=openvibe-community
StateDirectory=openvibe-community
-Environment=COMMUNITY_DB_PATH=/var/lib/openvibe-community/community.db
[Install]
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target
@@ -9,7 +9,6 @@
@@ -9,7 +9,6 @@
"version": "1.0.0",
"version": "1.0.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
- "better-sqlite3": "^11.10.0",
"cookie-parser": "^1.4.7",
"cookie-parser": "^1.4.7",
"dotenv": "^16.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express": "^4.21.2",
@@ -119,7 +118,9 @@
@@ -119,7 +118,9 @@
"url": "https://feross.org/support"
"url": "https://feross.org/support"
}
}
],
],
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
},
"node_modules/better-sqlite3": {
"node_modules/better-sqlite3": {
"version": "11.10.0",
"version": "11.10.0",
@@ -127,6 +128,8 @@
@@ -127,6 +128,8 @@
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
"hasInstallScript": true,
"hasInstallScript": true,
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"bindings": "^1.5.0",
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
"prebuild-install": "^7.1.1"
@@ -137,6 +140,8 @@
@@ -137,6 +140,8 @@
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"file-uri-to-path": "1.0.0"
"file-uri-to-path": "1.0.0"
}
}
@@ -146,6 +151,8 @@
@@ -146,6 +151,8 @@
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"buffer": "^5.5.0",
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"inherits": "^2.0.4",
@@ -195,6 +202,8 @@
@@ -195,6 +202,8 @@
}
}
],
],
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"base64-js": "^1.3.1",
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
"ieee754": "^1.1.13"
@@ -259,7 +268,9 @@
@@ -259,7 +268,9 @@
"version": "1.1.4",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
- "license": "ISC"
+ "license": "ISC",
+ "optional": true,
+ "peer": true
},
},
"node_modules/cluster-key-slot": {
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"version": "1.1.2",
@@ -333,6 +344,8 @@
@@ -333,6 +344,8 @@
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"mimic-response": "^3.1.0"
"mimic-response": "^3.1.0"
},
},
@@ -348,6 +361,8 @@
@@ -348,6 +361,8 @@
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"engines": {
"engines": {
"node": ">=4.0.0"
"node": ">=4.0.0"
}
}
@@ -385,6 +400,8 @@
@@ -385,6 +400,8 @@
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"license": "Apache-2.0",
+ "optional": true,
+ "peer": true,
"engines": {
"engines": {
"node": ">=8"
"node": ">=8"
}
}
@@ -444,6 +461,8 @@
@@ -444,6 +461,8 @@
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"once": "^1.4.0"
"once": "^1.4.0"
}
}
@@ -498,6 +517,8 @@
@@ -498,6 +517,8 @@
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"license": "(MIT OR WTFPL)",
"license": "(MIT OR WTFPL)",
+ "optional": true,
+ "peer": true,
"engines": {
"engines": {
"node": ">=6"
"node": ">=6"
}
}
@@ -589,7 +610,9 @@
@@ -589,7 +610,9 @@
"version": "1.0.0",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
},
"node_modules/finalhandler": {
"node_modules/finalhandler": {
"version": "1.3.2",
"version": "1.3.2",
@@ -631,7 +654,9 @@
@@ -631,7 +654,9 @@
"version": "1.0.0",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
},
"node_modules/function-bind": {
"node_modules/function-bind": {
"version": "1.1.2",
"version": "1.1.2",
@@ -683,7 +708,9 @@
@@ -683,7 +708,9 @@
"version": "0.0.0",
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
},
"node_modules/gopd": {
"node_modules/gopd": {
"version": "1.2.0",
"version": "1.2.0",
@@ -792,7 +819,9 @@
@@ -792,7 +819,9 @@
"url": "https://feross.org/support"
"url": "https://feross.org/support"
}
}
],
],
- "license": "BSD-3-Clause"
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "peer": true
},
},
"node_modules/inherits": {
"node_modules/inherits": {
"version": "2.0.4",
"version": "2.0.4",
@@ -804,7 +833,9 @@
@@ -804,7 +833,9 @@
"version": "1.3.8",
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
- "license": "ISC"
+ "license": "ISC",
+ "optional": true,
+ "peer": true
},
},
"node_modules/iovalkey": {
"node_modules/iovalkey": {
"version": "0.4.0",
"version": "0.4.0",
@@ -1041,6 +1072,8 @@
@@ -1041,6 +1072,8 @@
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"engines": {
"engines": {
"node": ">=10"
"node": ">=10"
},
},
@@ -1053,6 +1086,8 @@
@@ -1053,6 +1086,8 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"funding": {
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://github.com/sponsors/ljharb"
}
}
@@ -1061,7 +1096,9 @@
@@ -1061,7 +1096,9 @@
"version": "0.5.3",
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
},
"node_modules/ms": {
"node_modules/ms": {
"version": "2.0.0",
"version": "2.0.0",
@@ -1091,7 +1128,9 @@
@@ -1091,7 +1128,9 @@
"version": "2.0.0",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
},
"node_modules/negotiator": {
"node_modules/negotiator": {
"version": "0.6.3",
"version": "0.6.3",
@@ -1107,6 +1146,8 @@
@@ -1107,6 +1146,8 @@
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz",
"integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==",
"integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"semver": "^7.3.5"
"semver": "^7.3.5"
},
},
@@ -1143,6 +1184,8 @@
@@ -1143,6 +1184,8 @@
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"license": "ISC",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"wrappy": "1"
"wrappy": "1"
}
}
@@ -1350,6 +1393,8 @@
@@ -1350,6 +1393,8 @@
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"detect-libc": "^2.0.0",
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"expand-template": "^2.0.3",
@@ -1393,6 +1438,8 @@
@@ -1393,6 +1438,8 @@
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"end-of-stream": "^1.1.0",
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
"once": "^1.3.1"
@@ -1443,6 +1490,8 @@
@@ -1443,6 +1490,8 @@
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"deep-extend": "^0.6.0",
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"ini": "~1.3.0",
@@ -1458,6 +1507,8 @@
@@ -1458,6 +1507,8 @@
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"inherits": "^2.0.3",
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"string_decoder": "^1.1.1",
@@ -1676,7 +1727,9 @@
@@ -1676,7 +1727,9 @@
"url": "https://feross.org/support"
"url": "https://feross.org/support"
}
}
],
],
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
},
"node_modules/simple-get": {
"node_modules/simple-get": {
"version": "4.0.1",
"version": "4.0.1",
@@ -1697,6 +1750,8 @@
@@ -1697,6 +1750,8 @@
}
}
],
],
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"decompress-response": "^6.0.0",
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"once": "^1.3.1",
@@ -1740,6 +1795,8 @@
@@ -1740,6 +1795,8 @@
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"safe-buffer": "~5.2.0"
"safe-buffer": "~5.2.0"
}
}
@@ -1749,6 +1806,8 @@
@@ -1749,6 +1806,8 @@
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"engines": {
"engines": {
"node": ">=0.10.0"
"node": ">=0.10.0"
}
}
@@ -1758,6 +1817,8 @@
@@ -1758,6 +1817,8 @@
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
"integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",
"integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"chownr": "^1.1.1",
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"mkdirp-classic": "^0.5.2",
@@ -1770,6 +1831,8 @@
@@ -1770,6 +1831,8 @@
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"bl": "^4.0.3",
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"end-of-stream": "^1.4.1",
@@ -1795,6 +1858,8 @@
@@ -1795,6 +1858,8 @@
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"license": "Apache-2.0",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"safe-buffer": "^5.0.1"
"safe-buffer": "^5.0.1"
},
},
@@ -1828,7 +1893,9 @@
@@ -1828,7 +1893,9 @@
"version": "1.0.2",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
},
"node_modules/utils-merge": {
"node_modules/utils-merge": {
"version": "1.0.1",
"version": "1.0.1",
@@ -1852,7 +1919,9 @@
@@ -1852,7 +1919,9 @@
"version": "1.0.2",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "license": "ISC"
+ "license": "ISC",
+ "optional": true,
+ "peer": true
},
},
"node_modules/xtend": {
"node_modules/xtend": {
"version": "4.0.2",
"version": "4.0.2",
@@ -20,7 +20,6 @@
@@ -20,7 +20,6 @@
"author": "OpenVibe Community",
"author": "OpenVibe Community",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
- "better-sqlite3": "^11.10.0",
"cookie-parser": "^1.4.7",
"cookie-parser": "^1.4.7",
"dotenv": "^16.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express": "^4.21.2",
deleted file mode 100755
deleted file mode 100755
@@ -1,26 +0,0 @@
@@ -1,26 +0,0 @@
-#!/usr/bin/env node
-'use strict';
-/**
- * The one-time move of Community's SQLite database (COMMUNITY_DB_PATH) into its PostgreSQL schema (ADR-035; the
- * procedure is openvibe-sdk docs/migrating-to-postgresql.md, section 6, carried out by openvibe-sdk/db
- * runSqliteMigration).
- *
- * node scripts/migrate-to-postgres.js [--sqlite <file>] [--pglite] [--json]
- *
- * Applies migrations/ as the owner (DATABASE_DIRECT_URL), copies every table into emptied tables, verifies row counts
- * and checksums, and exits 1 unless everything verified. The SQLite file is opened read-only. Every table keeps its
- * name and columns.
- */
-require('dotenv').config();
-const { runSqliteMigration } = require('openvibe-sdk/db');
-const config = require('../server/config');
-const { MIGRATIONS } = require('../server/db');
-
-const TABLES = {};
-
-if (require.main === module) {
- runSqliteMigration({ service: 'community', sqlite: config.dbPath, directUrl: config.db.directUrl, migrations: MIGRATIONS, tables: TABLES })
- .then((code) => process.exit(code), (err) => { console.error(`migrate-to-postgres failed: ${err.message}`); process.exit(1); });
-}
-
-module.exports = { TABLES };
@@ -1,8 +1,8 @@
@@ -1,8 +1,8 @@
#!/usr/bin/env node
#!/usr/bin/env node
'use strict';
'use strict';
/**
/**
- * Records the N-1 fixtures (roadmap WS-P task 11) from a release: what its client calls and reads,
- * and what SQL it runs on which schema. test/n-1.test.js replays them against this checkout.
+ * Records the N-1 fixtures (roadmap WS-P task 11) from a release: what its client calls and reads, and
+ * the migrations it ran. test/n-1.test.js replays them against this checkout.
*
*
* npm run n-1:record # from HEAD: run it right after a deploy, from the deployed commit,
* npm run n-1:record # from HEAD: run it right after a deploy, from the deployed commit,
* # so the fixture is N-1 for the next release
* # so the fixture is N-1 for the next release
@@ -14,9 +14,7 @@
@@ -14,9 +14,7 @@
* Writes test/fixtures/n-1/client.json and worker.json; commit them.
* Writes test/fixtures/n-1/client.json and worker.json; commit them.
*/
*/
const fs = require('fs');
const fs = require('fs');
-const os = require('os');
const path = require('path');
const path = require('path');
-const Database = require('better-sqlite3');
const h = require('../test/n-1/harness');
const h = require('../test/n-1/harness');
const svc = require('../test/n-1/service');
const svc = require('../test/n-1/service');
@@ -26,12 +24,6 @@ const OUT = path.join(ROOT, 'test', 'fixtures', 'n-1');
@@ -26,12 +24,6 @@ const OUT = path.join(ROOT, 'test', 'fixtures', 'n-1');
(async () => {
(async () => {
const ref = process.argv[2] || 'HEAD';
const ref = process.argv[2] || 'HEAD';
const wt = h.worktree(ROOT, ref);
const wt = h.worktree(ROOT, ref);
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-n-1-record-'));
- const dbPath = path.join(tmp, 'db', `${svc.service}.db`);
- const dataDir = path.join(tmp, 'data');
- const sqlOut = path.join(tmp, 'sql.json');
- fs.mkdirSync(path.dirname(dbPath), { recursive: true });
- fs.mkdirSync(dataDir, { recursive: true });
let server = null;
let server = null;
try {
try {
console.log(`n-1:record: ${svc.service} at ${wt.sha.slice(0, 12)} (${ref})`);
console.log(`n-1:record: ${svc.service} at ${wt.sha.slice(0, 12)} (${ref})`);
@@ -43,8 +35,8 @@ const OUT = path.join(ROOT, 'test', 'fixtures', 'n-1');
@@ -43,8 +35,8 @@ const OUT = path.join(ROOT, 'test', 'fixtures', 'n-1');
console.log(` client: ${files.length} files, ${calls.length} call sites with a path (${dynamic} with a computed one, not replayed)`);
console.log(` client: ${files.length} files, ${calls.length} call sites with a path (${dynamic} with a computed one, not replayed)`);
// 2. The release, seeded and booted; every call replayed.
// 2. The release, seeded and booted; every call replayed.
- await svc.seed({ dir: wt.dir, dbPath, dataDir });
- server = await svc.boot({ dir: wt.dir, dbPath, dataDir, sqlOut });
+ await svc.seed({ dir: wt.dir });
+ server = await svc.boot({ dir: wt.dir });
const manifest = await (await fetch(`${server.url}/release.json`)).json();
const manifest = await (await fetch(`${server.url}/release.json`)).json();
const probe = await h.notFoundProbe(server.url);
const probe = await h.notFoundProbe(server.url);
const requests = h.requestsFor(calls, svc.samples, { signedIn: svc.signedIn !== false });
const requests = h.requestsFor(calls, svc.samples, { signedIn: svc.signedIn !== false });
@@ -98,44 +90,20 @@ const OUT = path.join(ROOT, 'test', 'fixtures', 'n-1');
@@ -98,44 +90,20 @@ const OUT = path.join(ROOT, 'test', 'fixtures', 'n-1');
await server.close();
await server.close();
server = null;
server = null;
- // 3. The schema N-1 left, and the SQL it runs on it. A release on PostgreSQL (ADR-035) records its migration files
- // instead: N must keep every one of them unchanged and only add (the database's own ledger refuses an edit).
+ // 3. The migrations N-1 ran. N must keep every one of them unchanged and only add (the database's own
+ // ledger refuses an edit).
const head = { service: svc.service, release: wt.sha, recorded_at: new Date().toISOString(), note: 'N-1 fixture: written by `npm run n-1:record`, replayed by test/n-1.test.js' };
const head = { service: svc.service, release: wt.sha, recorded_at: new Date().toISOString(), note: 'N-1 fixture: written by `npm run n-1:record`, replayed by test/n-1.test.js' };
const clients = svc.clientReleases ? svc.clientReleases() : null;
const clients = svc.clientReleases ? svc.clientReleases() : null;
const migDir = path.join(wt.dir, 'migrations');
const migDir = path.join(wt.dir, 'migrations');
- if (fs.existsSync(migDir)) {
- const migrations = fs.readdirSync(migDir).filter((f) => f.endsWith('.sql')).sort()
- .map((name) => ({ name, sha256: require('crypto').createHash('sha256').update(fs.readFileSync(path.join(migDir, name))).digest('hex') }));
- fs.mkdirSync(OUT, { recursive: true });
- fs.writeFileSync(path.join(OUT, 'client.json'), `${JSON.stringify({ ...head, ...(clients ? { clients } : {}), manifest, dynamic_calls: dynamic, calls: recorded, ...(ws ? { ws } : {}) }, null, 1)}\n`);
- fs.writeFileSync(path.join(OUT, 'worker.json'), `${JSON.stringify({ ...head, engine: 'postgresql', migrations, statements: [] }, null, 1)}\n`);
- console.log(` postgresql: ${migrations.length} migration file(s); wrote ${path.relative(ROOT, OUT)}/client.json and worker.json`);
- return;
- }
- const db = new Database(dbPath);
- const schema = h.schemaDDL(db);
- const ledger = {};
- for (const t of svc.ledgerTables || []) {
- try { ledger[t] = db.prepare(`SELECT * FROM "${t}"`).all(); } catch { /* not there */ }
- }
- const userVersion = db.pragma('user_version', { simple: true });
- const serverFiles = h.readTree(wt.dir, svc.sqlDirs);
- // What N-1 creates itself on first use or at a full boot: in production it exists before N deploys.
- const lazy = h.lazyDDL(serverFiles, db);
- const ran = fs.existsSync(sqlOut) ? JSON.parse(fs.readFileSync(sqlOut, 'utf8')) : [];
- const candidates = [...new Set([...ran.map(h.normalizeSql).filter((s) => /^\s*(SELECT|INSERT|UPDATE|DELETE|REPLACE|WITH)\b/i.test(s)), ...h.sqlLiterals(serverFiles)])].sort();
- const failing = new Set(h.prepareProblems(db, candidates).map((p) => p.sql));
- const statements = candidates.filter((s) => !failing.has(s));
- db.close();
- console.log(` sql: ${statements.length} statements (${ran.length} ran, ${failing.size} candidates that do not prepare on N-1's own schema left out), ${schema.length} schema objects, ${lazy.length} created on first use`);
-
+ if (!fs.existsSync(migDir)) throw new Error(`${svc.service} at ${wt.sha.slice(0, 12)} has no migrations directory: it predates PostgreSQL`);
+ const migrations = fs.readdirSync(migDir).filter((f) => f.endsWith('.sql')).sort()
+ .map((name) => ({ name, sha256: require('crypto').createHash('sha256').update(fs.readFileSync(path.join(migDir, name))).digest('hex') }));
fs.mkdirSync(OUT, { recursive: true });
fs.mkdirSync(OUT, { recursive: true });
fs.writeFileSync(path.join(OUT, 'client.json'), `${JSON.stringify({ ...head, ...(clients ? { clients } : {}), manifest, dynamic_calls: dynamic, calls: recorded, ...(ws ? { ws } : {}) }, null, 1)}\n`);
fs.writeFileSync(path.join(OUT, 'client.json'), `${JSON.stringify({ ...head, ...(clients ? { clients } : {}), manifest, dynamic_calls: dynamic, calls: recorded, ...(ws ? { ws } : {}) }, null, 1)}\n`);
- fs.writeFileSync(path.join(OUT, 'worker.json'), `${JSON.stringify({ ...head, user_version: userVersion, schema, ledger, lazy, statements }, null, 1)}\n`);
- console.log(` wrote ${path.relative(ROOT, OUT)}/client.json and worker.json`);
+ fs.writeFileSync(path.join(OUT, 'worker.json'), `${JSON.stringify({ ...head, engine: 'postgresql', migrations }, null, 1)}\n`);
+ console.log(` postgresql: ${migrations.length} migration file(s); wrote ${path.relative(ROOT, OUT)}/client.json and worker.json`);
} finally {
} finally {
if (server) await server.close();
if (server) await server.close();
wt.remove();
wt.remove();
- fs.rmSync(tmp, { recursive: true, force: true });
}
}
})().catch((err) => { console.error(err.stack || err.message); process.exit(1); });
})().catch((err) => { console.error(err.stack || err.message); process.exit(1); });
@@ -66,8 +66,6 @@ module.exports = {
@@ -66,8 +66,6 @@ module.exports = {
db: { url: process.env.DATABASE_URL || '', directUrl: process.env.DATABASE_DIRECT_URL || '' },
db: { url: process.env.DATABASE_URL || '', directUrl: process.env.DATABASE_DIRECT_URL || '' },
// Valkey (ADR-035): per-actor limit counters shared across processes; without it they count in this process.
// Valkey (ADR-035): per-actor limit counters shared across processes; without it they count in this process.
valkey: { url: process.env.VALKEY_URL || '', prefix: process.env.VALKEY_PREFIX || 'ov:community:' },
valkey: { url: process.env.VALKEY_URL || '', prefix: process.env.VALKEY_PREFIX || 'ov:community:' },
- // The SQLite file of releases before PostgreSQL: read once by scripts/migrate-to-postgres.js.
- dbPath: process.env.COMMUNITY_DB_PATH || './data/community.db',
// Browser origins allowed to call the embeddable APIs (/api/v1/comments, /api/v1/pulse)
// Browser origins allowed to call the embeddable APIs (/api/v1/comments, /api/v1/pulse)
// with a Bearer Network JWT. No cookies cross origins.
// with a Bearer Network JWT. No cookies cross origins.
@@ -9,8 +9,8 @@
@@ -9,8 +9,8 @@
* (usr_… / gst_…), never by a service-local integer; subject_projection is only a display cache
* (usr_… / gst_…), never by a service-local integer; subject_projection is only a display cache
* of what the Network says about them.
* of what the Network says about them.
*
*
- * Timestamps stay SQLite's text ('YYYY-MM-DD HH:MM:SS', UTC): the migration defines ov_now(), datetime() and
- * julianday() with SQLite's behaviour, and ov_hot() for the forum's hot rank.
+ * Timestamps are text ('YYYY-MM-DD HH:MM:SS', UTC): the schema defines ov_now(), datetime() and julianday()
+ * with those semantics, and ov_hot() for the forum's hot rank.
*/
*/
const crypto = require('crypto');
const crypto = require('crypto');
const fs = require('fs');
const fs = require('fs');
@@ -229,7 +229,7 @@ async function softDeleteThread(db, id) {
@@ -229,7 +229,7 @@ async function softDeleteThread(db, id) {
return (await db.prepare('UPDATE threads SET deleted_at = ov_now() WHERE id = ? AND deleted_at IS NULL').run(id)).changes;
return (await db.prepare('UPDATE threads SET deleted_at = ov_now() WHERE id = ? AND deleted_at IS NULL').run(id)).changes;
}
}
-/** Threads a subject opened since `sinceSql` (SQLite modifier, e.g. '-1 day'); deleted ones count. */
+/** Threads a subject opened since `sinceSql` (a datetime modifier, e.g. '-1 day'); deleted ones count. */
async function countThreadsSince(db, subject, sinceSql) {
async function countThreadsSince(db, subject, sinceSql) {
return (await db.prepare("SELECT COUNT(*) AS c FROM threads WHERE author_subject = ? AND created_at > datetime('now', ?)").get(subject, sinceSql)).c;
return (await db.prepare("SELECT COUNT(*) AS c FROM threads WHERE author_subject = ? AND created_at > datetime('now', ?)").get(subject, sinceSql)).c;
}
}
@@ -100,7 +100,7 @@ function cors(origins) {
@@ -100,7 +100,7 @@ function cors(origins) {
const intIn = (v, def, min, max) => Math.min(Math.max(parseInt(v, 10) || def, min), max);
const intIn = (v, def, min, max) => Math.min(Math.max(parseInt(v, 10) || def, min), max);
-/** SQLite 'YYYY-MM-DD HH:MM:SS' (UTC) ↔ ISO. */
+/** Text timestamps 'YYYY-MM-DD HH:MM:SS' (UTC) ↔ ISO. */
function sqlTime(date = new Date()) { return new Date(date).toISOString().replace('T', ' ').slice(0, 19); }
function sqlTime(date = new Date()) { return new Date(date).toISOString().replace('T', ' ').slice(0, 19); }
function isoTime(v) {
function isoTime(v) {
if (!v) return null;
if (!v) return null;
@@ -26,7 +26,7 @@ const { createApp } = require('./app');
@@ -26,7 +26,7 @@ const { createApp } = require('./app');
const server = app.listen(config.port, config.host, () => {
const server = app.listen(config.port, config.host, () => {
console.log(`[Community] ${config.nodeEnv} on http://${config.host}:${config.port} → ${config.baseUrl}`);
console.log(`[Community] ${config.nodeEnv} on http://${config.host}:${config.port} → ${config.baseUrl}`);
console.log(config.pastesAuthority === 'community'
console.log(config.pastesAuthority === 'community'
- ? `[Community] pastes: this site is the authority (${config.dbPath}), identity via ${config.networkUrl}`
+ ? `[Community] pastes: this site is the authority (PostgreSQL), identity via ${config.networkUrl}`
: `[Community] pastes via ${config.liveInternalUrl}/api/pastes, identity via ${config.networkUrl}`);
: `[Community] pastes via ${config.liveInternalUrl}/api/pastes, identity via ${config.networkUrl}`);
});
});
server.keepAliveTimeout = 65_000;
server.keepAliveTimeout = 65_000;
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
/**
/**
* Track O: truthful readiness for GET /api/ready (openvibe-shared/ready).
* Track O: truthful readiness for GET /api/ready (openvibe-shared/ready).
*
*
- * db required a real query on Community's SQLite (the schema is there and answers)
+ * db required a real query on Community's PostgreSQL database (the schema is there and answers)
* network_jwks optional the Network signing key has loaded. Without it public pages and reads
* network_jwks optional the Network signing key has loaded. Without it public pages and reads
* still work, but nobody can sign in or write as a signed-in viewer or
* still work, but nobody can sign in or write as a signed-in viewer or
* service (those answer 503), so it degrades rather than fails
* service (those answer 503), so it degrades rather than fails
@@ -235,7 +235,7 @@ async function listPastes(db, opts = {}) {
@@ -235,7 +235,7 @@ async function listPastes(db, opts = {}) {
return { rows, total };
return { rows, total };
}
}
-/** Pastes created by a subject since `sinceSql` (an SQLite datetime modifier, e.g. '-1 day'). */
+/** Pastes created by a subject since `sinceSql` (a datetime modifier, e.g. '-1 day'). */
async function countOwnerSince(db, subject, sinceSql) {
async function countOwnerSince(db, subject, sinceSql) {
return (await db.prepare("SELECT COUNT(*) AS c FROM pastes WHERE owner_subject = ? AND created_at > datetime('now', ?)").get(subject, sinceSql)).c;
return (await db.prepare("SELECT COUNT(*) AS c FROM pastes WHERE owner_subject = ? AND created_at > datetime('now', ?)").get(subject, sinceSql)).c;
}
}
@@ -6,8 +6,8 @@
@@ -6,8 +6,8 @@
* Race safety: a vote is a single UPSERT (or DELETE for 0) followed, in the same transaction, by
* Race safety: a vote is a single UPSERT (or DELETE for 0) followed, in the same transaction, by
* recomputing the target's score from the vote rows. The score is never incremented or decremented,
* recomputing the target's score from the vote rows. The score is never incremented or decremented,
* so interleaved votes from any number of connections or processes cannot drift it. The transaction
* so interleaved votes from any number of connections or processes cannot drift it. The transaction
- * first locks the target row (SELECT … FOR UPDATE, PostgreSQL's stand-in for SQLite's IMMEDIATE), so
- * votes on one target queue and each recount sees every vote committed before it.
+ * first locks the target row (SELECT … FOR UPDATE), so votes on one target queue and each recount
+ * sees every vote committed before it.
*/
*/
const TARGETS = {
const TARGETS = {
comment: { votes: 'comment_votes', key: 'comment_id', table: 'comments', counts: true },
comment: { votes: 'comment_votes', key: 'comment_id', table: 'comments', counts: true },
@@ -9,6 +9,5 @@
@@ -9,6 +9,5 @@
"name": "0001_initial.sql",
"name": "0001_initial.sql",
"sha256": "508d5c4bdc753ad04bd17ed252dc5e8301fe6853d26bab6dcd41bd906dfc4629"
"sha256": "508d5c4bdc753ad04bd17ed252dc5e8301fe6853d26bab6dcd41bd906dfc4629"
}
}
- ],
- "statements": []
+ ]
}
}
@@ -1,8 +1,7 @@
@@ -1,8 +1,7 @@
'use strict';
'use strict';
/**
/**
* N-1 (roadmap WS-P task 11, ADR-016): during a deploy, and for the 24-hour mixed-version window after
* N-1 (roadmap WS-P task 11, ADR-016): during a deploy, and for the 24-hour mixed-version window after
- * it, open tabs run the previous release's client against this release's server, and a previous-release
- * process can still be working on the database this release migrated. Both must keep working.
+ * it, open tabs run the previous release's client against this release's server. Both must keep working.
*
*
* - An N-1 client against the N server: test/fixtures/n-1/client.json is every call the previous
* - An N-1 client against the N server: test/fixtures/n-1/client.json is every call the previous
* release's client code makes (found statically, made concrete with seeded values) with what that
* release's client code makes (found statically, made concrete with seeded values) with what that
@@ -11,11 +10,9 @@
@@ -11,11 +10,9 @@
* for a 2xx, the same status otherwise (a 5xx only needs the route), JSON where there was JSON,
* for a 2xx, the same status otherwise (a 5xx only needs the route), JSON where there was JSON,
* every read field still there with a type it had. The run sits inside openvibe-shared's
* every read field still there with a type it had. The run sits inside openvibe-shared's
* mixed-version matrix (release-compat assertMixedVersion) with both releases' manifests.
* mixed-version matrix (release-compat assertMixedVersion) with both releases' manifests.
- * - N-1 SQL against the N schema: test/fixtures/n-1/worker.json holds the previous release's schema
- * (and migration ledger, and what it creates on first use) and every statement it ran or has as a
- * literal. The database this test boots on is created from that schema, so this release's
- * migrations run over it; afterwards every N-1 statement must still prepare (no table or column it
- * names is gone), and no N-1 INSERT may miss a column this release made NOT NULL without a default.
+ * - N-1 migrations on the N schema: test/fixtures/n-1/worker.json holds the migration files the
+ * previous release ran. This release must keep every one of them byte for byte and only add
+ * (expand first, ADR-028).
*
*
* The fixtures come from the release in production: `npm run n-1:record` after each deploy (see
* The fixtures come from the release in production: `npm run n-1:record` after each deploy (see
* scripts/n-1-record.js). This test needs no git history and no network.
* scripts/n-1-record.js). This test needs no git history and no network.
@@ -24,9 +21,7 @@
@@ -24,9 +21,7 @@
*/
*/
const assert = require('assert');
const assert = require('assert');
const fs = require('fs');
const fs = require('fs');
-const os = require('os');
const path = require('path');
const path = require('path');
-const Database = require('better-sqlite3');
const compat = require('openvibe-shared/release-compat');
const compat = require('openvibe-shared/release-compat');
const h = require('./n-1/harness');
const h = require('./n-1/harness');
const svc = require('./n-1/service');
const svc = require('./n-1/service');
@@ -43,7 +38,7 @@ async function check(name, fn) {
@@ -43,7 +38,7 @@ async function check(name, fn) {
(async () => {
(async () => {
const age = Math.round((Date.now() - Date.parse(client.recorded_at)) / 86400000);
const age = Math.round((Date.now() - Date.parse(client.recorded_at)) / 86400000);
- console.log(`n-1: ${svc.service} N-1 = ${client.release.slice(0, 12)}, recorded ${client.recorded_at.slice(0, 10)} (${age} d ago); ${client.calls.length} calls, ${worker.statements.length} statements`);
+ console.log(`n-1: ${svc.service} N-1 = ${client.release.slice(0, 12)}, recorded ${client.recorded_at.slice(0, 10)} (${age} d ago); ${client.calls.length} calls, ${worker.migrations.length} migration file(s)`);
console.log('n-1: the harness');
console.log('n-1: the harness');
await check('finds call sites, their method and template, and skips computed paths', () => {
await check('finds call sites, their method and template, and skips computed paths', () => {
@@ -63,57 +58,17 @@ async function check(name, fn) {
@@ -63,57 +58,17 @@ async function check(name, fn) {
assert.deepStrictEqual(h.readProblems(reads, { streams: [{ title: 'b', viewer_count: '2' }] }), ['streams[].viewer_count was number, now string', 'total is gone']);
assert.deepStrictEqual(h.readProblems(reads, { streams: [{ title: 'b', viewer_count: '2' }] }), ['streams[].viewer_count was number, now string', 'total is gone']);
assert.ok(h.statusCompatible(200, 201) && h.statusCompatible(502, 503) && !h.statusCompatible(401, 403) && !h.statusCompatible(502, 404) && !h.statusCompatible(200, 404));
assert.ok(h.statusCompatible(200, 201) && h.statusCompatible(502, 503) && !h.statusCompatible(401, 403) && !h.statusCompatible(502, 404) && !h.statusCompatible(200, 404));
});
});
- await check('SQL: a dropped column fails the prepare; a new NOT NULL column without a default fails an old INSERT', () => {
- const d = new Database(':memory:');
- d.exec('CREATE TABLE t (id INTEGER PRIMARY KEY, a TEXT, b TEXT NOT NULL, c TEXT NOT NULL DEFAULT \'x\')');
- assert.deepStrictEqual(h.prepareProblems(d, ['SELECT a, gone FROM t']).map((p) => p.error), ['no such column: gone']);
- assert.deepStrictEqual(h.insertProblems(d, ['INSERT INTO t (a, b) VALUES (?, ?)', 'INSERT INTO t (a) VALUES (?)']).map((p) => p.error), ['t.b is NOT NULL without a default, and N-1 does not set it']);
- d.close();
- });
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-n-1-test-'));
- const dbPath = path.join(tmp, 'db', `${svc.service}.db`);
- const dataDir = path.join(tmp, 'data');
- fs.mkdirSync(path.dirname(dbPath), { recursive: true });
- fs.mkdirSync(dataDir, { recursive: true });
let server = null;
let server = null;
- // This release on PostgreSQL (ADR-035) and N-1 on SQLite: they never share a database (the switch stops the
- // service, imports, then starts this release), so N-1's SQL has no schema of this release to run on. The client
- // half still runs, against a fresh migrated database.
- const pg = !!require('../server/db').initDb;
- const sqlSkip = pg && worker.engine !== 'postgresql' ? 'N-1 SQL: skipped (N-1 runs on SQLite and this release on PostgreSQL: no shared database, so no N-1 statement runs on this schema; scripts/migrate-to-postgres.js moves the data and was rehearsed)' : null;
- // Both on PostgreSQL: N keeps every migration N-1 ran, byte for byte, and only adds (expand first, ADR-028).
- if (pg && worker.engine === 'postgresql') {
- await check(`N-1's ${worker.migrations.length} migration file(s) are all here, unchanged`, () => {
- const dir = path.join(ROOT, 'migrations');
- const hash = (f) => require('crypto').createHash('sha256').update(fs.readFileSync(path.join(dir, f))).digest('hex');
- const bad = worker.migrations.filter((m) => !fs.existsSync(path.join(dir, m.name)) || hash(m.name) !== m.sha256).map((m) => m.name);
- assert.deepStrictEqual(bad, [], 'a migration N-1 ran was removed or edited: write a new one instead');
- });
- }
+ // N keeps every migration N-1 ran, byte for byte, and only adds (expand first, ADR-028).
+ await check(`N-1's ${worker.migrations.length} migration file(s) are all here, unchanged`, () => {
+ const dir = path.join(ROOT, 'migrations');
+ const hash = (f) => require('crypto').createHash('sha256').update(fs.readFileSync(path.join(dir, f))).digest('hex');
+ const bad = worker.migrations.filter((m) => !fs.existsSync(path.join(dir, m.name)) || hash(m.name) !== m.sha256).map((m) => m.name);
+ assert.deepStrictEqual(bad, [], 'a migration N-1 ran was removed or edited: write a new one instead');
+ });
try {
try {
- if (pg) {
- server = await svc.boot({ dir: ROOT, dbPath, dataDir });
- } else {
- console.log(`n-1: this release on a database N-1 created`);
- await check('N-1\'s schema, ledger and first-use tables load, and this release migrates and seeds it', () => {
- const d = new Database(dbPath);
- d.transaction(() => {
- for (const ddl of worker.schema) d.exec(ddl);
- for (const [table, rows] of Object.entries(worker.ledger || {})) {
- for (const row of rows) {
- const cols = Object.keys(row);
- d.prepare(`INSERT INTO "${table}" (${cols.map((c) => `"${c}"`).join(', ')}) VALUES (${cols.map(() => '?').join(', ')})`).run(...cols.map((c) => row[c]));
- }
- }
- for (const ddl of worker.lazy || []) d.exec(ddl);
- })();
- d.pragma(`user_version = ${Number(worker.user_version) || 0}`);
- d.close();
- svc.seed({ dir: ROOT, dbPath, dataDir });
- });
- server = await svc.boot({ dir: ROOT, dbPath, dataDir });
- }
+ server = await svc.boot({ dir: ROOT });
const manifest = await (await fetch(`${server.url}/release.json`)).json();
const manifest = await (await fetch(`${server.url}/release.json`)).json();
console.log('n-1: the N-1 client against this server');
console.log('n-1: the N-1 client against this server');
@@ -141,20 +96,8 @@ async function check(name, fn) {
@@ -141,20 +96,8 @@ async function check(name, fn) {
});
});
await server.close();
await server.close();
server = null;
server = null;
-
- if (sqlSkip) console.log(sqlSkip);
- else if (!pg) {
- console.log('n-1: N-1\'s SQL on the schema this release migrated');
- await check(`every statement N-1 runs still prepares (${worker.statements.length}), and none of its INSERTs misses a new required column`, () => {
- const d = new Database(dbPath, { readonly: true });
- const problems = [...h.prepareProblems(d, worker.statements), ...h.insertProblems(d, worker.statements)];
- d.close();
- assert.deepStrictEqual(problems.map((p) => `${p.error}: ${p.sql.slice(0, 160)}`), [], 'N-1 SQL that breaks on this schema (expand first, contract a release later: ADR-028)');
- });
- }
} finally {
} finally {
if (server) { console.log(server.log().slice(-1500)); await server.close(); }
if (server) { console.log(server.log().slice(-1500)); await server.close(); }
- fs.rmSync(tmp, { recursive: true, force: true });
}
}
if (failures) { console.log(`\nn-1: ${failures} check(s) failed`); process.exit(1); }
if (failures) { console.log(`\nn-1: ${failures} check(s) failed`); process.exit(1); }
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
/**
/**
* Runs inside a Community release (its directory is the cwd): starts that release's own Live, Network
* Runs inside a Community release (its directory is the cwd): starts that release's own Live, Network
* and Media mocks (test/helpers/mock-*.js), then the app as production runs it (pastes authority
* and Media mocks (test/helpers/mock-*.js), then the app as production runs it (pastes authority
- * community, the shared database at COMMUNITY_DB_PATH = N1_DB), seeds a thread, a paste and a comment
+ * community, a migrated PostgreSQL test database of its own), seeds a thread, a paste and a comment
* through the API, and prints one line `{"n1": { url, token, ids }}`. SIGTERM stops it.
* through the API, and prints one line `{"n1": { url, token, ids }}`. SIGTERM stops it.
*/
*/
const http = require('http');
const http = require('http');
@@ -32,16 +32,16 @@ const path = require('path');
@@ -32,16 +32,16 @@ const path = require('path');
TRUST_PROXY: '1',
TRUST_PROXY: '1',
OV_MEDIA_INTERNAL_URL: mediaSrv.url,
OV_MEDIA_INTERNAL_URL: mediaSrv.url,
PASTES_AUTHORITY: 'community',
PASTES_AUTHORITY: 'community',
- COMMUNITY_DB_PATH: process.env.N1_DB,
});
});
const { createApp } = require(path.join(dir, 'server', 'app'));
const { createApp } = require(path.join(dir, 'server', 'app'));
- // A release on PostgreSQL (ADR-035; server/db.js initDb) gets a migrated test database of its own; a SQLite
- // release opens COMMUNITY_DB_PATH itself.
+ // Every release this test boots is on PostgreSQL (ADR-035; server/db.js initDb): give it a migrated
+ // test database of its own.
const dbMod = require(path.join(dir, 'server', 'db'));
const dbMod = require(path.join(dir, 'server', 'db'));
- const pgDb = dbMod.initDb ? await require(path.join(dir, 'test', 'helpers', 'db')).testDb() : null;
- if (pgDb) dbMod.setDb(pgDb);
+ const pgDb = await require(path.join(dir, 'test', 'helpers', 'db')).testDb();
+ dbMod.setDb(pgDb);
const app = await createApp({
const app = await createApp({
- ...(pgDb ? { db: pgDb, valkey: null } : {}),
+ db: pgDb,
+ valkey: null,
pasteLimits: { cooldownSeconds: 0 },
pasteLimits: { cooldownSeconds: 0 },
forumLimits: { threads: { cooldownSec: 0, perMinute: 1000 }, posts: { cooldownSec: 0, perMinute: 1000 }, threadsPerDay: 1000 },
forumLimits: { threads: { cooldownSec: 0, perMinute: 1000 }, posts: { cooldownSec: 0, perMinute: 1000 }, threadsPerDay: 1000 },
commentLimits: { comments: { cooldownSec: 0, perMinute: 1000 } },
commentLimits: { comments: { cooldownSec: 0, perMinute: 1000 } },
@@ -64,6 +64,6 @@ const path = require('path');
@@ -64,6 +64,6 @@ const path = require('path');
const commentThread = (await call('/api/v1/comments/threads/resolve', { method: 'POST', json: { ref } })).thread;
const commentThread = (await call('/api/v1/comments/threads/resolve', { method: 'POST', json: { ref } })).thread;
if (commentThread) await call(`/api/v1/comments/threads/${commentThread.id}/comments`, { method: 'POST', json: { body: 'A comment from N-1' } });
if (commentThread) await call(`/api/v1/comments/threads/${commentThread.id}/comments`, { method: 'POST', json: { body: 'A comment from N-1' } });
- process.on('SIGTERM', async () => { server.close(); try { if (pgDb) await pgDb.close(); else require(path.join(dir, 'server', 'db')).closeDb(); } catch { /* */ } process.exit(0); });
+ process.on('SIGTERM', async () => { server.close(); try { await pgDb.close(); } catch { /* */ } process.exit(0); });
process.stdout.write(`${JSON.stringify({ n1: { url, token, ids: { thread: thread && thread.id, thread_slug: thread && thread.slug, paste: paste && paste.slug, comment_thread: commentThread && commentThread.id, comment_access: commentThread && (commentThread.access_id || commentThread.id) } } })}\n`);
process.stdout.write(`${JSON.stringify({ n1: { url, token, ids: { thread: thread && thread.id, thread_slug: thread && thread.slug, paste: paste && paste.slug, comment_thread: commentThread && commentThread.id, comment_access: commentThread && (commentThread.access_id || commentThread.id) } } })}\n`);
})().catch((err) => { process.stderr.write(`${err.stack || err.message}\n`); process.exit(1); });
})().catch((err) => { process.stderr.write(`${err.stack || err.message}\n`); process.exit(1); });
@@ -1,15 +1,14 @@
@@ -1,15 +1,14 @@
'use strict';
'use strict';
/**
/**
* N-1 harness (roadmap WS-P task 11, ADR-016's mixed-version window): the previous release's client
* N-1 harness (roadmap WS-P task 11, ADR-016's mixed-version window): the previous release's client
- * against this release's server, and the previous release's SQL against this release's schema.
- * The same file in Live, Chat, Media and Community; each repository's test/n-1/service.js says what
- * its clients are, how to boot and seed it, and where its SQL lives.
+ * against this release's server. The same file in Live, Chat, Media and Community; each repository's
+ * test/n-1/service.js says what its clients are and how a release boots and seeds.
*
*
* scripts/n-1-record.js <ref> at release time: checks out <ref> (the release in production, which
* scripts/n-1-record.js <ref> at release time: checks out <ref> (the release in production, which
* is N-1 for the next deploy) into a temporary worktree, boots it, and
* is N-1 for the next deploy) into a temporary worktree, boots it, and
* writes test/fixtures/n-1/{client,worker}.json
* writes test/fixtures/n-1/{client,worker}.json
- * test/n-1.test.js in npm test and CI: boots this checkout on a database created with
- * N-1's schema and replays the fixture; no git history, no network
+ * test/n-1.test.js in npm test and CI: boots this checkout, replays the client fixture
+ * and checks the migrations N-1 ran; no git history, no network
*
*
* Client calls are found statically in the N-1 client code (the call sites of `api(...)`, `fetch(...)`
* Client calls are found statically in the N-1 client code (the call sites of `api(...)`, `fetch(...)`
* and the service's other wrappers, with a literal or template path), made concrete with sample values,
* and the service's other wrappers, with a literal or template path), made concrete with sample values,
@@ -17,9 +16,6 @@
@@ -17,9 +16,6 @@
* fixture keeps per call is what an old client depends on: the status, whether the answer is JSON,
* fixture keeps per call is what an old client depends on: the status, whether the answer is JSON,
* and the response fields it reads (the JSON key paths whose every key the N-1 client code names, with
* and the response fields it reads (the JSON key paths whose every key the N-1 client code names, with
* their types).
* their types).
- *
- * SQL is every statement N-1 prepared while it served those calls, plus every literal SQL string in its
- * server code that prepares on its own schema. The N-1 schema is its sqlite_master after the replay.
*/
*/
const fs = require('fs');
const fs = require('fs');
const os = require('os');
const os = require('os');
@@ -668,87 +664,6 @@ function wsProblems(recorded, received) {
@@ -668,87 +664,6 @@ function wsProblems(recorded, received) {
return problems;
return problems;
}
}
-// ── SQL ──────────────────────────────────────────────────────
-
-const SQL_START = /^\s*(SELECT|INSERT|UPDATE|DELETE|REPLACE|WITH)\b/i;
-
-/** Every plain JS string literal in the files (a template only without ${}), comments skipped. */
-function stringLiterals(files) {
- const out = [];
- for (const { text } of files) {
- for (let i = 0; i < text.length; i++) {
- const c = text[i];
- if (c === '/' && text[i + 1] === '/') { const nl = text.indexOf('\n', i); i = nl < 0 ? text.length : nl; continue; }
- if (c === '/' && text[i + 1] === '*') { const e = text.indexOf('*/', i + 2); i = e < 0 ? text.length : e + 1; continue; }
- if (c !== '\'' && c !== '"' && c !== '`') continue;
- const lit = readLiteral(text, i);
- if (!lit) continue;
- i = lit.end - 1;
- if (lit.parts.length === 1 && lit.parts[0].text != null) out.push(lit.parts[0].text);
- }
- }
- return out;
-}
-
-/** The literals that read like one SQL statement (DML). */
-function sqlLiterals(files) {
- const out = new Set();
- for (const t of stringLiterals(files)) {
- const sql = normalizeSql(t);
- if (SQL_START.test(sql) && /\b(FROM|INTO|SET|VALUES)\b/i.test(sql)) out.add(sql);
- }
- return [...out];
-}
-
-/** SQL without its comments (-- to the end of a line, and block comments), outside quoted text. */
-function stripSqlComments(sql) {
- let out = '';
- const s = String(sql);
- for (let i = 0; i < s.length; i++) {
- const c = s[i];
- if (c === '\'' || c === '"') {
- const j = s.indexOf(c, i + 1);
- const end = j < 0 ? s.length : j + 1;
- out += s.slice(i, end); i = end - 1; continue;
- }
- if (c === '-' && s[i + 1] === '-') { const nl = s.indexOf('\n', i); i = nl < 0 ? s.length : nl - 1; continue; }
- if (c === '/' && s[i + 1] === '*') { const e = s.indexOf('*/', i + 2); i = e < 0 ? s.length : e + 1; out += ' '; continue; }
- out += c;
- }
- return out;
-}
-
-function normalizeSql(sql) {
- return stripSqlComments(sql).replace(/\s+/g, ' ').trim();
-}
-
-/**
- * The DDL N-1 runs itself when it first needs a table or column (a module's CREATE TABLE IF NOT EXISTS,
- * a boot-time ALTER TABLE … ADD COLUMN) that `db` does not have yet. Each statement is applied to `db`
- * as it is found to apply; → the applied statements, tables first, in order.
- */
-function lazyDDL(files, db) {
- const tables = () => new Set(db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map((r) => r.name));
- const columns = (t) => { try { return new Set(db.prepare(`PRAGMA table_xinfo("${t}")`).all().map((c) => c.name.toLowerCase())); } catch { return new Set(); } };
- const lits = stringLiterals(files).map(stripSqlComments);
- const applied = [];
- const apply = (ddl) => { try { db.exec(ddl); applied.push(ddl); return true; } catch { return false; } };
- for (const text of lits) {
- for (const m of text.matchAll(/CREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\s+["`]?(\w+)["`]?\s*\(/gi)) {
- // A rebuild's scratch table (x_new) is the migration's own business, not a table N-1 uses.
- if (tables().has(m[1]) || /_(new|old|tmp|backup)$/i.test(m[1])) continue;
- const end = skipBalanced(text, m.index + m[0].length - 1, '(', ')');
- if (end >= 0) apply(normalizeSql(text.slice(m.index, end + 1)));
- }
- }
- for (const text of lits) {
- const m = /^\s*ALTER\s+TABLE\s+["`]?(\w+)["`]?\s+ADD\s+(?:COLUMN\s+)?["`]?(\w+)["`]?[^;]*$/i.exec(text.trim().replace(/;$/, ''));
- if (!m || !tables().has(m[1]) || columns(m[1]).has(m[2].toLowerCase())) continue;
- apply(normalizeSql(text).replace(/;$/, ''));
- }
- return applied;
-}
-
/** Every file under dir with one of the extensions, as { name (relative to root), text }. */
/** Every file under dir with one of the extensions, as { name (relative to root), text }. */
function readTree(root, dirs, exts = ['.js']) {
function readTree(root, dirs, exts = ['.js']) {
const out = [];
const out = [];
@@ -768,50 +683,6 @@ function readTree(root, dirs, exts = ['.js']) {
@@ -768,50 +683,6 @@ function readTree(root, dirs, exts = ['.js']) {
return out.sort((a, b) => (a.name < b.name ? -1 : 1));
return out.sort((a, b) => (a.name < b.name ? -1 : 1));
}
}
-/** The schema as DDL, tables first (no internal or FTS shadow tables), then indexes, triggers and views. */
-function schemaDDL(db) {
- const shadow = new Set(db.prepare('PRAGMA table_list').all().filter((t) => t.type === 'shadow').map((t) => t.name));
- const rows = db.prepare("SELECT type, name, sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%'").all()
- .filter((r) => !shadow.has(r.name));
- const order = { table: 0, index: 1, view: 2, trigger: 3 };
- return rows.sort((a, b) => order[a.type] - order[b.type] || (a.name < b.name ? -1 : 1)).map((r) => r.sql);
-}
-
-/** Prepares every statement on db (a missing table or column fails the prepare). → [{ sql, error }] */
-function prepareProblems(db, statements) {
- const problems = [];
- for (const sql of statements) {
- try { db.prepare(sql); } catch (e) { problems.push({ sql, error: e.message }); }
- }
- return problems;
-}
-
-/**
- * INSERTs of N-1 that N would refuse: a column N made NOT NULL without a default that the INSERT does
- * not name. → [{ sql, error }]
- */
-function insertProblems(db, statements) {
- const problems = [];
- const cols = new Map();
- const required = (table) => {
- if (!cols.has(table)) {
- let info = [];
- try { info = db.prepare(`PRAGMA table_xinfo("${table.replace(/"/g, '""')}")`).all(); } catch { info = []; }
- const pkInt = info.filter((c) => c.pk).length === 1 && info.find((c) => c.pk && /^INTEGER$/i.test(c.type));
- cols.set(table, info.filter((c) => c.notnull && c.dflt_value == null && !c.hidden && !(pkInt && c.pk)).map((c) => c.name));
- }
- return cols.get(table);
- };
- for (const sql of statements) {
- const m = /^\s*(?:INSERT|REPLACE)(?:\s+OR\s+\w+)?\s+INTO\s+["`]?(\w+)["`]?\s*\(([^)]*)\)\s*(VALUES|SELECT)/i.exec(sql);
- if (!m) continue;
- const named = new Set(m[2].split(',').map((c) => c.trim().replace(/^["`]|["`]$/g, '').toLowerCase()));
- const missing = required(m[1]).filter((c) => !named.has(c.toLowerCase()));
- if (missing.length) problems.push({ sql, error: `${m[1]}.${missing.join(', ')} is NOT NULL without a default, and N-1 does not set it` });
- }
- return problems;
-}
-
// ── Git ──────────────────────────────────────────────────────
// ── Git ──────────────────────────────────────────────────────
/** A detached worktree of `ref` in a temp directory sharing root's node_modules → { dir, sha, remove() }. */
/** A detached worktree of `ref` in a temp directory sharing root's node_modules → { dir, sha, remove() }. */
@@ -850,6 +721,6 @@ module.exports = {
@@ -850,6 +721,6 @@ module.exports = {
flatten, readsOf, readProblems, statusCompatible,
flatten, readsOf, readProblems, statusCompatible,
send, requestsFor, notFoundProbe, isNoRoute, callProblems,
send, requestsFor, notFoundProbe, isNoRoute, callProblems,
wsSends, wsHandled, wsSession, wsMessages, wsReads, wsProblems,
wsSends, wsHandled, wsSession, wsMessages, wsReads, wsProblems,
- stringLiterals, sqlLiterals, stripSqlComments, normalizeSql, lazyDDL, readTree, schemaDDL, prepareProblems, insertProblems,
+ readTree,
worktree, gitFiles, summarize,
worktree, gitFiles, summarize,
};
};
deleted file mode 100644
deleted file mode 100644
@@ -1,23 +0,0 @@
@@ -1,23 +0,0 @@
-'use strict';
-/**
- * Loaded with `node -r` into a release booted by test/n-1/service.js (N-1 at record time, N in the
- * test), from that release's directory. With N1_SQL_OUT set, every SQL text the process prepares or
- * executes on the community database (COMMUNITY_DB_PATH, read when the statement runs) is written
- * there as JSON when it exits.
- */
-const fs = require('fs');
-const path = require('path');
-
-const out = process.env.N1_SQL_OUT;
-if (out) {
- const Database = require(path.join(process.cwd(), 'node_modules', 'better-sqlite3'));
- const seen = new Set();
- const note = (db, sql) => {
- try { if (process.env.COMMUNITY_DB_PATH && path.resolve(db.name) === path.resolve(process.env.COMMUNITY_DB_PATH)) seen.add(String(sql)); } catch { /* */ }
- };
- const prepare = Database.prototype.prepare;
- Database.prototype.prepare = function (sql, ...rest) { note(this, sql); return prepare.call(this, sql, ...rest); };
- const exec = Database.prototype.exec;
- Database.prototype.exec = function (sql, ...rest) { note(this, sql); return exec.call(this, sql, ...rest); };
- process.on('exit', () => { try { fs.writeFileSync(out, JSON.stringify([...seen])); } catch { /* */ } });
-}
@@ -1,22 +1,21 @@
@@ -1,22 +1,21 @@
'use strict';
'use strict';
/**
/**
* Community's side of the N-1 harness (test/n-1/harness.js): its clients, how a release boots and is
* Community's side of the N-1 harness (test/n-1/harness.js): its clients, how a release boots and is
- * seeded, where its SQL lives. Used by scripts/n-1-record.js (on N-1, in a temporary worktree) and by
- * test/n-1.test.js (on this checkout), so both boot and seed the same way.
+ * seeded. Used by scripts/n-1-record.js (on N-1, in a temporary worktree) and by test/n-1.test.js (on
+ * this checkout), so both boot and seed the same way.
*
*
* Community's clients are its pages: public/js/community.js, the forms and scripts the templates in
* Community's clients are its pages: public/js/community.js, the forms and scripts the templates in
* server/render write (an open tab posts the form it was served), the links, scripts and forms of the
* server/render write (an open tab posts the form it was served), the links, scripts and forms of the
* pages N-1 served (crawled), and the openvibe-sdk community client (pastes) as that release installed it.
* pages N-1 served (crawled), and the openvibe-sdk community client (pastes) as that release installed it.
*
*
* A release boots in a child process (test/n-1/boot-child.js) with its own Live, Network and Media
* A release boots in a child process (test/n-1/boot-child.js) with its own Live, Network and Media
- * mocks, the pastes authority on, and its database at COMMUNITY_DB_PATH; sign-in is the ov_token
- * cookie the Network mock signs.
+ * mocks, the pastes authority on, and a migrated PostgreSQL test database of its own; sign-in is the
+ * ov_token cookie the Network mock signs.
*/
*/
const path = require('path');
const path = require('path');
const { spawn } = require('child_process');
const { spawn } = require('child_process');
const { readTree } = require('./harness');
const { readTree } = require('./harness');
-const PRELOAD = path.join(__dirname, 'preload.js');
const CHILD = path.join(__dirname, 'boot-child.js');
const CHILD = path.join(__dirname, 'boot-child.js');
function baseEnv(extra) {
function baseEnv(extra) {
@@ -50,17 +49,14 @@ module.exports = {
@@ -50,17 +49,14 @@ module.exports = {
[/id\)*$/i, '1'],
[/id\)*$/i, '1'],
],
],
- sqlDirs: ['server'],
- ledgerTables: [],
-
/** Community migrates when it opens its database (server/db.js openDb); nothing to do first. */
/** Community migrates when it opens its database (server/db.js openDb); nothing to do first. */
seed() {},
seed() {},
- /** Boots the release in `dir` on dbPath → { url, ids, headers(auth), close() }. */
- async boot({ dir, dbPath, sqlOut = '' }) {
- const child = spawn(process.execPath, ['-r', PRELOAD, CHILD], {
+ /** Boots the release in `dir` on a migrated PostgreSQL test database → { url, ids, headers(auth), close() }. */
+ async boot({ dir }) {
+ const child = spawn(process.execPath, [CHILD], {
cwd: dir,
cwd: dir,
- env: baseEnv({ N1_DB: dbPath, N1_SQL_OUT: sqlOut }),
+ env: baseEnv({}),
stdio: ['ignore', 'pipe', 'pipe'],
stdio: ['ignore', 'pipe', 'pipe'],
});
});
let log = '';
let log = '';
@@ -4,8 +4,6 @@
@@ -4,8 +4,6 @@
// measurement; raising one is a decision to state in the commit.
// measurement; raising one is a decision to state in the commit.
// node test/perf-budget.test.js
// node test/perf-budget.test.js
const assert = require('assert');
const assert = require('assert');
-const fs = require('fs');
-const os = require('os');
const net = require('net');
const net = require('net');
const path = require('path');
const path = require('path');
const { spawn } = require('child_process');
const { spawn } = require('child_process');
@@ -26,11 +24,10 @@ const BUDGETS = {
@@ -26,11 +24,10 @@ const BUDGETS = {
const freePort = () => new Promise((resolve) => { const s = net.createServer(); s.listen(0, '127.0.0.1', () => { const { port } = s.address(); s.close(() => resolve(port)); }); });
const freePort = () => new Promise((resolve) => { const s = net.createServer(); s.listen(0, '127.0.0.1', () => { const { port } = s.address(); s.close(() => resolve(port)); }); });
(async () => {
(async () => {
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-budget-'));
const port = await freePort();
const port = await freePort();
const child = spawn(process.execPath, [path.join(__dirname, '..', 'server', 'index.js')], {
const child = spawn(process.execPath, [path.join(__dirname, '..', 'server', 'index.js')], {
cwd: path.join(__dirname, '..'),
cwd: path.join(__dirname, '..'),
- env: { ...process.env, PORT: String(port), HOST: '127.0.0.1', NODE_ENV: 'test', COMMUNITY_DB_PATH: path.join(dir, 'community.db') },
+ env: { ...process.env, PORT: String(port), HOST: '127.0.0.1', NODE_ENV: 'test' },
stdio: ['ignore', 'ignore', 'pipe'],
stdio: ['ignore', 'ignore', 'pipe'],
});
});
let stderr = '';
let stderr = '';
@@ -50,6 +47,5 @@ const freePort = () => new Promise((resolve) => { const s = net.createServer();
@@ -50,6 +47,5 @@ const freePort = () => new Promise((resolve) => { const s = net.createServer();
console.log('perf budget: all checks passed');
console.log('perf budget: all checks passed');
} finally {
} finally {
child.kill();
child.kill();
- fs.rmSync(dir, { recursive: true, force: true });
}
}
})().catch((err) => { console.error(err); process.exitCode = 1; });
})().catch((err) => { console.error(err); process.exitCode = 1; });