-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(cli): import LB3 models with a custom base class #4737
Changes from all commits
8420c68
15c8cc2
b9716d1
8c5f3cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,6 +48,10 @@ module.exports = class Lb3ModelImporter extends BaseGenerator { | |
this.destinationPath(), | ||
this.artifactInfo.outDir, | ||
); | ||
this.artifactInfo.modelDir = path.resolve( | ||
this.artifactInfo.rootDir, | ||
utils.modelsDir, | ||
); | ||
return super.setOptions(); | ||
} | ||
|
||
|
@@ -99,6 +103,14 @@ Learn more at https://loopback.io/doc/en/lb4/Importing-LB3-models.html | |
this.modelNames = answers.modelNames; | ||
} | ||
|
||
async loadExistingLb4Models() { | ||
debug(`model list dir ${this.artifactInfo.modelDir}`); | ||
this.existingLb4ModelNames = await utils.getArtifactList( | ||
this.artifactInfo.modelDir, | ||
'model', | ||
); | ||
} | ||
|
||
async migrateSelectedModels() { | ||
if (this.shouldExit()) return; | ||
this.modelFiles = []; | ||
|
@@ -136,6 +148,20 @@ Learn more at https://loopback.io/doc/en/lb4/Importing-LB3-models.html | |
); | ||
debug('LB4 model data', templateData); | ||
|
||
if (!templateData.isModelBaseBuiltin) { | ||
const baseName = templateData.modelBaseClass; | ||
if ( | ||
!this.existingLb4ModelNames.includes(baseName) && | ||
!this.modelNames.includes(baseName) | ||
) { | ||
this.log( | ||
'Adding %s (base of %s) to the list of imported models.', | ||
chalk.yellow(baseName), | ||
chalk.yellow(name), | ||
); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here is an example console output.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So just making sure I understand correctly, we're importing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, exactly 👍
Yes. Every LoopBack 3 model has the LB3 built-in |
||
this.modelNames.push(baseName); | ||
} | ||
} | ||
const fileName = utils.getModelFileName(name); | ||
const fullTargetPath = path.resolve(this.artifactInfo.relPath, fileName); | ||
debug('Model %s output file', name, fullTargetPath); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ const { | |
findBuiltinType, | ||
} = require('../model/property-definition'); | ||
const chalk = require('chalk'); | ||
const {isDeepStrictEqual} = require('util'); | ||
|
||
module.exports = { | ||
importLb3ModelDefinition, | ||
|
@@ -43,12 +44,29 @@ function importLb3ModelDefinition(modelCtor, log) { | |
|
||
logNamingIssues(modelName, log); | ||
|
||
const baseDefinition = modelCtor.base.definition; | ||
const baseProps = {...baseDefinition.properties}; | ||
|
||
// Core LB3 models like PersistedModel come with an id property that's | ||
// injected by juggler. We don't want to inherit that property, because | ||
// in LB4, we want models to define the id property explicitly. | ||
if (isCoreModel(modelCtor.base)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry I am a bit confused. Does this mean if a custom model inherits a base model, it won't have the id property after importing to LB4 app? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When scaffolding code for an imported model, we want to skip all properties inherited from the base model. For example, if Now juggler's As a result, when deciding which properties belong to the model being imported (e.g. |
||
delete baseProps.id; | ||
} | ||
|
||
const templateData = { | ||
name: modelName, | ||
className: pascalCase(modelName), | ||
...migrateBaseClass(modelCtor.settings.base), | ||
properties: migrateModelProperties(modelCtor.definition.properties), | ||
settings: migrateModelSettings(modelCtor.definition.settings, log), | ||
properties: migrateModelProperties( | ||
modelCtor.definition.properties, | ||
baseProps, | ||
), | ||
settings: migrateModelSettings( | ||
modelCtor.definition.settings, | ||
baseDefinition.settings, | ||
log, | ||
), | ||
}; | ||
|
||
const settings = templateData.settings; | ||
|
@@ -59,7 +77,7 @@ function importLb3ModelDefinition(modelCtor, log) { | |
return templateData; | ||
} | ||
|
||
function migrateModelProperties(properties) { | ||
function migrateModelProperties(properties = {}, inherited = {}) { | ||
const templateData = {}; | ||
|
||
// In LB 3.x, primary keys are typically contributed by connectors later in | ||
|
@@ -72,7 +90,16 @@ function migrateModelProperties(properties) { | |
}); | ||
|
||
for (const prop in properties) { | ||
const def = migratePropertyDefinition(properties[prop]); | ||
const propDef = properties[prop]; | ||
|
||
// Skip the property if it was inherited from the base model (the parent) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. reasonable to me 👍 |
||
const baseProp = inherited[prop]; | ||
if (baseProp && isDeepStrictEqual(propDef, baseProp)) { | ||
delete templateData[prop]; | ||
continue; | ||
} | ||
|
||
const def = migratePropertyDefinition(propDef); | ||
templateData[prop] = createPropertyTemplateData(def); | ||
} | ||
|
||
|
@@ -132,21 +159,30 @@ function migrateBaseClass(base) { | |
}; | ||
} | ||
|
||
// TODO: handle inheritance from application models | ||
throw new Error( | ||
'Models inheriting from app-specific models cannot be migrated yet. ' + | ||
`Base model configured: ${baseModelName}`, | ||
); | ||
return { | ||
modelBaseClass: baseModelName, | ||
isModelBaseBuiltin: false, | ||
}; | ||
} | ||
|
||
function migrateModelSettings(settings = {}, log) { | ||
function migrateModelSettings(settings = {}, inherited = {}, log) { | ||
// Shallow-clone the object to prevent modification of external data | ||
settings = {...settings}; | ||
|
||
// "strict" mode is enabled only when explicitly requested | ||
// LB3 models allow additional properties by default | ||
settings.strict = settings.strict === true; | ||
|
||
// Remove settings inherited from the base model | ||
for (const key in inherited) { | ||
// Always emit the value of 'strict' setting, make it explicit | ||
if (key === 'strict') continue; | ||
|
||
if (isDeepStrictEqual(settings[key], inherited[key])) { | ||
delete settings[key]; | ||
} | ||
} | ||
|
||
if (settings.forceId === 'auto') { | ||
// The value 'auto' is used when a parent model wants to let the child | ||
// model make the decision automatically, depending on whether the child | ||
|
@@ -204,3 +240,10 @@ function migrateModelSettings(settings = {}, log) { | |
|
||
return settings; | ||
} | ||
|
||
function isCoreModel(modelCtor) { | ||
const name = modelCtor.modelName; | ||
return ( | ||
name === 'Model' || name === 'PersistedModel' || name === 'KeyValueModel' | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,13 +11,7 @@ exports[`lb4 import-lb3-models imports CoffeeShop model from lb3-example app 1`] | |
import {Entity, model, property} from '@loopback/repository'; | ||
|
||
@model({ | ||
settings: { | ||
strict: false, | ||
forceId: false, | ||
replaceOnPUT: true, | ||
validateUpsert: true, | ||
idInjection: true | ||
} | ||
settings: {strict: false, forceId: false, validateUpsert: true, idInjection: true} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The following settings are no longer imported because they are inherited:
Please note that |
||
}) | ||
export class CoffeeShop extends Entity { | ||
@property({ | ||
|
@@ -62,3 +56,140 @@ exports[`lb4 import-lb3-models imports CoffeeShop model from lb3-example app 2`] | |
export * from './coffee-shop.model'; | ||
|
||
`; | ||
|
||
|
||
exports[`lb4 import-lb3-models imports a model inheriting from a custom base class 1`] = ` | ||
import {model, property} from '@loopback/repository'; | ||
import {UserBase} from '.'; | ||
|
||
@model({settings: {strict: false, customCustomerSetting: true}}) | ||
export class Customer extends UserBase { | ||
@property({ | ||
type: 'boolean', | ||
}) | ||
isPreferred?: boolean; | ||
|
||
// Define well-known properties here | ||
|
||
// Indexer property to allow additional data | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
[prop: string]: any; | ||
|
||
constructor(data?: Partial<Customer>) { | ||
super(data); | ||
} | ||
} | ||
|
||
export interface CustomerRelations { | ||
// describe navigational properties here | ||
} | ||
|
||
export type CustomerWithRelations = Customer & CustomerRelations; | ||
|
||
`; | ||
|
||
|
||
exports[`lb4 import-lb3-models imports a model inheriting from a custom base class 2`] = ` | ||
import {model, property} from '@loopback/repository'; | ||
import {User} from '.'; | ||
|
||
@model({settings: {strict: false, customUserBaseSetting: true}}) | ||
export class UserBase extends User { | ||
@property({ | ||
type: 'boolean', | ||
required: true, | ||
default: false, | ||
}) | ||
isAccountVerified: boolean; | ||
|
||
// Define well-known properties here | ||
|
||
// Indexer property to allow additional data | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
[prop: string]: any; | ||
|
||
constructor(data?: Partial<UserBase>) { | ||
super(data); | ||
} | ||
} | ||
|
||
export interface UserBaseRelations { | ||
// describe navigational properties here | ||
} | ||
|
||
export type UserBaseWithRelations = UserBase & UserBaseRelations; | ||
|
||
`; | ||
|
||
|
||
exports[`lb4 import-lb3-models imports a model inheriting from a custom base class 3`] = ` | ||
import {Entity, model, property} from '@loopback/repository'; | ||
|
||
@model({ | ||
settings: { | ||
strict: false, | ||
caseSensitiveEmail: true, | ||
hidden: ['password', 'verificationToken'], | ||
maxTTL: 31556926, | ||
ttl: 1209600 | ||
} | ||
}) | ||
export class User extends Entity { | ||
@property({ | ||
type: 'number', | ||
id: 1, | ||
generated: true, | ||
updateOnly: true, | ||
}) | ||
id?: number; | ||
|
||
@property({ | ||
type: 'string', | ||
}) | ||
realm?: string; | ||
|
||
@property({ | ||
type: 'string', | ||
}) | ||
username?: string; | ||
|
||
@property({ | ||
type: 'string', | ||
required: true, | ||
}) | ||
password: string; | ||
|
||
@property({ | ||
type: 'string', | ||
required: true, | ||
}) | ||
email: string; | ||
|
||
@property({ | ||
type: 'boolean', | ||
}) | ||
emailVerified?: boolean; | ||
|
||
@property({ | ||
type: 'string', | ||
}) | ||
verificationToken?: string; | ||
|
||
// Define well-known properties here | ||
|
||
// Indexer property to allow additional data | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
[prop: string]: any; | ||
|
||
constructor(data?: Partial<User>) { | ||
super(data); | ||
} | ||
} | ||
|
||
export interface UserRelations { | ||
// describe navigational properties here | ||
} | ||
|
||
export type UserWithRelations = User & UserRelations; | ||
|
||
`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just want to make sure that I understand it correctly, the reason we check the existence of the base model is because we need to import the custom base class and also it's parent class if that doesn't exist?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
Let's say the application has:
AuditedPersistedModel
which is adding auditing functionality for all CRUD operations provided byPersistedModel
,Product
,Category
andStore
, where all models are based onAuditedPersistedModel
.When we run import for the first time and choose only
Store
to import, we also need to importAuditedPersistedModel
to allow the new LB4 model to inherit from the correct base class (model).When we run import for the second time and choose to import
Product
andCategory
, we should not importAuditedPersistedModel
again, because that could overwrite bits imported manually in the previous step. Instead, LB4 CLI will skip importing this base model.