Skip to content

Commit f89c55c

Browse files
authored
add addMissingSchemaFields task (#5101)
* add addMissingSchemaFields task * add skipMigration flag * add node 24 support * move migration handler from apostrophe index to migration after handler * add migration requirements handler --------- Co-authored-by: Harouna Traoré <haroun@users.noreply.github.com>
1 parent a99262f commit f89c55c

10 files changed

Lines changed: 329 additions & 58 deletions

File tree

‎.github/workflows/main.yml‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,21 @@ jobs:
1818
runs-on: ubuntu-latest
1919
strategy:
2020
matrix:
21-
node-version: [20, 22]
21+
node-version: [20, 22, 24]
2222
mongodb-version: [6.0, 7.0, 8.0]
2323

2424
# Steps represent a sequence of tasks that will be executed as part of the job
2525
steps:
2626
- name: Git checkout
27-
uses: actions/checkout@v4
27+
uses: actions/checkout@v5
2828

2929
- name: Use Node.js ${{ matrix.node-version }}
30-
uses: actions/setup-node@v4
30+
uses: actions/setup-node@v6
3131
with:
3232
node-version: ${{ matrix.node-version }}
3333

3434
- name: Start MongoDB
35-
uses: supercharge/mongodb-github-action@1.11.0
35+
uses: supercharge/mongodb-github-action@1.12.0
3636
with:
3737
mongodb-version: ${{ matrix.mongodb-version }}
3838

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44

55
### Adds
66

7+
* Add `@apostrophecms/migration:add-missing-schema-fields` task. This task does not run database migrations.
78
* Translation strings added for the layout- and layout-column-widgets.
89
* New `box` schema field type
910
* When switching locale from the doc editor, ask if the user wants to localize the current document in the target locale or want to start a blank document.
1011
* Introduced a new `longPolling: false` option for the `@apostrophecms/notification` module. This eliminates long-pending requests when logged in, but also slows down the delivery of notifications. The behavior can be tuned further via the `pollingInterval` option, which defaults to `5000` milliseconds.
1112

1213
### Changes
1314

15+
* `@apostrophecms/migration:requirements` handler now runs the migration requirements like `insertIfMissing`, `implementParkAllInDefaultLocale`, `replicate` and `implementParkAllInOtherLocales`.
1416
* Bump nodemailer to v7.x.
1517

1618
### Fixes

‎index.js‎

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -323,21 +323,7 @@ async function apostrophe(options, telemetry, rootSpan) {
323323
await self.emit('modulesRegistered'); // formerly modulesReady
324324
self.apos.schema.validateAllSchemas();
325325
self.apos.schema.registerAllSchemas();
326-
await self.apos.lock.withLock('@apostrophecms/migration:migrate', async () => {
327-
await self.apos.migration.migrate(self.argv);
328-
// Inserts the global doc in the default locale if it does not exist;
329-
// same for other singleton piece types registered by other modules
330-
for (const apostropheModule of Object.values(self.modules)) {
331-
if (self.instanceOf(apostropheModule, '@apostrophecms/piece-type') && apostropheModule.options.singletonAuto) {
332-
await apostropheModule.insertIfMissing();
333-
}
334-
}
335-
await self.apos.page.implementParkAllInDefaultLocale();
336-
await self.apos.doc.replicate(); // emits beforeReplicate and afterReplicate events
337-
// Replicate will have created the parked pages across locales if needed,
338-
// but we may still need to reset parked properties
339-
await self.apos.page.implementParkAllInOtherLocales();
340-
});
326+
await self.apos.migration.migrate(self.argv);
341327
await self.emit('ready'); // formerly afterInit
342328

343329
if (self.taskRan) {

‎modules/@apostrophecms/migration/index.js‎

Lines changed: 89 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,53 @@ const addMissingSchemaFields = require('./lib/addMissingSchemaFields.js');
1414
// is difficult to guarantee, you may wish to write a task instead.
1515

1616
module.exports = {
17-
options: { alias: 'migration' },
17+
options: {
18+
alias: 'migration',
19+
skipMigrationTasks: [
20+
'@apostrophecms/migration:add-missing-schema-fields'
21+
]
22+
},
1823
async init(self) {
1924
self.migrations = [];
2025
await self.enableCollection();
2126
},
2227
handlers(self) {
2328
return {
29+
'apostrophe:modulesRegistered': {
30+
setSkipMigration() {
31+
if (
32+
self.apos.isTask() &&
33+
self.options.skipMigrationTasks.includes(self.apos.argv._.at(0))
34+
) {
35+
self.apos.skipMigration = true;
36+
}
37+
}
38+
},
39+
before: {
40+
async addMissingSchemaFields() {
41+
await self.addMissingSchemaFields();
42+
}
43+
},
44+
requirements: {
45+
async insertRequired() {
46+
// Inserts the global doc in the default locale if it does not exist;
47+
// same for other singleton piece types registered by other modules
48+
for (const apostropheModule of Object.values(self.apos.modules)) {
49+
if (
50+
self.apos.instanceOf(apostropheModule, '@apostrophecms/piece-type') &&
51+
apostropheModule.options.singletonAuto
52+
) {
53+
await apostropheModule.insertIfMissing();
54+
}
55+
}
56+
await self.apos.page.implementParkAllInDefaultLocale();
57+
await self.apos.doc.replicate();
58+
// emits beforeReplicate and afterReplicate events
59+
// Replicate will have created the parked pages across locales if needed,
60+
// but we may still need to reset parked properties
61+
await self.apos.page.implementParkAllInOtherLocales();
62+
}
63+
},
2464
'apostrophe:ready': {
2565
addSortifyMigrations() {
2666
const managers = self.apos.doc.managers;
@@ -41,11 +81,6 @@ module.exports = {
4181
});
4282
});
4383
}
44-
},
45-
before: {
46-
async addMissingSchemaFields() {
47-
await self.addMissingSchemaFields();
48-
}
4984
}
5085
};
5186
},
@@ -236,43 +271,53 @@ module.exports = {
236271
// Perform the actual migrations. Implementation of
237272
// the @apostrophecms/migration:migrate task
238273
async migrate(options) {
239-
await self.emit('before');
240-
if (self.apos.isNew) {
274+
await self.apos.lock.withLock('@apostrophecms/migration:migrate', async () => {
275+
if (self.apos.skipMigration === true) {
276+
return;
277+
}
278+
279+
await self.emit('before');
280+
281+
if (self.apos.isNew) {
241282
// Since the site is brand new (zero documents), we may assume
242283
// it requires no migrations. Mark them all as "done" but note
243284
// that they were skipped, just in case we decide that's an issue
244285
// later
245-
const at = new Date();
246-
// Just in case the db has no documents but did
247-
// start to run migrations on a previous attempt,
248-
// which causes an occasional unique key error if not
249-
// corrected for here.
250-
//
251-
// Other migration-related facts that are not migration
252-
// names are stored with a leading *, leave them alone
253-
await self.db.removeMany({
254-
_id: /^[^*]/
255-
});
256-
await self.db.insertMany(self.migrations.map(migration => ({
257-
_id: migration.name,
258-
at,
259-
skipped: true
260-
})));
261-
} else {
262-
for (const migration of self.migrations) {
263-
await self.runOne(migration);
286+
const at = new Date();
287+
// Just in case the db has no documents but did
288+
// start to run migrations on a previous attempt,
289+
// which causes an occasional unique key error if not
290+
// corrected for here.
291+
//
292+
// Other migration-related facts that are not migration
293+
// names are stored with a leading *, leave them alone
294+
await self.db.removeMany({
295+
_id: /^[^*]/
296+
});
297+
await self.db.insertMany(self.migrations.map(migration => ({
298+
_id: migration.name,
299+
at,
300+
skipped: true
301+
})));
302+
} else {
303+
for (const migration of self.migrations) {
304+
await self.runOne(migration);
305+
}
264306
}
265-
}
266-
// In production, this event is emitted only at the end of the migrate
267-
// command line task. In dev it is emitted at every startup after the
268-
// automatic migration.
269-
//
270-
// Intentionally emitted regardless of whether the site is new or not.
271-
//
272-
// This is the right time to park pages, for instance, because the
273-
// database is guaranteed to be in a stable state, whether because the
274-
// site is new or because migrations ran successfully.
275-
await self.emit('after');
307+
308+
// In production, this event is emitted only at the end of the migrate
309+
// command line task. In dev it is emitted at every startup after the
310+
// automatic migration.
311+
//
312+
// Intentionally emitted regardless of whether the site is new or not.
313+
//
314+
// This is the right time to park pages, for instance, because the
315+
// database is guaranteed to be in a stable state, whether because the
316+
// site is new or because migrations ran successfully.
317+
await self.emit('after');
318+
319+
await self.emit('requirements');
320+
});
276321
},
277322
async runOne(migration) {
278323
const info = await self.db.findOne({ _id: migration.name });
@@ -305,6 +350,12 @@ module.exports = {
305350
// and automatically detect whether any work
306351
// must be done
307352
task: () => {}
353+
},
354+
'add-missing-schema-fields': {
355+
usage: 'Add missing schema fields to existing database documents',
356+
task: async () => {
357+
await self.addMissingSchemaFields();
358+
}
308359
}
309360
};
310361
}

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"main": "index.js",
66
"scripts": {
77
"pretest": "npm run lint",
8-
"test": "nyc mocha -t 10000 --ignore=test/assets.js && nyc mocha -t 10000 test/assets.js && mocha -t 1000 test/esm-project/esm.js",
8+
"test": "nyc mocha -t 10000 --ignore=test/assets.js && nyc mocha -t 10000 test/add-missing-schema-fields-project/test.js && nyc mocha -t 10000 test/assets.js && mocha -t 1000 test/esm-project/esm.js",
99
"eslint": "eslint .",
1010
"eslint-fix": "npm run eslint -- --fix",
1111
"i18n": "node scripts/lint-i18n",
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import config from './config.js';
2+
import apostrophe from '../../index.js';
3+
4+
export default apostrophe(config);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export default {
2+
root: import.meta,
3+
shortName: 'add-missing-schema-fields-project',
4+
baseUrl: 'http://localhost:3000',
5+
modules: {
6+
'@apostrophecms/express': {
7+
options: {
8+
address: '127.0.0.1'
9+
}
10+
},
11+
product: {}
12+
}
13+
};
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
export default {
2+
extend: '@apostrophecms/piece-type',
3+
options: {
4+
label: 'Product',
5+
pluralLabel: 'Products'
6+
},
7+
fields: {
8+
add: {
9+
price: {
10+
type: 'float',
11+
label: 'Price',
12+
required: true
13+
},
14+
description: {
15+
type: 'string',
16+
label: 'Description',
17+
textarea: true,
18+
required: true
19+
},
20+
image: {
21+
label: 'Product photo',
22+
type: 'area',
23+
options: {
24+
max: 1,
25+
widgets: {
26+
'@apostrophecms/image': {}
27+
}
28+
}
29+
},
30+
copyright: {
31+
type: 'string',
32+
label: 'Copyright'
33+
}
34+
},
35+
group: {
36+
basics: {
37+
label: 'Basics',
38+
fields: [ 'title', 'price', 'description', 'image', 'copyright' ]
39+
// 👆 'title' is included here because it is in the default `basics`
40+
// group for all piece types. Since we are replacing that group, we
41+
// include it ourselves.
42+
}
43+
}
44+
},
45+
init(self) {
46+
self.apos.migration.add('add-first-product', self.addFirstProduct);
47+
self.apos.migration.add('add-copyright-notice', self.addCopyrightNotice);
48+
},
49+
methods(self) {
50+
return {
51+
async addFirstProduct() {
52+
await self.apos.modules.product.insert(
53+
self.apos.task.getReq(),
54+
{
55+
...self.apos.modules.product.newInstance(),
56+
title: 'My first product',
57+
price: 10.00,
58+
description: 'Product description'
59+
}
60+
);
61+
},
62+
async addCopyrightNotice() {
63+
await self.apos.migration.eachDoc({
64+
type: 'product'
65+
}, async (doc) => {
66+
if (doc.copyright === undefined) {
67+
await self.apos.doc.db.updateOne({
68+
_id: doc._id
69+
}, {
70+
$set: { copyright: '©2024 ApostropheCMS. All rights reserved.' }
71+
});
72+
}
73+
});
74+
}
75+
};
76+
}
77+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "add-missing-schema-fields-project",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "app.js",
6+
"type": "module",
7+
"scripts": {
8+
"test": "echo \"Error: no test specified\" && exit 1"
9+
},
10+
"keywords": [],
11+
"author": "",
12+
"license": "ISC",
13+
"dependencies": {
14+
"apostrophe": "file:../../."
15+
}
16+
}

0 commit comments

Comments
 (0)