apostrophe 4.29.0 → 4.30.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.30.0-alpha.1
4
+
5
+ ### Adds
6
+
7
+ - Postgres and SQLite alpha release
8
+
3
9
  ## 4.29.0 (2026-04-15)
4
10
 
5
11
  ### Adds
@@ -0,0 +1,46 @@
1
+ // Require this before running mocha to detect what activates process.stdin
2
+ // Usage: npx mocha -t 10000 --require ./claude-tools/detect-handles.js test/assets.js
3
+
4
+ console.log(`stdin paused at startup: ${process.stdin.isPaused()}`);
5
+ console.log(`stdin readableFlowing at startup: ${process.stdin.readableFlowing}`);
6
+
7
+ // Monkey-patch stdin.resume to capture the call stack
8
+ const origResume = process.stdin.resume.bind(process.stdin);
9
+ process.stdin.resume = function(...args) {
10
+ console.log('\n=== process.stdin.resume() called ===');
11
+ console.log(new Error().stack);
12
+ return origResume(...args);
13
+ };
14
+
15
+ // Monkey-patch stdin.on to detect 'data' listener additions
16
+ const origOn = process.stdin.on.bind(process.stdin);
17
+ process.stdin.on = function(event, ...args) {
18
+ if (event === 'data' || event === 'readable') {
19
+ console.log(`\n=== process.stdin.on('${event}') called ===`);
20
+ console.log(new Error().stack);
21
+ }
22
+ return origOn(event, ...args);
23
+ };
24
+
25
+ // Periodically check stdin state changes
26
+ let lastState = process.stdin.readableFlowing;
27
+ const checker = setInterval(() => {
28
+ if (process.stdin.readableFlowing !== lastState) {
29
+ console.log(`\n=== stdin readableFlowing changed: ${lastState} -> ${process.stdin.readableFlowing} ===`);
30
+ console.log(new Error().stack);
31
+ lastState = process.stdin.readableFlowing;
32
+ }
33
+ }, 100);
34
+ checker.unref();
35
+
36
+ const origRun = require('mocha/lib/runner').prototype.run;
37
+ require('mocha/lib/runner').prototype.run = function(fn) {
38
+ return origRun.call(this, function(failures) {
39
+ console.log(`\nstdin paused at end: ${process.stdin.isPaused()}`);
40
+ console.log(`stdin readableFlowing at end: ${process.stdin.readableFlowing}`);
41
+ setTimeout(() => {
42
+ process.exit(failures ? 3 : 0);
43
+ }, 2000);
44
+ if (fn) fn(failures);
45
+ });
46
+ };
@@ -0,0 +1,28 @@
1
+ // Minimal test to isolate what causes the hang.
2
+ // Must reference the test/ directory as root for proper module resolution.
3
+ const t = require('../test-lib/test.js');
4
+ const path = require('path');
5
+
6
+ // Fake a module object rooted in test/ like the real tests do
7
+ const fakeModule = {
8
+ id: path.join(__dirname, '../test/fake'),
9
+ filename: path.join(__dirname, '../test/fake.js'),
10
+ paths: [path.join(__dirname, '../test/node_modules')]
11
+ };
12
+
13
+ describe('Minimal hang test', function() {
14
+ this.timeout(60000);
15
+ let apos;
16
+
17
+ after(async function() {
18
+ await t.destroy(apos);
19
+ console.log('after: destroy complete');
20
+ });
21
+
22
+ it('should create and use apos without hanging', async function() {
23
+ apos = await t.create({
24
+ root: fakeModule
25
+ });
26
+ console.log('apos created successfully');
27
+ });
28
+ });
@@ -0,0 +1,11 @@
1
+ // Test whether a MongoDB connection keeps the process alive after close()
2
+ const mongoConnect = require('../../../packages/db-connect/lib/mongodb-connect');
3
+
4
+ (async () => {
5
+ const uri = 'mongodb://localhost:27017/test_handle_leak';
6
+ console.log('Connecting...');
7
+ const client = await mongoConnect(uri);
8
+ console.log('Connected. Closing...');
9
+ await client.close();
10
+ console.log('Closed. Process should exit now if no leaked handles.');
11
+ })();
@@ -0,0 +1,14 @@
1
+ // Check if process.stdin keeps the process alive
2
+ // If this script hangs, stdin is ref'd. If it exits, stdin is unref'd.
3
+
4
+ console.log(`stdin isTTY: ${process.stdin.isTTY}`);
5
+ console.log(`stdin readableFlowing: ${process.stdin.readableFlowing}`);
6
+ console.log(`stdin isPaused: ${process.stdin.isPaused()}`);
7
+
8
+ // Check ref status
9
+ if (typeof process.stdin.unref === 'function') {
10
+ console.log('stdin has unref method');
11
+ }
12
+
13
+ console.log('Waiting to see if process exits on its own...');
14
+ // Don't do anything - just see if the process exits
package/eslint.config.js CHANGED
@@ -7,7 +7,8 @@ module.exports = defineConfig([
7
7
  '**/blueimp/**/*.js',
8
8
  'test/public',
9
9
  'test/apos-build',
10
- 'coverage'
10
+ 'coverage',
11
+ 'claude-tools'
11
12
  ]),
12
13
  apostrophe
13
14
  ]);
@@ -4,11 +4,12 @@
4
4
  //
5
5
  // ### `uri`
6
6
  //
7
- // The MongoDB connection URI. See the [MongoDB URI documentation](https://docs.mongodb.com/manual/reference/connection-string/).
7
+ // The databse connection URI. See the [MongoDB URI documentation](https://docs.mongodb.com/manual/reference/connection-string/)
8
+ // and the postgres documentation.
8
9
  //
9
10
  // ### `connect`
10
11
  //
11
- // If present, this object is passed on as options to MongoDB's "connect"
12
+ // If present, this object is passed on as options to the database adapters "connect"
12
13
  // method, along with the uri. See the [MongoDB connect settings documentation](http://mongodb.github.io/node-mongodb-native/2.2/reference/connecting/connection-settings/).
13
14
  //
14
15
  // By default, Apostrophe sets options to retry lost connections forever,
@@ -20,9 +21,16 @@
20
21
  //
21
22
  // ### `client`
22
23
  //
23
- // An existing MongoDB connection (MongoClient) object. If present, it is used
24
+ // An existing MongoDB-compatible client object. If present, it is used
24
25
  // and `uri`, `host`, `connect`, etc. are ignored.
25
26
  //
27
+ // ### `adapters`
28
+ //
29
+ // An array of adapters, each of which must provide `name`, `connect(uri, options)`,
30
+ // and `protocols` properties. `name` may be used to override a core adapter,
31
+ // such as `postgres` or `mongodb`. `connect` must resolve to a client object
32
+ // supporting a sufficient subset of the mongodb API.
33
+ //
26
34
  // ### `versionCheck`
27
35
  //
28
36
  // If `true`, check to make sure the database does not belong to an
@@ -49,15 +57,15 @@
49
57
  // in your project. However you may find it easier to just use the
50
58
  // `client` option.
51
59
 
52
- const mongodbConnect = require('../../../lib/mongodb-connect');
53
- const escapeHost = require('../../../lib/escape-host');
60
+ const dbConnect = require('@apostrophecms/db-connect');
61
+ const escapeHost = require('../../../lib/escape-host.js');
54
62
 
55
63
  module.exports = {
56
64
  options: {
57
65
  versionCheck: true
58
66
  },
59
67
  async init(self) {
60
- await self.connectToMongo();
68
+ await self.connectToDb();
61
69
  await self.versionCheck();
62
70
  },
63
71
  handlers(self) {
@@ -81,14 +89,12 @@ module.exports = {
81
89
  },
82
90
  methods(self) {
83
91
  return {
84
- // Open the database connection. Always uses MongoClient with its
85
- // sensible defaults. Builds a URI if necessary, so we can call it
86
- // in a consistent way.
87
- //
88
- // One default we override: if the connection is lost, we keep
89
- // attempting to reconnect forever. This is the most sensible behavior
90
- // for a persistent process that requires MongoDB in order to operate.
91
- async connectToMongo() {
92
+ // Connect to the database and sets self.apos.dbClient
93
+ // and self.apos.db. Builds a mongodb URI by default,
94
+ // accepting host, port, user, password and name options
95
+ // if present. More typically a URI is specified via
96
+ // APOS_DB_URI, or via APOS_MONGODB_URI for bc.
97
+ async connectToDb() {
92
98
  if (self.options.client) {
93
99
  // Reuse a single client connection http://mongodb.github.io/node-mongodb-native/2.2/api/Db.html#db
94
100
  self.apos.dbClient = self.options.client;
@@ -96,32 +102,67 @@ module.exports = {
96
102
  self.connectionReused = true;
97
103
  return;
98
104
  }
99
- let uri = 'mongodb://';
100
- if (process.env.APOS_MONGODB_URI) {
101
- uri = process.env.APOS_MONGODB_URI;
105
+ let uri;
106
+ const viaEnv = process.env.APOS_DB_URI || process.env.APOS_MONGODB_URI;
107
+ if (viaEnv) {
108
+ uri = viaEnv;
102
109
  } else if (self.options.uri) {
103
110
  uri = self.options.uri;
104
111
  } else {
105
- if (self.options.user) {
106
- uri += self.options.user + ':' + self.options.password + '@';
107
- }
108
- if (!self.options.host) {
109
- self.options.host = 'localhost';
110
- }
111
- if (!self.options.port) {
112
- self.options.port = 27017;
112
+ const validAdapters = [ 'mongodb', 'sqlite', 'postgres', 'multipostgres' ];
113
+ const adapter = process.env.APOS_DEFAULT_DB_ADAPTER || self.options.defaultAdapter || 'mongodb';
114
+ if (!validAdapters.includes(adapter)) {
115
+ throw new Error(`Invalid defaultAdapter: "${adapter}". Must be one of: ${validAdapters.join(', ')}`);
113
116
  }
114
117
  if (!self.options.name) {
115
118
  self.options.name = self.apos.shortName;
116
119
  }
117
- uri += escapeHost(self.options.host) + ':' + self.options.port + '/' + self.options.name;
120
+ if (adapter === 'sqlite') {
121
+ const path = require('path');
122
+ uri = `sqlite://${path.resolve(self.apos.rootDir, 'data', self.options.name + '.sqlite')}`;
123
+ } else {
124
+ const credentials = self.options.user
125
+ ? encodeURIComponent(self.options.user) + ':' + encodeURIComponent(self.options.password) + '@'
126
+ : '';
127
+ if (adapter === 'mongodb') {
128
+ if (!self.options.host) {
129
+ self.options.host = 'localhost';
130
+ }
131
+ if (!self.options.port) {
132
+ self.options.port = 27017;
133
+ }
134
+ uri = 'mongodb://' + credentials + escapeHost(self.options.host) + ':' + self.options.port + '/' + self.options.name;
135
+ } else {
136
+ // postgres or multipostgres
137
+ if (!self.options.host) {
138
+ self.options.host = 'localhost';
139
+ }
140
+ if (!self.options.port) {
141
+ self.options.port = 5432;
142
+ }
143
+ uri = adapter + '://' + credentials + escapeHost(self.options.host) + ':' + self.options.port + '/' + self.options.name;
144
+ }
145
+ }
118
146
  }
119
147
 
120
- self.apos.dbClient = await mongodbConnect(uri, self.options.connect);
148
+ self.apos.dbClient = await dbConnect(uri, {
149
+ ...self.options.connect,
150
+ adapters: self.options.adapters
151
+ });
121
152
  self.uri = uri;
122
153
  // Automatically uses the db name in the connection string
123
154
  self.apos.db = self.apos.dbClient.db();
124
155
  },
156
+ // Connect to a database using the appropriate adapter based on the URI protocol.
157
+ // Returns a client object compatible with the MongoDB driver interface.
158
+ // This method has no side effects — it does not set apos.db or apos.dbClient.
159
+ // It can be used to make temporary connections, e.g. for dropping a test database.
160
+ async connectToAdapter(uri, options) {
161
+ return dbConnect(uri, {
162
+ ...options,
163
+ adapters: self.options.adapters
164
+ });
165
+ },
125
166
  async versionCheck() {
126
167
  if (!self.options.versionCheck) {
127
168
  return;
@@ -2,7 +2,7 @@ const _ = require('lodash');
2
2
  const qs = require('qs');
3
3
  const fetch = require('node-fetch');
4
4
  const tough = require('tough-cookie');
5
- const escapeHost = require('../../../lib/escape-host');
5
+ const escapeHost = require('../../../lib/escape-host.js');
6
6
  const util = require('util');
7
7
 
8
8
  module.exports = {
@@ -244,7 +244,9 @@ module.exports = {
244
244
  },
245
245
  setTotal (n) {
246
246
  total = n;
247
- return self.setTotal(job, n);
247
+ const result = self.setTotal(job, n);
248
+ promises.push(result);
249
+ return result;
248
250
  },
249
251
  setResults (_results) {
250
252
  results = _results;
@@ -412,12 +414,12 @@ module.exports = {
412
414
  //
413
415
  // No promise is returned as this method just updates
414
416
  // the job tracking information in the background.
415
- setTotal(job, total) {
416
- self.db.updateOne({ _id: job._id }, { $set: { total } }, function (err) {
417
- if (err) {
418
- self.apos.util.error(err);
419
- }
420
- });
417
+ async setTotal(job, total) {
418
+ try {
419
+ await self.db.updateOne({ _id: job._id }, { $set: { total } });
420
+ } catch (err) {
421
+ self.apos.util.error(err);
422
+ }
421
423
  },
422
424
  // Mark the given job as ended. If `success`
423
425
  // is true the job is reported as an overall
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "4.29.0",
3
+ "version": "4.30.0-alpha.1",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -117,33 +117,33 @@
117
117
  "webpack": "^5.106.1",
118
118
  "webpack-merge": "^5.7.3",
119
119
  "xregexp": "^2.0.0",
120
- "@apostrophecms/emulate-mongo-3-driver": "^1.0.6",
121
- "express-cache-on-demand": "^1.0.4",
120
+ "@apostrophecms/db-connect": "^1.0.0-alpha.1",
122
121
  "broadband": "^1.1.0",
123
- "boring": "^1.1.1",
124
- "oembetter": "^1.1.4",
125
122
  "postcss-viewport-to-container-toggle": "^2.3.0",
123
+ "sanitize-html": "^2.17.3",
124
+ "oembetter": "^1.1.4",
126
125
  "uploadfs": "^1.26.1",
127
- "sanitize-html": "^2.17.3"
126
+ "express-cache-on-demand": "^1.0.4",
127
+ "boring": "^1.1.1"
128
128
  },
129
129
  "devDependencies": {
130
+ "chai": "^4.3.10",
130
131
  "eslint": "^9.39.1",
131
132
  "form-data": "^4.0.4",
132
133
  "mocha": "^11.7.5",
133
134
  "nyc": "^17.1.0",
134
135
  "stylelint": "^16.5.0",
135
- "stylelint-config-apostrophe": "^4.4.0",
136
- "eslint-config-apostrophe": "^6.0.2"
136
+ "eslint-config-apostrophe": "^6.0.2",
137
+ "stylelint-config-apostrophe": "^4.4.0"
137
138
  },
138
139
  "browserslist": [
139
140
  "ie >= 10"
140
141
  ],
141
142
  "scripts": {
142
143
  "pretest": "npm run lint",
143
- "test": "npm run test:base && npm run test:missing && npm run test:assets && npm run test:esm",
144
- "test:base": "nyc mocha -t 10000 --ignore=test/assets.js",
144
+ "test": "npm run test:base && npm run test:missing && npm run test:esm",
145
+ "test:base": "nyc mocha -t 10000",
145
146
  "test:missing": "nyc mocha -t 10000 test/add-missing-schema-fields-project/test.js",
146
- "test:assets": "nyc mocha -t 10000 test/assets.js",
147
147
  "test:esm": "mocha -t 1000 test/esm-project/esm.js",
148
148
  "eslint": "eslint .",
149
149
  "eslint-fix": "npm run eslint -- --fix",
@@ -11,10 +11,18 @@ describe('Apostrophe - add-missing-schema-fields task', function() {
11
11
 
12
12
  let apos;
13
13
 
14
+ // When APOS_TEST_DB_PROTOCOL is set, child processes that run `node app.js`
15
+ // need the matching APOS_DB_URI so they use the same database as t.create()
16
+ const projectCwd = path.resolve(process.cwd(), 'test/add-missing-schema-fields-project/');
17
+ const testDbUri = t.getTestDbUri('add-missing-schema-fields-project');
18
+ const execEnv = testDbUri
19
+ ? { env: { ...process.env, APOS_DB_URI: testDbUri } }
20
+ : {};
21
+
14
22
  before(async function() {
15
23
  await util.promisify(exec)(
16
24
  'npm install',
17
- { cwd: path.resolve(process.cwd(), 'test/add-missing-schema-fields-project/') }
25
+ { cwd: projectCwd }
18
26
  );
19
27
  });
20
28
 
@@ -25,7 +33,7 @@ describe('Apostrophe - add-missing-schema-fields task', function() {
25
33
  it('should not run migrations when running the task', async function() {
26
34
  await util.promisify(exec)(
27
35
  'node app.js @apostrophecms/migration:add-missing-schema-fields',
28
- { cwd: path.resolve(process.cwd(), 'test/add-missing-schema-fields-project/') }
36
+ { cwd: projectCwd, ...execEnv }
29
37
  );
30
38
 
31
39
  apos = await t.create({
@@ -64,7 +72,7 @@ describe('Apostrophe - add-missing-schema-fields task', function() {
64
72
  it('should run migrations when running @apostrophecms/migration:migrate task', async function() {
65
73
  await util.promisify(exec)(
66
74
  'node app.js @apostrophecms/migration:migrate',
67
- { cwd: path.resolve(process.cwd(), 'test/add-missing-schema-fields-project/') }
75
+ { cwd: projectCwd, ...execEnv }
68
76
  );
69
77
 
70
78
  apos = await t.create({