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.
Files changed (64) hide show
  1. package/.claude/settings.local.json +15 -0
  2. package/CHANGELOG.md +30 -2
  3. package/eslint.config.js +1 -2
  4. package/lib/mongodb-connect.js +62 -0
  5. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBar.vue +0 -1
  6. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarMenu.vue +25 -8
  7. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBreakpointPreviewMode.vue +9 -0
  8. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +20 -2
  9. package/modules/@apostrophecms/area/index.js +10 -5
  10. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +2 -0
  11. package/modules/@apostrophecms/command-menu/ui/apos/components/TheAposCommandMenu.vue +11 -1
  12. package/modules/@apostrophecms/db/index.js +27 -68
  13. package/modules/@apostrophecms/http/index.js +1 -1
  14. package/modules/@apostrophecms/i18n/i18n/en.json +8 -0
  15. package/modules/@apostrophecms/i18n/index.js +1 -8
  16. package/modules/@apostrophecms/image-widget/index.js +29 -1
  17. package/modules/@apostrophecms/job/index.js +7 -9
  18. package/modules/@apostrophecms/layout-widget/index.js +124 -2
  19. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +89 -6
  20. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +2 -2
  21. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridManager.vue +2 -2
  22. package/modules/@apostrophecms/layout-widget/ui/apos/layout.css +8 -0
  23. package/modules/@apostrophecms/layout-widget/ui/src/layout.css +1 -1
  24. package/modules/@apostrophecms/layout-widget/views/widget.html +3 -3
  25. package/modules/@apostrophecms/login/index.js +13 -15
  26. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +2 -1
  27. package/modules/@apostrophecms/oembed/index.js +18 -13
  28. package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedIcon.vue +2 -0
  29. package/modules/@apostrophecms/rich-text-widget/index.js +36 -0
  30. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputCheckboxes.js +1 -1
  31. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRadio.js +1 -1
  32. package/modules/@apostrophecms/styles/index.js +16 -0
  33. package/modules/@apostrophecms/styles/lib/handlers.js +6 -0
  34. package/modules/@apostrophecms/styles/lib/methods.js +93 -0
  35. package/modules/@apostrophecms/styles/lib/presets.js +17 -0
  36. package/modules/@apostrophecms/styles/ui/apos/universal/render.mjs +10 -1
  37. package/modules/@apostrophecms/submitted-draft/ui/apos/components/AposSubmittedDraftIcon.vue +1 -0
  38. package/modules/@apostrophecms/template/views/outerLayoutBase.html +1 -1
  39. package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +14 -2
  40. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +29 -5
  41. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +8 -0
  42. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuItem.vue +5 -2
  43. package/modules/@apostrophecms/ui/ui/apos/components/AposLocalePicker.vue +14 -1
  44. package/modules/@apostrophecms/ui/ui/apos/scss/global/_utilities.scss +13 -1
  45. package/modules/@apostrophecms/util/index.js +4 -0
  46. package/modules/@apostrophecms/widget-type/index.js +6 -0
  47. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +32 -0
  48. package/package.json +13 -13
  49. package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +131 -0
  50. package/test/add-missing-schema-fields-project/test.js +3 -11
  51. package/test/assets.js +67 -110
  52. package/test/db.js +15 -24
  53. package/test/job.js +1 -1
  54. package/test/layout-widget-gap.js +530 -0
  55. package/test/login.js +122 -1
  56. package/test/rich-text-widget.js +200 -0
  57. package/test/styles.js +50 -0
  58. package/test-lib/util.js +14 -50
  59. package/claude-tools/detect-handles.js +0 -46
  60. package/claude-tools/minimal-hang-test.js +0 -28
  61. package/claude-tools/mongo-close-test.js +0 -11
  62. package/claude-tools/stdin-ref-test.js +0 -14
  63. package/test/db-tools.js +0 -365
  64. 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
- });
@@ -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
- }