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 +6 -0
- package/claude-tools/detect-handles.js +46 -0
- package/claude-tools/minimal-hang-test.js +28 -0
- package/claude-tools/mongo-close-test.js +11 -0
- package/claude-tools/stdin-ref-test.js +14 -0
- package/eslint.config.js +2 -1
- package/modules/@apostrophecms/db/index.js +68 -27
- package/modules/@apostrophecms/http/index.js +1 -1
- package/modules/@apostrophecms/job/index.js +9 -7
- package/package.json +11 -11
- package/test/add-missing-schema-fields-project/test.js +11 -3
- package/test/assets.js +110 -67
- package/test/db-tools.js +365 -0
- package/test/db.js +24 -15
- package/test/default-adapter.js +256 -0
- package/test/job.js +1 -1
- package/test-lib/util.js +50 -14
- package/lib/mongodb-connect.js +0 -62
- package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +0 -131
package/CHANGELOG.md
CHANGED
|
@@ -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
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
//
|
|
5
5
|
// ### `uri`
|
|
6
6
|
//
|
|
7
|
-
// The
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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.
|
|
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/
|
|
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
|
-
"
|
|
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
|
-
"
|
|
136
|
-
"
|
|
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:
|
|
144
|
-
"test:base": "nyc mocha -t 10000
|
|
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:
|
|
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:
|
|
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:
|
|
75
|
+
{ cwd: projectCwd, ...execEnv }
|
|
68
76
|
);
|
|
69
77
|
|
|
70
78
|
apos = await t.create({
|