webspresso 0.0.100 → 0.0.102
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/.agents/ADMIN.md +47 -4
- package/.agents/AGENTS.md +3 -1
- package/.agents/ARCHITECTURE.md +11 -0
- package/.agents/AUTH.md +63 -0
- package/.agents/CLI.md +81 -0
- package/.agents/CONVENTIONS.md +4 -0
- package/.agents/EMAIL.md +84 -2
- package/.agents/EVENTS.md +179 -0
- package/.agents/ORM.md +59 -0
- package/.agents/PLUGINS.md +32 -6
- package/.agents/ROUTING.md +163 -3
- package/.agents/skills/webspresso-framework/SKILL.md +32 -0
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +4 -4
- package/AGENTS.md +7 -0
- package/README.md +18 -5
- package/bin/commands/add-tailwind.js +10 -0
- package/bin/commands/build.js +34 -0
- package/bin/commands/db-makemigrations.js +83 -0
- package/bin/commands/list.js +96 -0
- package/bin/commands/make-command.js +93 -0
- package/bin/commands/make-event.js +80 -0
- package/bin/commands/make-factory.js +72 -0
- package/bin/commands/make-mail.js +93 -0
- package/bin/commands/make-policy.js +106 -0
- package/bin/commands/routes.js +8 -0
- package/bin/commands/run.js +195 -0
- package/bin/webspresso.js +17 -0
- package/core/applySchema.js +18 -1
- package/core/auth/bouncer.js +196 -0
- package/core/auth/index.js +10 -0
- package/core/auth/middleware.js +15 -1
- package/core/commands/define-command.js +48 -0
- package/core/commands/index.js +18 -0
- package/core/commands/registry.js +156 -0
- package/core/commands/runner.js +76 -0
- package/core/compression/compressible.js +1 -1
- package/core/compression/middleware.js +35 -251
- package/core/content/service.js +8 -2
- package/core/events/discovery.js +142 -0
- package/core/events/dispatcher.js +552 -0
- package/core/events/index.js +23 -0
- package/core/events/signal.js +92 -0
- package/core/forms/flash.js +234 -0
- package/core/forms/form-builder.js +214 -0
- package/core/forms/index.js +16 -0
- package/core/mail/index.js +23 -0
- package/core/mail/mailable.js +250 -0
- package/core/mail/mailer.js +184 -0
- package/core/mail/transports.js +238 -0
- package/core/orm/factory.js +250 -0
- package/core/orm/index.js +6 -0
- package/core/orm/migrations/diff.js +185 -0
- package/core/orm/migrations/index.js +22 -4
- package/core/orm/model.js +4 -0
- package/index.d.ts +422 -1
- package/index.js +111 -16
- package/package.json +9 -5
- package/plugins/admin-panel/api.js +509 -3
- package/plugins/admin-panel/index.js +29 -0
- package/plugins/content/models/content-entry.js +4 -3
- package/plugins/content/models/content-type.js +4 -3
- package/plugins/cors.js +2 -2
- package/plugins/csrf/index.js +1 -1
- package/plugins/email/index.js +48 -0
- package/plugins/email/preview-handler.js +555 -0
- package/plugins/email/service.js +23 -12
- package/plugins/email/transport.js +30 -11
- package/plugins/file-manager/index.js +13 -0
- package/plugins/index.js +2 -0
- package/plugins/lazy.js +258 -0
- package/plugins/redirect/index.js +1 -1
- package/src/api/api-loader.js +18 -2
- package/src/api/validation-i18n.js +231 -0
- package/src/app-context.js +48 -6
- package/src/dev-reload/index.js +9 -1
- package/src/discovery/route-manifest.js +197 -0
- package/src/discovery/scan-routes.js +80 -5
- package/src/file-router.js +153 -91
- package/src/helpers.js +143 -0
- package/src/njk-frontmatter.js +17 -1
- package/src/pages/page-loader.js +1 -1
- package/src/plugin-manager.js +12 -0
- package/src/routing/cascading.js +14 -10
- package/src/routing/route-table.js +1 -1
- package/src/server.js +243 -34
- package/src/services/builtins/system.js +4 -6
- package/src/services/registry.js +12 -2
- package/src/template/precompile.js +247 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso ORM - Model Factory & Seeding (AdonisJS Lucid-inspired)
|
|
3
|
+
* Declarative mock data generation and seeding for tests and development
|
|
4
|
+
* @module core/orm/factory
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Fallback lightweight mock faker when @faker-js/faker is not present
|
|
9
|
+
*/
|
|
10
|
+
function createMinimalFaker() {
|
|
11
|
+
let counter = 1000;
|
|
12
|
+
return {
|
|
13
|
+
person: {
|
|
14
|
+
fullName: () => `User_${++counter}`,
|
|
15
|
+
firstName: () => `First_${++counter}`,
|
|
16
|
+
lastName: () => `Last_${++counter}`,
|
|
17
|
+
},
|
|
18
|
+
internet: {
|
|
19
|
+
email: () => `user_${++counter}@example.com`,
|
|
20
|
+
userName: () => `user_${++counter}`,
|
|
21
|
+
url: () => `https://example.com/item/${++counter}`,
|
|
22
|
+
},
|
|
23
|
+
lorem: {
|
|
24
|
+
sentence: (words = 5) => `Sample text description ${++counter}`,
|
|
25
|
+
paragraph: () => `Sample paragraph for model testing with counter ${++counter}.`,
|
|
26
|
+
words: (n = 3) => `words_${++counter}`,
|
|
27
|
+
},
|
|
28
|
+
number: {
|
|
29
|
+
int: (opts = { min: 1, max: 1000 }) => {
|
|
30
|
+
const min = opts.min ?? 1;
|
|
31
|
+
const max = opts.max ?? 1000;
|
|
32
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
33
|
+
},
|
|
34
|
+
float: (opts = { min: 1, max: 100, precision: 0.01 }) => {
|
|
35
|
+
const min = opts.min ?? 1;
|
|
36
|
+
const max = opts.max ?? 100;
|
|
37
|
+
return Number((Math.random() * (max - min) + min).toFixed(2));
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
date: {
|
|
41
|
+
recent: () => new Date(),
|
|
42
|
+
past: () => new Date(Date.now() - 86400000 * 30),
|
|
43
|
+
future: () => new Date(Date.now() + 86400000 * 30),
|
|
44
|
+
},
|
|
45
|
+
datatype: {
|
|
46
|
+
boolean: () => Math.random() > 0.5,
|
|
47
|
+
},
|
|
48
|
+
string: {
|
|
49
|
+
nanoid: () => `nano_${Math.random().toString(36).slice(2, 10)}`,
|
|
50
|
+
uuid: () => `00000000-0000-4000-8000-${String(++counter).padStart(12, '0')}`,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveFaker() {
|
|
56
|
+
try {
|
|
57
|
+
const fakerMod = require('@faker-js/faker');
|
|
58
|
+
return fakerMod.faker || fakerMod.default?.faker || fakerMod;
|
|
59
|
+
} catch {
|
|
60
|
+
return createMinimalFaker();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Factory Builder for generating single or multiple model instances
|
|
66
|
+
*/
|
|
67
|
+
class FactoryBuilder {
|
|
68
|
+
/**
|
|
69
|
+
* @param {string} modelName
|
|
70
|
+
* @param {Function} definitionFn
|
|
71
|
+
* @param {Map<string, Function|Object>} states
|
|
72
|
+
*/
|
|
73
|
+
constructor(modelName, definitionFn, states = new Map()) {
|
|
74
|
+
this._modelName = modelName;
|
|
75
|
+
this._definitionFn = definitionFn;
|
|
76
|
+
this._states = states;
|
|
77
|
+
this._appliedStates = [];
|
|
78
|
+
this._count = 1;
|
|
79
|
+
this._overrides = {};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Set number of records to generate
|
|
84
|
+
* @param {number} n
|
|
85
|
+
* @returns {FactoryBuilder}
|
|
86
|
+
*/
|
|
87
|
+
count(n) {
|
|
88
|
+
const clone = this._clone();
|
|
89
|
+
clone._count = Math.max(1, Math.floor(n));
|
|
90
|
+
return clone;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Apply one or more named states
|
|
95
|
+
* @param {...string} stateNames
|
|
96
|
+
* @returns {FactoryBuilder}
|
|
97
|
+
*/
|
|
98
|
+
state(...stateNames) {
|
|
99
|
+
return this.apply(...stateNames);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Apply one or more named states
|
|
104
|
+
* @param {...string} stateNames
|
|
105
|
+
* @returns {FactoryBuilder}
|
|
106
|
+
*/
|
|
107
|
+
apply(...stateNames) {
|
|
108
|
+
const clone = this._clone();
|
|
109
|
+
clone._appliedStates.push(...stateNames);
|
|
110
|
+
return clone;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Merge field overrides
|
|
115
|
+
* @param {Object} overrides
|
|
116
|
+
* @returns {FactoryBuilder}
|
|
117
|
+
*/
|
|
118
|
+
merge(overrides = {}) {
|
|
119
|
+
const clone = this._clone();
|
|
120
|
+
clone._overrides = { ...clone._overrides, ...overrides };
|
|
121
|
+
return clone;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Build single raw record object without persisting
|
|
126
|
+
* @param {number} index
|
|
127
|
+
* @param {Object} [additionalOverrides={}]
|
|
128
|
+
* @returns {Object}
|
|
129
|
+
*/
|
|
130
|
+
_buildOne(index = 0, additionalOverrides = {}) {
|
|
131
|
+
const faker = resolveFaker();
|
|
132
|
+
let data = this._definitionFn({ faker, index }) || {};
|
|
133
|
+
|
|
134
|
+
for (const stateName of this._appliedStates) {
|
|
135
|
+
const stateHandler = this._states.get(stateName);
|
|
136
|
+
if (stateHandler) {
|
|
137
|
+
const stateData =
|
|
138
|
+
typeof stateHandler === 'function'
|
|
139
|
+
? stateHandler({ faker, index, current: data })
|
|
140
|
+
: stateHandler;
|
|
141
|
+
data = { ...data, ...stateData };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { ...data, ...this._overrides, ...additionalOverrides };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Generate raw data objects in memory (does not insert into DB)
|
|
150
|
+
* @param {Object} [overrides={}]
|
|
151
|
+
* @returns {Object|Object[]}
|
|
152
|
+
*/
|
|
153
|
+
make(overrides = {}) {
|
|
154
|
+
if (this._count === 1) {
|
|
155
|
+
return this._buildOne(0, overrides);
|
|
156
|
+
}
|
|
157
|
+
return Array.from({ length: this._count }, (_, i) => this._buildOne(i, overrides));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Generate and persist record(s) to the database using repository
|
|
162
|
+
* @param {Object} options
|
|
163
|
+
* @param {import('./types').DatabaseInstance|import('./types').Repository} options.db - Database or repository
|
|
164
|
+
* @param {Object} [options.merge={}] - Additional field overrides
|
|
165
|
+
* @returns {Promise<Object|Object[]>}
|
|
166
|
+
*/
|
|
167
|
+
async create(options = {}) {
|
|
168
|
+
const target = options.db || options;
|
|
169
|
+
let repo;
|
|
170
|
+
|
|
171
|
+
if (target && typeof target.getRepository === 'function') {
|
|
172
|
+
repo = target.getRepository(this._modelName);
|
|
173
|
+
} else if (target && typeof target.create === 'function') {
|
|
174
|
+
repo = target;
|
|
175
|
+
} else {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Factory.create() requires a database instance: .create({ db }) for model "${this._modelName}"`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const mergeData = options.merge || {};
|
|
182
|
+
|
|
183
|
+
if (this._count === 1) {
|
|
184
|
+
const attributes = this._buildOne(0, mergeData);
|
|
185
|
+
return await repo.create(attributes);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const items = Array.from({ length: this._count }, (_, i) => this._buildOne(i, mergeData));
|
|
189
|
+
const results = [];
|
|
190
|
+
for (const item of items) {
|
|
191
|
+
results.push(await repo.create(item));
|
|
192
|
+
}
|
|
193
|
+
return results;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Clone builder preserving definition and state
|
|
198
|
+
* @protected
|
|
199
|
+
*/
|
|
200
|
+
_clone() {
|
|
201
|
+
const clone = new FactoryBuilder(this._modelName, this._definitionFn, this._states);
|
|
202
|
+
clone._appliedStates = [...this._appliedStates];
|
|
203
|
+
clone._count = this._count;
|
|
204
|
+
clone._overrides = { ...this._overrides };
|
|
205
|
+
return clone;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Define a model factory (AdonisJS Lucid-inspired)
|
|
211
|
+
* @param {string} modelName - Model identifier in registry (e.g. 'User')
|
|
212
|
+
* @param {Function} definitionFn - Factory blueprint `({ faker, index }) => ({ ... })`
|
|
213
|
+
* @returns {FactoryBuilder & { state: (name: string, handler: Function|Object) => any }}
|
|
214
|
+
*/
|
|
215
|
+
function defineFactory(modelName, definitionFn) {
|
|
216
|
+
if (typeof modelName !== 'string' || !modelName.trim()) {
|
|
217
|
+
throw new Error('defineFactory expects a model name string as first argument');
|
|
218
|
+
}
|
|
219
|
+
if (typeof definitionFn !== 'function') {
|
|
220
|
+
throw new Error(`defineFactory expects a definition function for model "${modelName}"`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const states = new Map();
|
|
224
|
+
const rootBuilder = new FactoryBuilder(modelName, definitionFn, states);
|
|
225
|
+
|
|
226
|
+
// Extend root builder with fluent state registration
|
|
227
|
+
rootBuilder.registerState = function (name, handler) {
|
|
228
|
+
states.set(name, handler);
|
|
229
|
+
return rootBuilder;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Allow .state('admin', fn) as registration when called with 2 args,
|
|
233
|
+
// or as apply when called with 1 arg.
|
|
234
|
+
const originalState = rootBuilder.state.bind(rootBuilder);
|
|
235
|
+
rootBuilder.state = function (nameOrFirst, maybeHandler, ...rest) {
|
|
236
|
+
if (typeof maybeHandler === 'function' || (maybeHandler && typeof maybeHandler === 'object')) {
|
|
237
|
+
states.set(nameOrFirst, maybeHandler);
|
|
238
|
+
return rootBuilder;
|
|
239
|
+
}
|
|
240
|
+
return originalState(nameOrFirst, ...rest);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
return rootBuilder;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = {
|
|
247
|
+
defineFactory,
|
|
248
|
+
FactoryBuilder,
|
|
249
|
+
resolveFaker,
|
|
250
|
+
};
|
package/core/orm/index.js
CHANGED
|
@@ -386,4 +386,10 @@ module.exports = {
|
|
|
386
386
|
buildSearchClause: require('./search').buildSearchClause,
|
|
387
387
|
tokenizeSearchTerm: require('./search').tokenizeSearchTerm,
|
|
388
388
|
detectDialect: require('./search').detectDialect,
|
|
389
|
+
// Model Factory & Seeding (AdonisJS Lucid-inspired)
|
|
390
|
+
defineFactory: require('./factory').defineFactory,
|
|
391
|
+
FactoryBuilder: require('./factory').FactoryBuilder,
|
|
392
|
+
// Schema Diff & Makemigrations (Django-inspired)
|
|
393
|
+
diffSchema: require('./migrations/diff').diffSchema,
|
|
394
|
+
makeMigrations: require('./migrations/diff').makeMigrations,
|
|
389
395
|
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso ORM - Schema Diff & Auto-Migration Generator (Django-inspired makemigrations)
|
|
3
|
+
* Compares ORM models with live database schema and generates migrations
|
|
4
|
+
* @module core/orm/migrations/diff
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { getAllModels } = require('../model');
|
|
10
|
+
const {
|
|
11
|
+
scaffoldMigration,
|
|
12
|
+
scaffoldAlterMigration,
|
|
13
|
+
generateMigrationName,
|
|
14
|
+
} = require('./scaffold');
|
|
15
|
+
const {
|
|
16
|
+
migrationTimestamp,
|
|
17
|
+
sortModelsByDependencies,
|
|
18
|
+
loadModelsFromDirectory,
|
|
19
|
+
} = require('../../../bin/utils/model-migrations');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Compare models with existing database schema to find diffs
|
|
23
|
+
* @param {Object} options
|
|
24
|
+
* @param {import('knex').Knex} options.knex - Active Knex instance
|
|
25
|
+
* @param {import('../types').ModelDefinition[]} [options.models] - List of models to diff
|
|
26
|
+
* @param {boolean} [options.includeDrops=false] - Whether to generate dropColumn for removed fields
|
|
27
|
+
* @returns {Promise<{ hasChanges: boolean, changes: Array<Object> }>}
|
|
28
|
+
*/
|
|
29
|
+
async function diffSchema(options = {}) {
|
|
30
|
+
const {
|
|
31
|
+
knex,
|
|
32
|
+
models = [...getAllModels().values()],
|
|
33
|
+
includeDrops = false,
|
|
34
|
+
} = options;
|
|
35
|
+
|
|
36
|
+
if (!knex) {
|
|
37
|
+
throw new Error('diffSchema requires an active knex instance');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sortedModels = sortModelsByDependencies(models);
|
|
41
|
+
const changes = [];
|
|
42
|
+
|
|
43
|
+
for (const model of sortedModels) {
|
|
44
|
+
const tableExists = await knex.schema.hasTable(model.table);
|
|
45
|
+
|
|
46
|
+
if (!tableExists) {
|
|
47
|
+
// Table doesn't exist at all -> generate createTable
|
|
48
|
+
const migrationName = generateMigrationName(model, 'create');
|
|
49
|
+
const content = scaffoldMigration(model);
|
|
50
|
+
changes.push({
|
|
51
|
+
model: model.name,
|
|
52
|
+
table: model.table,
|
|
53
|
+
type: 'create_table',
|
|
54
|
+
addedColumns: Array.from(model.columns.keys()),
|
|
55
|
+
removedColumns: [],
|
|
56
|
+
migrationName,
|
|
57
|
+
content,
|
|
58
|
+
});
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Table exists -> inspect columns
|
|
63
|
+
const columnInfo = await knex(model.table).columnInfo();
|
|
64
|
+
const existingColNames = new Set(Object.keys(columnInfo));
|
|
65
|
+
|
|
66
|
+
const addedColumns = new Map();
|
|
67
|
+
for (const [colName, colMeta] of model.columns.entries()) {
|
|
68
|
+
if (!existingColNames.has(colName)) {
|
|
69
|
+
addedColumns.set(colName, colMeta);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const removedColumnNames = [];
|
|
74
|
+
if (includeDrops) {
|
|
75
|
+
for (const existingCol of existingColNames) {
|
|
76
|
+
// Skip knex internal migrations or common internal columns if needed
|
|
77
|
+
if (!model.columns.has(existingCol)) {
|
|
78
|
+
removedColumnNames.push(existingCol);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (addedColumns.size > 0) {
|
|
84
|
+
const addedKeys = Array.from(addedColumns.keys());
|
|
85
|
+
const prefix = addedKeys.slice(0, 3).join('_');
|
|
86
|
+
const migrationName = `add_${prefix}${addedKeys.length > 3 ? '_etc' : ''}_to_${model.table}_table`;
|
|
87
|
+
const content = scaffoldAlterMigration(model.table, addedColumns);
|
|
88
|
+
|
|
89
|
+
changes.push({
|
|
90
|
+
model: model.name,
|
|
91
|
+
table: model.table,
|
|
92
|
+
type: 'alter_table',
|
|
93
|
+
addedColumns: addedKeys,
|
|
94
|
+
removedColumns: removedColumnNames,
|
|
95
|
+
migrationName,
|
|
96
|
+
content,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
hasChanges: changes.length > 0,
|
|
103
|
+
changes,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* High-level makemigrations execution: diffs schema and writes migration files
|
|
109
|
+
* @param {Object} options
|
|
110
|
+
* @param {import('knex').Knex} options.knex - Knex instance
|
|
111
|
+
* @param {string} [options.modelsDir='./models'] - Models directory
|
|
112
|
+
* @param {string} [options.migrationDir='./migrations'] - Target migrations directory
|
|
113
|
+
* @param {boolean} [options.dryRun=false] - Preview without writing files
|
|
114
|
+
* @param {boolean} [options.includeDrops=false] - Include dropColumn operations
|
|
115
|
+
* @param {string} [options.name] - Custom migration name override
|
|
116
|
+
* @returns {Promise<{ hasChanges: boolean, created: Array<Object>, skipped: Array<Object> }>}
|
|
117
|
+
*/
|
|
118
|
+
async function makeMigrations(options = {}) {
|
|
119
|
+
const {
|
|
120
|
+
knex,
|
|
121
|
+
modelsDir = './models',
|
|
122
|
+
migrationDir = './migrations',
|
|
123
|
+
dryRun = false,
|
|
124
|
+
includeDrops = false,
|
|
125
|
+
name = null,
|
|
126
|
+
} = options;
|
|
127
|
+
|
|
128
|
+
let models;
|
|
129
|
+
if (fs.existsSync(path.resolve(process.cwd(), modelsDir))) {
|
|
130
|
+
models = loadModelsFromDirectory(modelsDir);
|
|
131
|
+
} else {
|
|
132
|
+
models = [...getAllModels().values()];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const { hasChanges, changes } = await diffSchema({
|
|
136
|
+
knex,
|
|
137
|
+
models,
|
|
138
|
+
includeDrops,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const created = [];
|
|
142
|
+
const skipped = [];
|
|
143
|
+
|
|
144
|
+
if (!hasChanges) {
|
|
145
|
+
return { hasChanges: false, created, skipped };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const absoluteMigrationDir = path.resolve(process.cwd(), migrationDir);
|
|
149
|
+
if (!dryRun && !fs.existsSync(absoluteMigrationDir)) {
|
|
150
|
+
fs.mkdirSync(absoluteMigrationDir, { recursive: true });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
changes.forEach((change, index) => {
|
|
154
|
+
const customName = name && changes.length === 1 ? name : change.migrationName;
|
|
155
|
+
const stamp = migrationTimestamp(new Date(Date.now() + index * 1000));
|
|
156
|
+
const filename = `${stamp}_${customName}.js`;
|
|
157
|
+
const filepath = path.join(absoluteMigrationDir, filename);
|
|
158
|
+
|
|
159
|
+
if (!dryRun) {
|
|
160
|
+
fs.writeFileSync(filepath, change.content, 'utf8');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
created.push({
|
|
164
|
+
model: change.model,
|
|
165
|
+
table: change.table,
|
|
166
|
+
type: change.type,
|
|
167
|
+
addedColumns: change.addedColumns,
|
|
168
|
+
removedColumns: change.removedColumns,
|
|
169
|
+
file: filename,
|
|
170
|
+
path: filepath,
|
|
171
|
+
content: change.content,
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
hasChanges: true,
|
|
177
|
+
created,
|
|
178
|
+
skipped,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = {
|
|
183
|
+
diffSchema,
|
|
184
|
+
makeMigrations,
|
|
185
|
+
};
|
|
@@ -167,11 +167,27 @@ function createMigrationManager(knex, config = {}) {
|
|
|
167
167
|
},
|
|
168
168
|
|
|
169
169
|
/**
|
|
170
|
-
*
|
|
171
|
-
* @
|
|
170
|
+
* Compare models with live database schema
|
|
171
|
+
* @param {Object} [options={}]
|
|
172
|
+
* @returns {Promise<{ hasChanges: boolean, changes: Array<Object> }>}
|
|
173
|
+
*/
|
|
174
|
+
async diff(options = {}) {
|
|
175
|
+
const { diffSchema } = require('./diff');
|
|
176
|
+
return diffSchema({ knex, ...options });
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Auto-generate migrations from schema diffs (Django-inspired makemigrations)
|
|
181
|
+
* @param {Object} [options={}]
|
|
182
|
+
* @returns {Promise<{ hasChanges: boolean, created: Array<Object>, skipped: Array<Object> }>}
|
|
172
183
|
*/
|
|
173
|
-
async
|
|
174
|
-
|
|
184
|
+
async makeMigrations(options = {}) {
|
|
185
|
+
const { makeMigrations } = require('./diff');
|
|
186
|
+
return makeMigrations({
|
|
187
|
+
knex,
|
|
188
|
+
migrationDir: directory,
|
|
189
|
+
...options,
|
|
190
|
+
});
|
|
175
191
|
},
|
|
176
192
|
};
|
|
177
193
|
}
|
|
@@ -201,5 +217,7 @@ exports.down = function(knex) {
|
|
|
201
217
|
module.exports = {
|
|
202
218
|
createMigrationManager,
|
|
203
219
|
getDefaultMigrationTemplate,
|
|
220
|
+
diffSchema: require('./diff').diffSchema,
|
|
221
|
+
makeMigrations: require('./diff').makeMigrations,
|
|
204
222
|
};
|
|
205
223
|
|
package/core/orm/model.js
CHANGED
|
@@ -138,6 +138,10 @@ function defineModel(options) {
|
|
|
138
138
|
bulkActions: Array.isArray(admin.bulkActions) ? admin.bulkActions : null,
|
|
139
139
|
hooks: typeof admin.hooks === 'object' && admin.hooks !== null ? admin.hooks : null,
|
|
140
140
|
relations: typeof admin.relations === 'object' && admin.relations !== null ? admin.relations : null,
|
|
141
|
+
inlines: Array.isArray(admin.inlines) ? admin.inlines : null,
|
|
142
|
+
listEditable: Array.isArray(admin.listEditable) ? admin.listEditable : null,
|
|
143
|
+
dateHierarchy: typeof admin.dateHierarchy === 'string' ? admin.dateHierarchy : null,
|
|
144
|
+
listFilter: Array.isArray(admin.listFilter) ? admin.listFilter : (Array.isArray(admin.filterable) ? admin.filterable : null),
|
|
141
145
|
},
|
|
142
146
|
rest: {
|
|
143
147
|
enabled: rest.enabled === true,
|