apostrophe 4.30.0-alpha.1 → 4.30.0
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/.claude/settings.local.json +15 -0
- package/CHANGELOG.md +30 -2
- package/eslint.config.js +1 -2
- package/lib/mongodb-connect.js +62 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBar.vue +0 -1
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarMenu.vue +25 -8
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBreakpointPreviewMode.vue +9 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +20 -2
- package/modules/@apostrophecms/area/index.js +10 -5
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +2 -0
- package/modules/@apostrophecms/command-menu/ui/apos/components/TheAposCommandMenu.vue +11 -1
- package/modules/@apostrophecms/db/index.js +27 -68
- package/modules/@apostrophecms/http/index.js +1 -1
- package/modules/@apostrophecms/i18n/i18n/en.json +8 -0
- package/modules/@apostrophecms/i18n/index.js +1 -8
- package/modules/@apostrophecms/image-widget/index.js +29 -1
- package/modules/@apostrophecms/job/index.js +7 -9
- package/modules/@apostrophecms/layout-widget/index.js +124 -2
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +89 -6
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +2 -2
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridManager.vue +2 -2
- package/modules/@apostrophecms/layout-widget/ui/apos/layout.css +8 -0
- package/modules/@apostrophecms/layout-widget/ui/src/layout.css +1 -1
- package/modules/@apostrophecms/layout-widget/views/widget.html +3 -3
- package/modules/@apostrophecms/login/index.js +13 -15
- package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +2 -1
- package/modules/@apostrophecms/oembed/index.js +18 -13
- package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedIcon.vue +2 -0
- package/modules/@apostrophecms/rich-text-widget/index.js +36 -0
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputCheckboxes.js +1 -1
- package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRadio.js +1 -1
- package/modules/@apostrophecms/styles/index.js +16 -0
- package/modules/@apostrophecms/styles/lib/handlers.js +6 -0
- package/modules/@apostrophecms/styles/lib/methods.js +93 -0
- package/modules/@apostrophecms/styles/lib/presets.js +17 -0
- package/modules/@apostrophecms/styles/ui/apos/universal/render.mjs +10 -1
- package/modules/@apostrophecms/submitted-draft/ui/apos/components/AposSubmittedDraftIcon.vue +1 -0
- package/modules/@apostrophecms/template/views/outerLayoutBase.html +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +14 -2
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +29 -5
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +8 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuItem.vue +5 -2
- package/modules/@apostrophecms/ui/ui/apos/components/AposLocalePicker.vue +14 -1
- package/modules/@apostrophecms/ui/ui/apos/scss/global/_utilities.scss +13 -1
- package/modules/@apostrophecms/util/index.js +4 -0
- package/modules/@apostrophecms/widget-type/index.js +6 -0
- package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +32 -0
- package/package.json +13 -13
- package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +131 -0
- package/test/add-missing-schema-fields-project/test.js +3 -11
- package/test/assets.js +67 -110
- package/test/db.js +15 -24
- package/test/job.js +1 -1
- package/test/layout-widget-gap.js +530 -0
- package/test/login.js +122 -1
- package/test/rich-text-widget.js +200 -0
- package/test/styles.js +50 -0
- package/test-lib/util.js +14 -50
- package/claude-tools/detect-handles.js +0 -46
- package/claude-tools/minimal-hang-test.js +0 -28
- package/claude-tools/mongo-close-test.js +0 -11
- package/claude-tools/stdin-ref-test.js +0 -14
- package/test/db-tools.js +0 -365
- package/test/default-adapter.js +0 -256
package/test/db-tools.js
DELETED
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
const assert = require('assert');
|
|
2
|
-
const { execFile } = require('child_process');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const os = require('os');
|
|
6
|
-
const dbConnect = require('@apostrophecms/db-connect');
|
|
7
|
-
|
|
8
|
-
const dumpBin = require.resolve('@apostrophecms/db-connect/bin/apos-db-dump.js');
|
|
9
|
-
const restoreBin = require.resolve('@apostrophecms/db-connect/bin/apos-db-restore.js');
|
|
10
|
-
|
|
11
|
-
const testDbProtocol = process.env.APOS_TEST_DB_PROTOCOL || 'mongodb';
|
|
12
|
-
|
|
13
|
-
function testUri(dbName) {
|
|
14
|
-
const dbSafe = dbName.replace(/-/g, '_').replace(/[^a-zA-Z0-9_]/g, '');
|
|
15
|
-
if (testDbProtocol === 'sqlite') {
|
|
16
|
-
return `sqlite://${path.join(os.tmpdir(), `${dbSafe}.db`)}`;
|
|
17
|
-
}
|
|
18
|
-
if (testDbProtocol === 'postgres') {
|
|
19
|
-
return `postgres://localhost:5432/${dbSafe}`;
|
|
20
|
-
}
|
|
21
|
-
const baseUri = process.env.DB_URI || 'mongodb://localhost:27017';
|
|
22
|
-
return `${baseUri}/${dbName}`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function run(bin, args) {
|
|
26
|
-
return new Promise((resolve, reject) => {
|
|
27
|
-
execFile(process.execPath, [ bin, ...args ], {
|
|
28
|
-
timeout: 30000,
|
|
29
|
-
maxBuffer: 50 * 1024 * 1024
|
|
30
|
-
}, (err, stdout, stderr) => {
|
|
31
|
-
if (err) {
|
|
32
|
-
err.stdout = stdout;
|
|
33
|
-
err.stderr = stderr;
|
|
34
|
-
return reject(err);
|
|
35
|
-
}
|
|
36
|
-
resolve({
|
|
37
|
-
stdout,
|
|
38
|
-
stderr
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function dropAll(uri) {
|
|
45
|
-
let client;
|
|
46
|
-
try {
|
|
47
|
-
client = await dbConnect(uri);
|
|
48
|
-
} catch (e) {
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
const db = client.db();
|
|
52
|
-
const collections = await db.listCollections().toArray();
|
|
53
|
-
for (const col of collections) {
|
|
54
|
-
await db.collection(col.name).drop();
|
|
55
|
-
}
|
|
56
|
-
await client.close();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
describe('apos-db-dump and apos-db-restore', function () {
|
|
60
|
-
this.timeout(30000);
|
|
61
|
-
|
|
62
|
-
const sourceUri = testUri('dbtest_dump_source');
|
|
63
|
-
const targetUri = testUri('dbtest_dump_target');
|
|
64
|
-
let tmpFile;
|
|
65
|
-
|
|
66
|
-
before(async function () {
|
|
67
|
-
tmpFile = path.join(os.tmpdir(), `apos-db-test-${process.pid}.ndjson`);
|
|
68
|
-
await dropAll(sourceUri);
|
|
69
|
-
await dropAll(targetUri);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
after(async function () {
|
|
73
|
-
await dropAll(sourceUri);
|
|
74
|
-
await dropAll(targetUri);
|
|
75
|
-
try {
|
|
76
|
-
fs.unlinkSync(tmpFile);
|
|
77
|
-
} catch (e) {
|
|
78
|
-
// ignore
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('should dump an empty database without error', async function () {
|
|
83
|
-
const { stdout } = await run(dumpBin, [ sourceUri ]);
|
|
84
|
-
assert.strictEqual(stdout.trim(), '');
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('should dump and restore documents', async function () {
|
|
88
|
-
// Insert test data
|
|
89
|
-
const client = await dbConnect(sourceUri);
|
|
90
|
-
const db = client.db();
|
|
91
|
-
await db.collection('aposDocs').insertMany([
|
|
92
|
-
{
|
|
93
|
-
_id: 'doc1',
|
|
94
|
-
title: 'Hello',
|
|
95
|
-
tags: [ 'a', 'b' ]
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
_id: 'doc2',
|
|
99
|
-
title: 'World'
|
|
100
|
-
}
|
|
101
|
-
]);
|
|
102
|
-
await db.collection('aposCache').insertMany([
|
|
103
|
-
{
|
|
104
|
-
_id: 'cache1',
|
|
105
|
-
value: 42
|
|
106
|
-
}
|
|
107
|
-
]);
|
|
108
|
-
await client.close();
|
|
109
|
-
|
|
110
|
-
// Dump to file
|
|
111
|
-
await run(dumpBin, [ sourceUri, `--output=${tmpFile}` ]);
|
|
112
|
-
const content = fs.readFileSync(tmpFile, 'utf8');
|
|
113
|
-
const lines = content.split('\n').filter(l => l.trim());
|
|
114
|
-
|
|
115
|
-
// Should have header + docs for each collection
|
|
116
|
-
assert(lines.length >= 4, `Expected at least 4 lines, got ${lines.length}`);
|
|
117
|
-
|
|
118
|
-
// Every line should be valid JSON
|
|
119
|
-
for (const line of lines) {
|
|
120
|
-
JSON.parse(line);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Should have collection headers
|
|
124
|
-
const headers = lines
|
|
125
|
-
.map(l => JSON.parse(l))
|
|
126
|
-
.filter(e => e._collection && !e._doc);
|
|
127
|
-
const collNames = headers.map(h => h._collection).sort();
|
|
128
|
-
assert(collNames.includes('aposDocs'));
|
|
129
|
-
assert(collNames.includes('aposCache'));
|
|
130
|
-
|
|
131
|
-
// Restore to target
|
|
132
|
-
await run(restoreBin, [ targetUri, `--input=${tmpFile}` ]);
|
|
133
|
-
|
|
134
|
-
// Verify target has the data
|
|
135
|
-
const client2 = await dbConnect(targetUri);
|
|
136
|
-
const db2 = client2.db();
|
|
137
|
-
const docs = await db2.collection('aposDocs').find({}).sort({ _id: 1 }).toArray();
|
|
138
|
-
assert.strictEqual(docs.length, 2);
|
|
139
|
-
assert.strictEqual(docs[0]._id, 'doc1');
|
|
140
|
-
assert.strictEqual(docs[0].title, 'Hello');
|
|
141
|
-
assert.deepStrictEqual(docs[0].tags, [ 'a', 'b' ]);
|
|
142
|
-
assert.strictEqual(docs[1]._id, 'doc2');
|
|
143
|
-
|
|
144
|
-
const cacheDoc = await db2.collection('aposCache').findOne({ _id: 'cache1' });
|
|
145
|
-
assert(cacheDoc);
|
|
146
|
-
assert.strictEqual(cacheDoc.value, 42);
|
|
147
|
-
await client2.close();
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('should preserve Date objects via $date serialization', async function () {
|
|
151
|
-
await dropAll(sourceUri);
|
|
152
|
-
const client = await dbConnect(sourceUri);
|
|
153
|
-
const db = client.db();
|
|
154
|
-
const testDate = new Date('2024-06-15T10:30:00.000Z');
|
|
155
|
-
await db.collection('aposDocs').insertOne({
|
|
156
|
-
_id: 'dateDoc',
|
|
157
|
-
createdAt: testDate,
|
|
158
|
-
nested: { updatedAt: testDate }
|
|
159
|
-
});
|
|
160
|
-
await client.close();
|
|
161
|
-
|
|
162
|
-
// Dump and check format
|
|
163
|
-
const { stdout } = await run(dumpBin, [ sourceUri ]);
|
|
164
|
-
assert(stdout.includes('"$date"'));
|
|
165
|
-
assert(stdout.includes('2024-06-15T10:30:00.000Z'), 'Should contain ISO date string');
|
|
166
|
-
|
|
167
|
-
// Restore and verify dates come back as Date objects
|
|
168
|
-
await dropAll(targetUri);
|
|
169
|
-
await run(dumpBin, [ sourceUri, `--output=${tmpFile}` ]);
|
|
170
|
-
await run(restoreBin, [ targetUri, `--input=${tmpFile}` ]);
|
|
171
|
-
|
|
172
|
-
const client2 = await dbConnect(targetUri);
|
|
173
|
-
const db2 = client2.db();
|
|
174
|
-
const doc = await db2.collection('aposDocs').findOne({ _id: 'dateDoc' });
|
|
175
|
-
assert(doc.createdAt instanceof Date);
|
|
176
|
-
assert.strictEqual(doc.createdAt.toISOString(), '2024-06-15T10:30:00.000Z');
|
|
177
|
-
assert(doc.nested.updatedAt instanceof Date);
|
|
178
|
-
assert.strictEqual(doc.nested.updatedAt.toISOString(), '2024-06-15T10:30:00.000Z');
|
|
179
|
-
await client2.close();
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('should dump and restore indexes', async function () {
|
|
183
|
-
await dropAll(sourceUri);
|
|
184
|
-
const client = await dbConnect(sourceUri);
|
|
185
|
-
const db = client.db();
|
|
186
|
-
const col = db.collection('aposDocs');
|
|
187
|
-
await col.insertMany([
|
|
188
|
-
{
|
|
189
|
-
_id: 'idx1',
|
|
190
|
-
slug: 'hello',
|
|
191
|
-
price: 10
|
|
192
|
-
},
|
|
193
|
-
{
|
|
194
|
-
_id: 'idx2',
|
|
195
|
-
slug: 'world',
|
|
196
|
-
price: 20
|
|
197
|
-
}
|
|
198
|
-
]);
|
|
199
|
-
await col.createIndex({ slug: 1 });
|
|
200
|
-
await col.createIndex({ slug: 1 }, {
|
|
201
|
-
unique: true,
|
|
202
|
-
name: 'slug_unique'
|
|
203
|
-
});
|
|
204
|
-
await col.createIndex({ price: 1 }, { type: 'number' });
|
|
205
|
-
await client.close();
|
|
206
|
-
|
|
207
|
-
// Dump
|
|
208
|
-
await run(dumpBin, [ sourceUri, `--output=${tmpFile}` ]);
|
|
209
|
-
|
|
210
|
-
const content = fs.readFileSync(tmpFile, 'utf8');
|
|
211
|
-
const header = JSON.parse(content.split('\n')[0]);
|
|
212
|
-
assert(header._indexes, 'Header should contain _indexes');
|
|
213
|
-
assert(header._indexes.length >= 2, 'Should have at least 2 custom indexes');
|
|
214
|
-
|
|
215
|
-
// Restore
|
|
216
|
-
await dropAll(targetUri);
|
|
217
|
-
await run(restoreBin, [ targetUri, `--input=${tmpFile}` ]);
|
|
218
|
-
|
|
219
|
-
// Verify indexes exist on target
|
|
220
|
-
const client2 = await dbConnect(targetUri);
|
|
221
|
-
const db2 = client2.db();
|
|
222
|
-
const indexes = await db2.collection('aposDocs').indexes();
|
|
223
|
-
assert(indexes.find(i => i.key && i.key.slug === 1 && !i.unique),
|
|
224
|
-
'Should have regular slug index');
|
|
225
|
-
assert(indexes.find(i => i.key && i.key.slug === 1 && i.unique),
|
|
226
|
-
'Should have unique slug index');
|
|
227
|
-
|
|
228
|
-
// Verify unique constraint is enforced
|
|
229
|
-
try {
|
|
230
|
-
await db2.collection('aposDocs').insertOne({
|
|
231
|
-
_id: 'idx3',
|
|
232
|
-
slug: 'hello'
|
|
233
|
-
});
|
|
234
|
-
assert.fail('Should have rejected duplicate slug');
|
|
235
|
-
} catch (e) {
|
|
236
|
-
assert(e.code === 11000 || /duplicate|unique|already exists/i.test(e.message));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
await client2.close();
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it('should handle piped stdout-to-stdin', async function () {
|
|
243
|
-
await dropAll(sourceUri);
|
|
244
|
-
const client = await dbConnect(sourceUri);
|
|
245
|
-
const db = client.db();
|
|
246
|
-
await db.collection('aposDocs').insertMany([
|
|
247
|
-
{
|
|
248
|
-
_id: 'pipe1',
|
|
249
|
-
title: 'Piped'
|
|
250
|
-
}
|
|
251
|
-
]);
|
|
252
|
-
await client.close();
|
|
253
|
-
|
|
254
|
-
// Dump to file, then restore from file (simulating pipe)
|
|
255
|
-
const { stdout } = await run(dumpBin, [ sourceUri ]);
|
|
256
|
-
|
|
257
|
-
// Write stdout to tmp, restore from it
|
|
258
|
-
fs.writeFileSync(tmpFile, stdout);
|
|
259
|
-
await dropAll(targetUri);
|
|
260
|
-
await run(restoreBin, [ targetUri, `--input=${tmpFile}` ]);
|
|
261
|
-
|
|
262
|
-
const client2 = await dbConnect(targetUri);
|
|
263
|
-
const db2 = client2.db();
|
|
264
|
-
const doc = await db2.collection('aposDocs').findOne({ _id: 'pipe1' });
|
|
265
|
-
assert(doc);
|
|
266
|
-
assert.strictEqual(doc.title, 'Piped');
|
|
267
|
-
await client2.close();
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it('should handle large collections in batches', async function () {
|
|
271
|
-
await dropAll(sourceUri);
|
|
272
|
-
const client = await dbConnect(sourceUri);
|
|
273
|
-
const db = client.db();
|
|
274
|
-
const docs = [];
|
|
275
|
-
for (let i = 0; i < 350; i++) {
|
|
276
|
-
docs.push({
|
|
277
|
-
_id: `batch${String(i).padStart(4, '0')}`,
|
|
278
|
-
value: i
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
await db.collection('aposDocs').insertMany(docs);
|
|
282
|
-
await client.close();
|
|
283
|
-
|
|
284
|
-
// Dump
|
|
285
|
-
await run(dumpBin, [ sourceUri, `--output=${tmpFile}` ]);
|
|
286
|
-
|
|
287
|
-
const content = fs.readFileSync(tmpFile, 'utf8');
|
|
288
|
-
const lines = content.split('\n').filter(l => l.trim());
|
|
289
|
-
// 1 header + 350 doc lines
|
|
290
|
-
assert.strictEqual(lines.length, 351);
|
|
291
|
-
|
|
292
|
-
// Docs should be sorted by _id
|
|
293
|
-
const docLines = lines.slice(1).map(l => JSON.parse(l));
|
|
294
|
-
for (let i = 1; i < docLines.length; i++) {
|
|
295
|
-
assert(docLines[i]._doc._id > docLines[i - 1]._doc._id,
|
|
296
|
-
'Docs should be sorted by _id');
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Restore and verify count
|
|
300
|
-
await dropAll(targetUri);
|
|
301
|
-
await run(restoreBin, [ targetUri, `--input=${tmpFile}` ]);
|
|
302
|
-
|
|
303
|
-
const client2 = await dbConnect(targetUri);
|
|
304
|
-
const db2 = client2.db();
|
|
305
|
-
const count = await db2.collection('aposDocs').countDocuments({});
|
|
306
|
-
assert.strictEqual(count, 350);
|
|
307
|
-
await client2.close();
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it('should restore to a clean state (drop existing data)', async function () {
|
|
311
|
-
// Put some pre-existing data in target
|
|
312
|
-
const client = await dbConnect(targetUri);
|
|
313
|
-
const db = client.db();
|
|
314
|
-
try {
|
|
315
|
-
await db.collection('aposDocs').drop();
|
|
316
|
-
} catch (e) {
|
|
317
|
-
// ignore
|
|
318
|
-
}
|
|
319
|
-
await db.collection('aposDocs').insertOne({
|
|
320
|
-
_id: 'old',
|
|
321
|
-
title: 'Should be removed'
|
|
322
|
-
});
|
|
323
|
-
await client.close();
|
|
324
|
-
|
|
325
|
-
// Set up source with different data
|
|
326
|
-
await dropAll(sourceUri);
|
|
327
|
-
const client2 = await dbConnect(sourceUri);
|
|
328
|
-
const db2 = client2.db();
|
|
329
|
-
await db2.collection('aposDocs').insertOne({
|
|
330
|
-
_id: 'new',
|
|
331
|
-
title: 'Fresh data'
|
|
332
|
-
});
|
|
333
|
-
await client2.close();
|
|
334
|
-
|
|
335
|
-
// Dump source and restore to target
|
|
336
|
-
await run(dumpBin, [ sourceUri, `--output=${tmpFile}` ]);
|
|
337
|
-
await run(restoreBin, [ targetUri, `--input=${tmpFile}` ]);
|
|
338
|
-
|
|
339
|
-
// Target should only have the new data
|
|
340
|
-
const client3 = await dbConnect(targetUri);
|
|
341
|
-
const db3 = client3.db();
|
|
342
|
-
const all = await db3.collection('aposDocs').find({}).toArray();
|
|
343
|
-
assert.strictEqual(all.length, 1);
|
|
344
|
-
assert.strictEqual(all[0]._id, 'new');
|
|
345
|
-
await client3.close();
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
it('should fail with usage error when no URI is provided', async function () {
|
|
349
|
-
try {
|
|
350
|
-
await run(dumpBin, []);
|
|
351
|
-
assert.fail('Should have exited with error');
|
|
352
|
-
} catch (e) {
|
|
353
|
-
assert.strictEqual(e.code, 1);
|
|
354
|
-
assert(e.stderr.includes('Usage'));
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
try {
|
|
358
|
-
await run(restoreBin, []);
|
|
359
|
-
assert.fail('Should have exited with error');
|
|
360
|
-
} catch (e) {
|
|
361
|
-
assert.strictEqual(e.code, 1);
|
|
362
|
-
assert(e.stderr.includes('Usage'));
|
|
363
|
-
}
|
|
364
|
-
});
|
|
365
|
-
});
|
package/test/default-adapter.js
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
const assert = require('assert');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
describe('Default Adapter', function() {
|
|
5
|
-
|
|
6
|
-
this.timeout(20000);
|
|
7
|
-
|
|
8
|
-
// Save and restore env vars
|
|
9
|
-
let savedAposDefaultDbAdapter;
|
|
10
|
-
let savedAposDbUri;
|
|
11
|
-
let savedAposMongdbUri;
|
|
12
|
-
|
|
13
|
-
beforeEach(function() {
|
|
14
|
-
savedAposDefaultDbAdapter = process.env.APOS_DEFAULT_DB_ADAPTER;
|
|
15
|
-
savedAposDbUri = process.env.APOS_DB_URI;
|
|
16
|
-
savedAposMongdbUri = process.env.APOS_MONGODB_URI;
|
|
17
|
-
delete process.env.APOS_DEFAULT_DB_ADAPTER;
|
|
18
|
-
delete process.env.APOS_DB_URI;
|
|
19
|
-
delete process.env.APOS_MONGODB_URI;
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
afterEach(function() {
|
|
23
|
-
if (savedAposDefaultDbAdapter !== undefined) {
|
|
24
|
-
process.env.APOS_DEFAULT_DB_ADAPTER = savedAposDefaultDbAdapter;
|
|
25
|
-
} else {
|
|
26
|
-
delete process.env.APOS_DEFAULT_DB_ADAPTER;
|
|
27
|
-
}
|
|
28
|
-
if (savedAposDbUri !== undefined) {
|
|
29
|
-
process.env.APOS_DB_URI = savedAposDbUri;
|
|
30
|
-
} else {
|
|
31
|
-
delete process.env.APOS_DB_URI;
|
|
32
|
-
}
|
|
33
|
-
if (savedAposMongdbUri !== undefined) {
|
|
34
|
-
process.env.APOS_MONGODB_URI = savedAposMongdbUri;
|
|
35
|
-
} else {
|
|
36
|
-
delete process.env.APOS_MONGODB_URI;
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// These tests verify URI construction by examining module internals
|
|
41
|
-
// without needing actual database connections.
|
|
42
|
-
|
|
43
|
-
it('builds mongodb:// URI by default', function() {
|
|
44
|
-
const uri = buildUri({ shortName: 'mysite' });
|
|
45
|
-
assert(uri.startsWith('mongodb://'));
|
|
46
|
-
assert(uri.includes('localhost:27017'));
|
|
47
|
-
assert(uri.includes('/mysite'));
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('builds mongodb:// URI when defaultAdapter is "mongodb"', function() {
|
|
51
|
-
const uri = buildUri({
|
|
52
|
-
shortName: 'mysite',
|
|
53
|
-
dbOptions: { defaultAdapter: 'mongodb' }
|
|
54
|
-
});
|
|
55
|
-
assert(uri.startsWith('mongodb://'));
|
|
56
|
-
assert(uri.includes('/mysite'));
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('builds sqlite:// URI when defaultAdapter is "sqlite"', function() {
|
|
60
|
-
const uri = buildUri({
|
|
61
|
-
shortName: 'mysite',
|
|
62
|
-
rootDir: '/app',
|
|
63
|
-
dbOptions: { defaultAdapter: 'sqlite' }
|
|
64
|
-
});
|
|
65
|
-
assert(uri.startsWith('sqlite://'));
|
|
66
|
-
assert(uri.includes(path.join('data', 'mysite.sqlite')));
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('builds postgres:// URI when defaultAdapter is "postgres"', function() {
|
|
70
|
-
const uri = buildUri({
|
|
71
|
-
shortName: 'mysite',
|
|
72
|
-
dbOptions: { defaultAdapter: 'postgres' }
|
|
73
|
-
});
|
|
74
|
-
assert(uri.startsWith('postgres://'));
|
|
75
|
-
assert(uri.includes('localhost:5432'));
|
|
76
|
-
assert(uri.includes('/mysite'));
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('builds multipostgres:// URI when defaultAdapter is "multipostgres"', function() {
|
|
80
|
-
const uri = buildUri({
|
|
81
|
-
shortName: 'mysite',
|
|
82
|
-
dbOptions: { defaultAdapter: 'multipostgres' }
|
|
83
|
-
});
|
|
84
|
-
assert(uri.startsWith('multipostgres://'));
|
|
85
|
-
assert(uri.includes('localhost:5432'));
|
|
86
|
-
assert(uri.includes('/mysite'));
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('includes URI-encoded credentials for postgres', function() {
|
|
90
|
-
const uri = buildUri({
|
|
91
|
-
shortName: 'mysite',
|
|
92
|
-
dbOptions: {
|
|
93
|
-
defaultAdapter: 'postgres',
|
|
94
|
-
user: 'admin',
|
|
95
|
-
password: 'p@ss:word/special'
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
assert(uri.startsWith('postgres://'));
|
|
99
|
-
assert(uri.includes('admin'));
|
|
100
|
-
assert(uri.includes(encodeURIComponent('p@ss:word/special')));
|
|
101
|
-
assert(!uri.includes('p@ss:word/special'));
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('includes URI-encoded credentials for mongodb', function() {
|
|
105
|
-
const uri = buildUri({
|
|
106
|
-
shortName: 'mysite',
|
|
107
|
-
dbOptions: {
|
|
108
|
-
defaultAdapter: 'mongodb',
|
|
109
|
-
user: 'admin',
|
|
110
|
-
password: 'p@ss:word'
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
assert(uri.startsWith('mongodb://'));
|
|
114
|
-
assert(uri.includes(encodeURIComponent('p@ss:word')));
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('APOS_DEFAULT_DB_ADAPTER env var overrides the option', function() {
|
|
118
|
-
process.env.APOS_DEFAULT_DB_ADAPTER = 'postgres';
|
|
119
|
-
const uri = buildUri({
|
|
120
|
-
shortName: 'mysite',
|
|
121
|
-
dbOptions: { defaultAdapter: 'mongodb' }
|
|
122
|
-
});
|
|
123
|
-
assert(uri.startsWith('postgres://'));
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('throws for invalid adapter name', function() {
|
|
127
|
-
assert.throws(() => {
|
|
128
|
-
buildUri({
|
|
129
|
-
shortName: 'mysite',
|
|
130
|
-
dbOptions: { defaultAdapter: 'invalid' }
|
|
131
|
-
});
|
|
132
|
-
}, /Invalid defaultAdapter/);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('explicit uri option overrides defaultAdapter', function() {
|
|
136
|
-
const uri = buildUri({
|
|
137
|
-
shortName: 'mysite',
|
|
138
|
-
dbOptions: {
|
|
139
|
-
defaultAdapter: 'postgres',
|
|
140
|
-
uri: 'mongodb://custom:27017/other'
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
assert.strictEqual(uri, 'mongodb://custom:27017/other');
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('APOS_DB_URI env var overrides everything', function() {
|
|
147
|
-
process.env.APOS_DB_URI = 'mongodb://envhost:27017/envdb';
|
|
148
|
-
const uri = buildUri({
|
|
149
|
-
shortName: 'mysite',
|
|
150
|
-
dbOptions: { defaultAdapter: 'postgres' }
|
|
151
|
-
});
|
|
152
|
-
assert.strictEqual(uri, 'mongodb://envhost:27017/envdb');
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('honors custom host and port for postgres', function() {
|
|
156
|
-
const uri = buildUri({
|
|
157
|
-
shortName: 'mysite',
|
|
158
|
-
dbOptions: {
|
|
159
|
-
defaultAdapter: 'postgres',
|
|
160
|
-
host: 'dbserver',
|
|
161
|
-
port: 5433
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
assert(uri.includes('dbserver:5433'));
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('honors custom host and port for mongodb', function() {
|
|
168
|
-
const uri = buildUri({
|
|
169
|
-
shortName: 'mysite',
|
|
170
|
-
dbOptions: {
|
|
171
|
-
defaultAdapter: 'mongodb',
|
|
172
|
-
host: 'mongohost',
|
|
173
|
-
port: 27018
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
assert(uri.includes('mongohost:27018'));
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('uses shortName as database name by default', function() {
|
|
180
|
-
const uri = buildUri({ shortName: 'my-app' });
|
|
181
|
-
assert(uri.endsWith('/my-app'));
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('uses name option over shortName when provided', function() {
|
|
185
|
-
const uri = buildUri({
|
|
186
|
-
shortName: 'my-app',
|
|
187
|
-
dbOptions: { name: 'custom-db' }
|
|
188
|
-
});
|
|
189
|
-
assert(uri.includes('/custom-db'));
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('ignores host/port/user/password for sqlite', function() {
|
|
193
|
-
const uri = buildUri({
|
|
194
|
-
shortName: 'mysite',
|
|
195
|
-
dbOptions: {
|
|
196
|
-
defaultAdapter: 'sqlite',
|
|
197
|
-
host: 'shouldbeignored',
|
|
198
|
-
port: 9999,
|
|
199
|
-
user: 'nobody',
|
|
200
|
-
password: 'nothing'
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
assert(uri.startsWith('sqlite://'));
|
|
204
|
-
assert(!uri.includes('shouldbeignored'));
|
|
205
|
-
assert(!uri.includes('9999'));
|
|
206
|
-
assert(!uri.includes('nobody'));
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
// Helper: simulate the URI construction logic from the db module
|
|
211
|
-
// without actually connecting. This extracts the same logic path.
|
|
212
|
-
function buildUri(options = {}) {
|
|
213
|
-
const escapeHost = require('../lib/escape-host.js');
|
|
214
|
-
const shortName = options.shortName || 'test-app';
|
|
215
|
-
const rootDir = options.rootDir || '/tmp/test-app';
|
|
216
|
-
const dbOptions = { ...(options.dbOptions || {}) };
|
|
217
|
-
|
|
218
|
-
// Simulate the connectToDb URI construction
|
|
219
|
-
const viaEnv = process.env.APOS_DB_URI || process.env.APOS_MONGODB_URI;
|
|
220
|
-
if (viaEnv) {
|
|
221
|
-
return viaEnv;
|
|
222
|
-
}
|
|
223
|
-
if (dbOptions.uri) {
|
|
224
|
-
return dbOptions.uri;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const validAdapters = [ 'mongodb', 'sqlite', 'postgres', 'multipostgres' ];
|
|
228
|
-
const adapter = process.env.APOS_DEFAULT_DB_ADAPTER || dbOptions.defaultAdapter || 'mongodb';
|
|
229
|
-
if (!validAdapters.includes(adapter)) {
|
|
230
|
-
throw new Error(`Invalid defaultAdapter: "${adapter}". Must be one of: ${validAdapters.join(', ')}`);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (!dbOptions.name) {
|
|
234
|
-
dbOptions.name = shortName;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (adapter === 'sqlite') {
|
|
238
|
-
const path = require('path');
|
|
239
|
-
return `sqlite://${path.resolve(rootDir, 'data', dbOptions.name + '.sqlite')}`;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const credentials = dbOptions.user
|
|
243
|
-
? encodeURIComponent(dbOptions.user) + ':' + encodeURIComponent(dbOptions.password) + '@'
|
|
244
|
-
: '';
|
|
245
|
-
|
|
246
|
-
if (adapter === 'mongodb') {
|
|
247
|
-
const host = dbOptions.host || 'localhost';
|
|
248
|
-
const port = dbOptions.port || 27017;
|
|
249
|
-
return 'mongodb://' + credentials + escapeHost(host) + ':' + port + '/' + dbOptions.name;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// postgres or multipostgres
|
|
253
|
-
const host = dbOptions.host || 'localhost';
|
|
254
|
-
const port = dbOptions.port || 5432;
|
|
255
|
-
return adapter + '://' + credentials + escapeHost(host) + ':' + port + '/' + dbOptions.name;
|
|
256
|
-
}
|