Skip to content
Merged
Show file tree
Hide file tree
Changes from 61 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
de730f5
postgres experimental WIP
boutell Feb 16, 2026
dccb36d
astonishingly, all mocha tests of apostrophe pass with this
boutell Feb 16, 2026
266841c
mocha tests pass, actual sites work
boutell Feb 17, 2026
9d67565
lint clean
boutell Feb 17, 2026
04d4edd
listDatabases support, but changes are coming
boutell Mar 7, 2026
bc7b72b
wip
boutell Mar 9, 2026
9ba57fc
dump and restore updates
boutell Mar 9, 2026
5ad412a
backpressure, adequate handling of ObjectId for our needs (becomes it…
boutell Mar 15, 2026
db1434b
mild performance optimization
boutell Mar 15, 2026
a083c25
profiling
boutell Mar 15, 2026
ddf80df
testing issue resolved
boutell Mar 16, 2026
3c7a23b
refactored to db-connect module, introduced sqlite adapter
boutell Mar 17, 2026
8b095f1
sqlite WIP
boutell Mar 17, 2026
80dd7ab
debugging
Mar 18, 2026
d08f502
programmatic API for dump/restore/copy dbs
Mar 18, 2026
374b5d8
linting, documentation
boutell Mar 21, 2026
6a5b931
MIT license
boutell Mar 21, 2026
fea05a7
text ranking is more accurate, documentation is more complete
boutell Mar 21, 2026
b67e2cd
good full text search for sqlite
boutell Mar 21, 2026
54dc9b5
updates for compatibility with the rest of the public and private mod…
boutell Mar 21, 2026
f0f45f9
requirements found by testing private modules
boutell Mar 21, 2026
f4ae5d8
Merge branch 'main' into postgres
boutell Mar 21, 2026
9c9105d
fixes from full cypress run
boutell Mar 25, 2026
ab875ef
eslint passing
boutell Mar 25, 2026
c95c9b3
restore permissions
boutell Mar 25, 2026
88e8e48
maximize atomicity
boutell Mar 26, 2026
2eadf8e
bug fixes
boutell Mar 26, 2026
9494c00
* exit properly when asset tests fail
boutell Mar 31, 2026
d3090df
ignore claude-tools in eslint
boutell Mar 31, 2026
5b3c675
postgres and sqlite-inclusive ci matrix attempt
boutell Mar 31, 2026
fe2cc10
clean up logs
boutell Mar 31, 2026
50897fd
We hit github's limit on total configurations because every package g…
boutell Mar 31, 2026
3c29e2a
hardened the asset tests, made them less timing sensitive, fixed a ba…
boutell Mar 31, 2026
1370762
fix a root cause of asset test instability
boutell Mar 31, 2026
4b1716e
log mess
boutell Mar 31, 2026
2fc2224
implemented missing $size operator
boutell Mar 31, 2026
c260229
test compatibility
boutell Mar 31, 2026
7408e4b
advanced permission uses regex in $in
boutell Apr 2, 2026
f0bfb0e
regex in $in
boutell Apr 2, 2026
1f1e6b3
.db() should not make false promises in plain postgres mode, it shoul…
boutell Apr 12, 2026
c198a01
ability to specify a default adapter
boutell Apr 12, 2026
14dffe3
obsolete file
boutell Apr 12, 2026
7e60e35
put escapeHost back where it belongs
boutell Apr 12, 2026
bef7978
dead code removal, test cleanup
boutell Apr 12, 2026
8cb3c5a
emulate-mongo-3-driver only needed in db-connect
boutell Apr 12, 2026
ec9ec17
no claude logs in repo (tools are welcome)
boutell Apr 12, 2026
0283d85
* shared aggregation implementation, other shared things
boutell Apr 12, 2026
3a8901d
vanilla postgres should not attempt to use .db() with alternate names…
boutell Apr 12, 2026
061136a
documentation corrections
boutell Apr 12, 2026
9c91f9f
documentation errors
boutell Apr 12, 2026
4e11aad
listDatabases and documentation corrections
boutell Apr 13, 2026
615399f
Merge branch 'main' into postgres
boutell Apr 13, 2026
f492eb4
more edge cases revealed by latest work from Miro
boutell Apr 13, 2026
c7afe8d
anchored prefix regexps are optimized
boutell Apr 14, 2026
7aa5fd1
* matchesQuery in the aggregation cursor implementation doesn't thr…
boutell Apr 14, 2026
cfdd3c6
do not swallow dump/restore errors on indexes
boutell Apr 14, 2026
a9338c4
cover how to run the utilities
boutell Apr 14, 2026
c863da6
fix detection of source
boutell Apr 14, 2026
6971100
separate sanitization for index names
boutell Apr 15, 2026
3885742
Merge branch 'main' into postgres
boutell Apr 15, 2026
8bc9d3f
regex prefix safety
boutell Apr 16, 2026
9af8358
pnpm
boutell Apr 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 64 additions & 4 deletions .github/workflows/monorepo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ env:
NODE_VERSIONS_JSON: "[20,22,24]"
MONGODB_VERSIONS_JSON: '["7","8"]'
REDIS_VERSION: "7"
POSTGRES_VERSION: "16"

on:
push:
Expand Down Expand Up @@ -124,7 +125,7 @@ jobs:
run: |
month='${{ steps.cache-month.outputs.value }}'
mongo_suffix=$(jq -r '.[]' <<< '${{ env.MONGODB_VERSIONS_JSON }}' | paste -sd'-' -)
echo "key=docker-images-${month}-mongo${mongo_suffix}-redis${{ env.REDIS_VERSION }}" >> "$GITHUB_OUTPUT"
echo "key=docker-images-${month}-mongo${mongo_suffix}-redis${{ env.REDIS_VERSION }}-pg${{ env.POSTGRES_VERSION }}" >> "$GITHUB_OUTPUT"

- name: Restore docker image cache
id: docker-cache
Expand All @@ -143,6 +144,8 @@ jobs:
done
docker pull "redis:${{ env.REDIS_VERSION }}"
docker save "redis:${{ env.REDIS_VERSION }}" -o ".github/docker-cache/redis-${{ env.REDIS_VERSION }}.tar"
docker pull "postgres:${{ env.POSTGRES_VERSION }}"
docker save "postgres:${{ env.POSTGRES_VERSION }}" -o ".github/docker-cache/postgres-${{ env.POSTGRES_VERSION }}.tar"

warm-sharp-cache:
name: Warm sharp/libvips cache (Node ${{ matrix.nodeVersion }})
Expand Down Expand Up @@ -216,7 +219,7 @@ jobs:
du -sh ~/.npm/_libvips || true

package-tests:
name: ${{ format('{0} ({1}, {2})', matrix.package, matrix.nodeVersion, matrix.needsMongo && matrix.mongodbVersion || 'n/a') }}
name: ${{ format('{0} ({1}, {2}{3})', matrix.group, matrix.nodeVersion, matrix.adapter || 'n/a', matrix.needsMongo && format(' mongo {0}', matrix.mongodbVersion) || '') }}
needs:
- setup
- shared-runtime
Expand Down Expand Up @@ -279,7 +282,7 @@ jobs:
- name: Restore docker image cache
id: docker-cache-restore
uses: actions/cache/restore@v4
if: matrix.needsMongo || matrix.needsRedis
if: matrix.needsMongo || matrix.needsRedis || matrix.needsPostgres
with:
path: .github/docker-cache
key: ${{ needs.shared-runtime.outputs.docker-cache-key }}
Expand All @@ -300,6 +303,14 @@ jobs:
if: steps.docker-cache-restore.outputs.cache-hit != 'true' && matrix.needsRedis
run: docker pull redis:${{ env.REDIS_VERSION }}

- name: Load cached Postgres image
if: steps.docker-cache-restore.outputs.cache-hit == 'true' && matrix.needsPostgres
run: docker load -i ".github/docker-cache/postgres-${{ env.POSTGRES_VERSION }}.tar"

- name: Pull Postgres image (cache miss)
if: steps.docker-cache-restore.outputs.cache-hit != 'true' && matrix.needsPostgres
run: docker pull postgres:${{ env.POSTGRES_VERSION }}

- name: Start MongoDB
if: matrix.needsMongo
run: |
Expand Down Expand Up @@ -358,15 +369,64 @@ jobs:
docker logs redis
exit 1

- name: Start PostgreSQL
if: matrix.needsPostgres
run: |
docker rm -f postgres >/dev/null 2>&1 || true
docker run -d \
--name postgres \
--publish 5432:5432 \
-e POSTGRES_HOST_AUTH_METHOD=trust \
--health-cmd "pg_isready -U postgres" \
--health-interval 5s \
--health-timeout 5s \
--health-retries 12 \
postgres:${{ env.POSTGRES_VERSION }}
echo "Waiting for PostgreSQL to report healthy..."
for attempt in $(seq 1 60); do
status=$(docker inspect --format='{{.State.Health.Status}}' postgres 2>/dev/null || echo "starting")
if [ "$status" = "healthy" ]; then
exit 0
fi
if [ "$status" = "unhealthy" ]; then
echo "PostgreSQL reported unhealthy" >&2
docker logs postgres
exit 1
fi
sleep 2
done
echo "PostgreSQL failed to become healthy in time" >&2
docker logs postgres
exit 1

- name: Install workspace dependencies
run: pnpm install --frozen-lockfile

- name: Run package tests
run: pnpm run --filter "${{ matrix.package }}" --if-present test
run: |
failed=0
for pkg in $(echo '${{ matrix.packages }}' | jq -r '.[]'); do
echo "::group::Testing $pkg"
if ! pnpm run --filter "$pkg" --if-present test; then
echo "::error::Tests failed for $pkg"
failed=1
fi
echo "::endgroup::"
done
exit $failed
env:
CI: true
# Yes we want import-export to test with automatic-translation
TEST_WITH_PRO: "1"
# Adapter selection: mongodb (default), postgres, or sqlite.
# ADAPTER is used by db-connect tests, APOS_TEST_DB_PROTOCOL by apostrophe tests.
ADAPTER: ${{ matrix.adapter }}
APOS_TEST_DB_PROTOCOL: ${{ matrix.adapter }}
PGUSER: postgres

- name: Stop PostgreSQL
if: always() && matrix.needsPostgres
run: docker rm -f postgres || true

- name: Stop Redis
if: always() && matrix.needsRedis
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/scripts/detect-impacted-packages.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ async function main() {
package: name,
directory: packages.get(name).relativeDir,
requiresMongo: packages.get(name).requiresMongo !== false,
requiresRedis: packages.get(name).requiresRedis === true
requiresRedis: packages.get(name).requiresRedis === true,
mongodbOnly: packages.get(name).mongodbOnly === true
}))
};

Expand Down Expand Up @@ -130,14 +131,16 @@ async function loadPackages() {
const testConfig = manifest.apostropheTestConfig || {};
const requiresMongo = testConfig.requiresMongo !== false;
const requiresRedis = testConfig.requiresRedis === true;
const mongodbOnly = testConfig.mongodbOnly === true;

map.set(manifest.name, {
name: manifest.name,
relativeDir: path.posix.join('packages', entry.name),
dependencies,
hasTestScript,
requiresMongo,
requiresRedis
requiresRedis,
mongodbOnly
});
}));

Expand Down
126 changes: 118 additions & 8 deletions .github/workflows/scripts/expand-runtime-matrix.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
#!/usr/bin/env node
// Expands the impacted package matrix with runtime permutations supplied
// Expands the impacted package matrix with runtime permutations supplied
// via env vars.
//
// Packages are grouped into jobs to stay within GitHub's 256-entry matrix limit:
// - "apostrophe" runs solo (the main package, benefits from its own status).
// - All other database packages are grouped into an "ecosystem" job.
// - mongodbOnly packages are grouped into "ecosystem-mongodb".
// - Non-database packages are grouped into "standalone".
//
// For groups that need a database, three adapter variants are emitted:
// 1. mongodb – all Node versions × all MongoDB versions
// 2. postgres – latest LTS Node only, no MongoDB
// 3. sqlite – latest LTS Node only, no MongoDB
//
// The latest LTS Node version is the highest even-numbered entry in
// NODE_VERSIONS_JSON.

import { readFile } from 'fs/promises';

const args = process.argv.slice(2);
Expand Down Expand Up @@ -36,25 +51,120 @@ function parseJsonArray(name, raw) {
const nodeVersions = parseJsonArray('NODE_VERSIONS_JSON', process.env.NODE_VERSIONS_JSON);
const mongodbVersions = parseJsonArray('MONGODB_VERSIONS_JSON', process.env.MONGODB_VERSIONS_JSON);

// Latest LTS = highest even-numbered Node version
const latestLts = [...nodeVersions]
.filter((v) => Number(v) % 2 === 0)
.sort((a, b) => Number(b) - Number(a))[0];

const impact = JSON.parse(await readFile(impactPath, 'utf8'));
const packages = impact?.matrix?.include || [];
const include = [];

// The main apostrophe package always gets its own jobs for clear CI status.
const SOLO_PACKAGES = new Set(['apostrophe']);

// Sort packages into groups
const solo = [];
const ecosystem = [];
const ecosystemMongodbOnly = [];
const standalone = [];

for (const pkg of packages) {
const needsMongo = pkg.requiresMongo !== false;
const needsRedis = pkg.requiresRedis === true;
const mongoTargets = needsMongo ? mongodbVersions : [''];
const needsDb = pkg.requiresMongo !== false;
if (SOLO_PACKAGES.has(pkg.package)) {
solo.push(pkg);
} else if (needsDb && pkg.mongodbOnly) {
ecosystemMongodbOnly.push(pkg);
} else if (needsDb) {
ecosystem.push(pkg);
} else {
standalone.push(pkg);
}
}

const include = [];

// Emit runtime combinations for a group of packages.
function emitGroup(group, pkgs) {
if (!pkgs.length) {
return;
}
const packageNames = JSON.stringify(pkgs.map((p) => p.package));
const needsRedis = pkgs.some((p) => p.requiresRedis === true);
const mongodbOnly = pkgs.every((p) => p.mongodbOnly);

// mongodb: all Node versions × all MongoDB versions
for (const nodeVersion of nodeVersions) {
for (const mongodbVersion of mongoTargets) {
for (const mongodbVersion of mongodbVersions) {
include.push({
...pkg,
group,
packages: packageNames,
nodeVersion,
mongodbVersion,
needsMongo,
adapter: 'mongodb',
needsMongo: true,
needsPostgres: false,
needsRedis
});
}
}
// postgres and sqlite: latest LTS only, skip for mongodb-only groups
if (!mongodbOnly) {
include.push({
group,
packages: packageNames,
nodeVersion: latestLts,
mongodbVersion: '',
adapter: 'postgres',
needsMongo: false,
needsPostgres: true,
needsRedis
});
include.push({
group,
packages: packageNames,
nodeVersion: latestLts,
mongodbVersion: '',
adapter: 'sqlite',
needsMongo: false,
needsPostgres: false,
needsRedis
});
}
}

// Emit non-database group (no adapter variants, just Node versions)
function emitStandalone(group, pkgs) {
if (!pkgs.length) {
return;
}
const packageNames = JSON.stringify(pkgs.map((p) => p.package));
for (const nodeVersion of nodeVersions) {
include.push({
group,
packages: packageNames,
nodeVersion,
mongodbVersion: '',
adapter: '',
needsMongo: false,
needsPostgres: false,
needsRedis: false
});
}
}

// Solo packages each get their own group
for (const pkg of solo) {
emitGroup(pkg.package, [pkg]);
}

if (ecosystem.length) {
emitGroup('ecosystem', ecosystem);
}
if (ecosystemMongodbOnly.length) {
emitGroup('ecosystem-mongodb', ecosystemMongodbOnly);
}
if (standalone.length) {
emitStandalone('standalone', standalone);
}

process.stdout.write(JSON.stringify({ include }));
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ public/apos-frontend
.DS_Store
coverage/
.nyc_output
claude-tools/logs/
.claude
37 changes: 37 additions & 0 deletions claude-tools/run-core-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/bash
# Run the apostrophe core test suite against a chosen DB adapter and log
# output to claude-tools/logs/core-<adapter>.log. Usage:
#
# ./claude-tools/run-core-tests.sh mongodb
# ./claude-tools/run-core-tests.sh postgres
# ./claude-tools/run-core-tests.sh sqlite
#
# NEVER run multiple adapters in parallel — the test suite is not designed
# for concurrent runs and the host has limited resources.

set -u
adapter="${1:-}"
if [[ -z "$adapter" ]]; then
echo "usage: $0 <mongodb|postgres|sqlite>" >&2
exit 2
fi

root="$(cd "$(dirname "$0")/.." && pwd)"
logdir="$root/claude-tools/logs"
mkdir -p "$logdir"
log="$logdir/core-$adapter.log"
: > "$log"

echo "=== $adapter core tests ($(date -Is)) ===" | tee -a "$log"

cd "$root/packages/apostrophe"

extra=()
if [[ "$adapter" == "postgres" ]]; then
extra=(env PGPASSWORD=testpassword)
fi

APOS_TEST_DB_PROTOCOL="$adapter" "${extra[@]}" npm run test:base >> "$log" 2>&1
code=$?
echo "=== exit=$code ===" | tee -a "$log"
exit "$code"
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"dev": "pnpm --parallel --recursive run dev",
"build": "pnpm --recursive run build",
"lint": "pnpm --recursive run lint",
"test": "pnpm --recursive run test",
"test": "APOS_TEST_DB_PROTOCOL=postgres npm run test:main && APOS_TEST_DB_PROTOCOL=mongodb npm run test:main && APOS_TEST_DB_PROTOCOL=sqlite npm run test:main && APOS_TEST_DB_PROTOCOL=multipostgres npm run test:main",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be probably pnpm instead npm.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed that.

"test:main": "echo \"APOS_TEST_DB_PROTOCOL IS: $APOS_TEST_DB_PROTOCOL\" && pnpm --recursive run test",
"eslint": "pnpm --recursive run eslint",
"mocha": "pnpm --recursive run mocha",
"clean": "pnpm -r exec rm -rf node_modules && rm -rf node_modules && rm pnpm-lock.yaml"
Expand All @@ -18,6 +19,7 @@
},
"pnpm": {
"onlyBuiltDependencies": [
"better-sqlite3",
"sharp",
"vue-demi",
"@parcel/watcher",
Expand Down
3 changes: 3 additions & 0 deletions packages/apostrophe/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ test/public/uploads

# vim swp files
.*.sw*

# claude-tools log files
claude-tools/**/*.log
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claude-tools is meant to persist, but its logs are not

Loading
Loading