~joey
JavaScript developer
Enjoys writing userscripts for Greasemonkey,
shellscripts for Linux,
and dweets for Dwitter.
Favourite computer games: Homeworld, Unreal Tournament, Wipeout, Deus Ex
Dream: Save the environment
"A platform for building APIs and microservices in Node.js"
$ npm install -g @loopback/cli
$ ls -l "$(which lb4)"
$ lb4
? Project name: my-new-project
// Default scripts
"build": "lb-tsc",
"build:watch": "lb-tsc --watch",
"test": "lb-mocha --allow-console-logs \"dist/__tests__\"",
// Empty the dist folder (removes any files from other branches, or renamed files)
"build:watch": "npm run clean && lb-tsc --watch",
// I want the app to auto-restart when the source code changes
"start:watch": "nodemon -e '*' -w dist -d 2 -x 'node .'",
// I want the tests and linters to run when the source code changes
"test": "NODE_ENV=testing lb-mocha --allow-console-logs -b -t 16000 dist/__tests__",
"test:watch-lite": "nice -n 12 nodemon -e '*' -w dist -d 2
-x 'npm run test && npm run lint'",
nodemon \ # Watch for changes
-e '*' \ # to a file with any extension
-w dist \ # under the dist folder
-d 2 \ # wait two seconds
-x 'node .' # then execute/restart node
$ lb4 datasource
? Datasource name: db1
? Select the connector for db1: MongoDB (supported by StrongLoop)
? Connection String url to override other settings (eg: mongodb://username:password@hostname:port/database):
? host: localhost
? port: 27017
? user:
? password: [hidden]
? database: MyNewProject
create src/datasources/db-1.datasource.json
create src/datasources/db-1.datasource.ts
Do this once for each DB you have
$ lb4 model
? Model class name: Student
? Please select the model base class Entity (A persisted model with an ID)
? Allow additional (free-form) properties? No
Let's add a property to Student
? Enter the property name: id
? Property type: string
? Is name the ID property? Yes
? Is it required?: Yes
? Default value [leave blank for none]:
Let's add a property to Student
? Enter the property name: name
? Property type: string
? Is name the ID property? No
? Is it required?: Yes
? Default value [leave blank for none]:
Let's add another property to Student
? Enter the property name: dateOfBirth
...
create src/models/student.model.ts
update src/models/index.ts
Model Student was created in src/models/
import {
Entity, model, property
} from '@loopback/repository';
@model({ settings: {} })
export class Student extends Entity {
@property({
type: 'string',
id: true,
required: true,
})
id: string;
@property({
type: 'string',
required: true,
})
name: string;
@property({
type: 'date',
})
dateOfBirth?: Date;
constructor(data?: Partial<Student>) {
super(data);
}
}
export class Student extends Entity {
id: string;
name: string;
dateOfBirth?: Date;
}
@property({
type: 'date', // or number, boolean, string, object, buffer, ...
required: false, // must be passed on creation? (default false)
default: () => new Date(), // can be a runtime function or a static value
})
dateOfBirth: Date;
// Will this property always be set, or not? Recommend doing one of these:
//
// required: true,
//
// or
//
// default: ..., // not required for creation, but always set
//
// or
//
// dateOfBirth?: Date; // mark the property as optional, with a ?
//
TypeScript will warn you if you try to use an optional field.
That is helpful! It can prevent 'undefined' errors.
$ lb4 repository
? Please select the datasource Db_1Datasource
? Select the model(s) you want to generate a repository Student
? Please enter the name of the ID property for Student: id
create src/repositories/student.repository.ts
update src/repositories/index.ts
Repository Student was created in src/repositories/
Tells loopback where it can find a model (which datasource)
Do this once for each model
$ lb4 controller
? Controller class name: Student
? What kind of controller would you like to generate? REST Controller with CRUD functions
? What is the name of the model to use with this CRUD repository? Student
? What is the name of your CRUD repository? StudentRepository
? What is the type of your ID? string
? What is the base HTTP path name of the CRUD operations? /students
create src/controllers/student.controller.ts
update src/controllers/index.ts
Controller Student was created in src/controllers/
Generates standard REST routes for the given model
Do this once for each model you want to expose
{
"bracketSpacing": true,
"singleQuote": true,
"printWidth": 120, <---
"trailingComma": "all"
}
Fit more on one line
Other personal preferences
export class StudentController {
// ...
@post('/students', {
responses: {
'200': {
description: 'Student model instance',
content: {'application/json': {schema: {'x-ts-type': Student}}},
},
},
})
async create(@requestBody() student: Student): Promise<Student> {
return await this.studentRepository.create(student);
}
// ...
}
studentRepo.create(studentObj) // required fields must be provided
// Filter by age, sort, paginate, select fields
studentRepo.find({
where: {dateOfBirth: {gt: new Date(2000, 0, 1)}},
order: ['name ASC'],
skip: 30,
limit: 10,
fields: {name: true, grade: true},
});
studentRepo.findOne(filter) // returns null if not found
studentRepo.findById(id) // throws Error if not found
studentRepo.updateById(id, data)
studentRepo.updateAll(data, where, opts)
studentRepo.deleteById(id)
studentRepo.deleteAll(where, opts)
const student = await this.studentRepository.findOne({
where: {
name: { regexp: /^Aaron /i }
}
});
// TS2531: Object is possibly 'null'.
console.log('student.name:', student.name);
// But this works
console.log('student.name:', student && student.name);
// Less safe, but if you are 100% sure
console.log('student.name:', student!.name);
// Guard
if (!student) {
throw new HttpErrors.NotFound('Aaron is absent'); // or just return
}
// After the guard, TS knows student is defined
console.log('student.name:', student.name);
http://localhost:3000/explorer
$ npm i --save oasgraph-cli
$ npx oasgraph --cors http://localhost:3000/openapi.json
...
GraphQL accessible at: http://localhost:3001/graphql
# With CORS you can point GraphQL Playground at that URL
export class MySequence implements SequenceHandler {
constructor(
@inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
@inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
@inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
@inject(SequenceActions.SEND) public send: Send,
@inject(SequenceActions.REJECT) public reject: Reject,
) {}
async handle(context: RequestContext) {
try {
const {request, response} = context;
// Authentication will go here
const route = this.findRoute(request);
const args = await this.parseParams(request, route);
const result = await this.invoke(route, args);
this.send(response, result);
} catch (err) {
// We added some conditional logging here
this.reject(context, err);
}
}
}
export class WhoAmIController {
constructor(
// If you want access to this.user (provided by the auth provider) then you must inject it
@inject(AuthenticationBindings.CURRENT_USER) private user: UserProfile,
// or to allow some unauthenticated routes in this controller
@inject(AuthenticationBindings.CURRENT_USER, {optional: true}) private user: User,
) {}
@authenticate('basic') // Add this to every relevant route
@get('/whoami')
whoAmI(): string {
return this.user.id;
}
}
https://loopback.io/doc/en/lb4/Decorators_authenticate.html
https://loopback.io/doc/en/lb4/Loopback-component-authentication.html
https://github.com/strongloop/loopback-next/blob/master/packages/authentication
Our approach (OAuth2):
export class MyController {
// 1. Instead of this common pattern
private userRepository: UserRepository;
constructor(userRepository: UserRepository) {
this.userRepository = userRepository;
}
// 2. We can just write this
constructor(private userRepository: UserRepository) {
}
// That 'private' modifier in the arguments creates a this.userRepository
}
A TypeScript trick:
private / protected / public modifiers can be used to accept instance properties in the constructor
Loopback keeps a Context registry of all repos, controllers, services, ...
Use `@repository()` or `@inject()` in the constructor
It will load an instance of the other class from the app context
So one controller can call functions in another
// Automatic dependency injection
export class UserController {
constructor(
@inject('controllers.PasswordController') protected passwordController: PasswordController,
@repository(UserRepository) public userRepository: UserRepository,
// does the same as
@inject('repositories.UserRepository') public userRepository: UserRepository,
) {}
// this.passwordController.clearPassword();
}
// Manually add our own service to the context (do this in the app's constructor)
app.bind('services.OldDataCleanupService')
.toClass(OldDataCleanupService)
.inScope(BindingScope.SINGLETON);
// Manually get it out later when we want to use it
const service = app.get<OldDataCleanupService>('services.OldDataCleanupService')
export class MyNewProjectApplication extends BootMixin(
ServiceMixin(RepositoryMixin(RestApplication)),
) {
constructor(options: ApplicationConfig = {}) {
super(options);
// Set up the custom sequence
this.sequence(MySequence);
// === Add authentication code here === //
// Set up default home page
this.static('/', path.join(__dirname, '../../public'));
// Customize @loopback/rest-explorer configuration here
this.bind(RestExplorerBindings.CONFIG).to({
path: '/explorer',
});
this.component(RestExplorerComponent);
this.projectRoot = __dirname;
// Customize @loopback/boot Booter Conventions here
this.bootOptions = {
controllers: {
// Customize ControllerBooter Conventions here
dirs: ['controllers'],
extensions: ['.controller.js'],
nested: true,
},
};
}
// When start() runs, the Booting happens
}
You want some code which runs always in the background?
Make a class, use @inject as much as you like,
then bind it to the app with this pattern:
export class MyTestApplication extends BootMixin(ServiceMixin(RepositoryMixin(RestApplication))) {
constructor(options: ApplicationConfig = {}) {
super(options);
// ...
// Add our own service into the registry
this.bind('services.MyCleanupService')
.toClass(MyCleanupService)
.inScope(BindingScope.SINGLETON);
}
async start() {
await super.start();
const myCleanupService = await this.get<MyCleanupService>('services.MyCleanupService');
await myCleanupService.startCleaning();
}
async stop() {
await super.stop();
const myCleanupService = await this.get<MyCleanupService>('services.MyCleanupService');
await myCleanupService.destroy();
}
}
¯\_(ツ)_/¯
Sometimes you can make a third, separate controller for the common code, and both of the original troublemakers inject that one.
What if you want to call a controller from an external script?
Controllers with authentication explode if you try to use them outside of a RestServer!
But you can create a class with no rest dependencies, and add it to the server with `app.bind()`.
That class can be used without starting the server.
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"allowJs": true, // enable JS files
"declaration": false, // needed for allowJs
},
Beware: Untyped JS code will silently leak 'any' values into your app!
(That's bad for safety, we should lose confidence in the type checking.)
Goal: Convert JS to TS when adding it to the project,
or write a .d.ts definition file for each .js file
To use JavaScript files in the project
"compilerOptions": {
// Don't stop compilation, let my IDE warn me
"noUnusedLocals": false,
// Easier to start with, but not recommended
"noImplicitAny": false,
},
Being less strict, for easy introduction
"compilerOptions": {
// This still lets you use 'any', just not by accident!
"noImplicitAny": true,
// Some other recommendations I have seen
"strict": true
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
},
Being more strict, the goal
"linterOptions": {
"exclude": ["src/lib"],
"strictNullChecks": true
},
"rules": {
"await-promise": true,
"no-floating-promises": true,
"no-unused": false
}
Catches some common accidents with promises
No longer relevant. The community is upgrading to typescript-eslint.
And no-floating-promises is now enabled by default anyway.
module.exports = {
extends: '@loopback/eslint-config',
rules: {
// Less strict, don't block hot fixes, clean up later
"@typescript-eslint/no-unused-vars": 'warn',
// More strict (recommended)
// Prevents/discourages you from using 'any'
// You can disable the rule when really needed
"@typescript-eslint/no-explicit-any": 'error',
}
};
Quick fix (when you really want to use 'any'):
Use // tslint:disable-next-line:no-any
Use // eslint-disable-next-line @typescript-eslint/no-explicit-any
then come back and fix the types later.
(In other situations, // @ts-ignore can work similarly for the next line.)
Or instead of 'any', use 'unknown' and type guards. (E.g. error logging.)
These markers make loss of type safety explicit, clearly visible in code review.
describe('PingController', () => {
let app: FooApplication;
let client: Client;
before('setupApplication', async () => {
({app, client} = await setupApplication());
});
after(async () => {
await app.stop();
});
it('invokes GET /ping', async () => {
const res = await client.get('/ping?msg=world').expect(200);
expect(res.body).to.containEql({greeting: 'Hello from LoopBack'});
});
});
# Log everything
DEBUG="*" npm run test
# Example output (more colourful on console)
loopback:build my-project/tsconfig.json found +4ms
loopback:build Spawn node my-project/node_modules/typescript/lib/tsc.js -p tsconfig.json
--outDir dist --target es2017 +0ms
loopback:build my-project/node_modules/mocha/bin/mocha -b -t 16000 dist/test +0ms
loopback:build Spawn node my-project/node_modules/mocha/bin/mocha -b -t 16000 dist/test +7ms
mocha:suite bail true +1ms
loopback:metadata:decorator BindElementProvider.constructor[0]:
{"":[{"methodDescriptorOrParameterIndex":0,"bindingKey":
{"key":"rest.http.request.context"},"metadata":{"decorator":"@inject"}}]} +0ms
loopback:metadata:decorator FindRouteProvider.constructor[1]:
{"":[null,{"methodDescriptorOrParameterIndex":1,"bindingKey":
{"key":"rest.handler"},"metadata":{"decorator":"@inject"}}]} +4ms
loopback:connector:mongodb all User
{ where: { id: 'd186da4eb' }, limit: 1, offset: 0, skip: 0 }
# Log only some channels
DEBUG="loopback:connector:*" node .
// migrate.ts
await app.migrateSchema({existingSchema});
/**
* Update or recreate the database schema for all repositories.
*
* **WARNING**: By default, `migrateSchema()` will attempt to preserve data
* while updating the schema in your target database, but this is not
* guaranteed to be safe.
*
* Please check the documentation for your specific connector(s) for
* a detailed breakdown of behaviors for automigrate!
*
* @param options Migration options, e.g. whether to update tables
* preserving data or rebuild everything from scratch.
*/
migrateSchema(options?: SchemaMigrationOptions): Promise<void>;
You've seen all the generated code
Everything else is in the Loopback framework
If you are curious to see that code, you can find it here:
https://github.com/strongloop/loopback-next
I learned a lot from reading their code,
especially inside __tests__
studentRepo.find({
where: {dateOfBirth: {gt: new Date(1990, 0, 1)}},
order: ['name ASC'],
limit: 100,
fields: {name: true, grade: true},
});
Problem:
If you have a variable/property of type `any`
then TypeScript silently stops checking things!
But it's not clear when this is happening. False confidence.
Because you can import an `any` from another module.
Solution:
Put `strict: true` in your tsconfig.json
and `no-any` in your tslint.json
Pray that the types you import from libraries
do not use `any`.
// A function with an options argument
function showThing(opts: { size?: number, enabled?: boolean }) {
grow(opts.size);
}
// With destructuring, we have to write them again!
function showThing({ size, enabled }: { size?: number, enabled?: boolean }) {
grow(size);
}
// An interface clears up the arguments, but at what cost?
interface ShowThingOptions {
size?: number;
enabled?: boolean;
}
function showThing({ size, enabled }: ShowThingOptions) {
grow(size);
}
When deconstructing an object argument, we have to list the property names twice!
No remedy. Just live with it.
Problem: You know a variable/property is defined, but TypeScript might not believe so.
Solution: Use `variable!`
Solution: Sometimes a guard can work, sometimes not
// findOne might return null, if no matching record was found
const child = await this.userRepo.findOne({ parent: myid });
// If you are 100% sure that findOne will return a record, then use !
console.log('Age:', child!.age);
// Or use a guard (only works when child is an argument?)
if (!child) {
throw Error('Expected to find a child!');
}
console.log('Age:', child.age);
Problem: You know a variable is of type A.
But TypeScript still thinks it is type `A | B | C`
Solution: Use `foo as A`
// Users can buy books or pens from the store
export class Purchase extends Entity {
user: string;
productType: 'book' | 'pen'; // enum in the decorator
metaData: BookMetadata | PenMetadata;
}
export interface BookMetadata { title: string; hardCover: boolean; }
export interface PenMetadata { color: string; }
// Runtime. We know this user purchased a pen:
const purchase = (
await this.purchaseRepo.findOne({ user: userId, productType: 'pen' });
)!;
// TypeScript will complain that .color is not guaranteed inside metaData
const penColor = purchase.metaData.color;
// Make TypeScript happy by "casting"
const penColor = (<PenMetadata>purchase.metaData).color;
// Alternative syntax with `as` (recommended)
const penColor = (purchase.metaData as PenMetadata).color;
Problem: You know a variable is of type A.
But TypeScript thinks it is type F
Solution: Use `foo as unknown as A`
interface QuestionnaireAnswers {
coding: number;
javascript: number;
typescript: number;
restify: number;
loopback: number;
}
function getRating(answers: QuestionnaireAnswers, category: string): number {
// TypeScript says this lookup is not legal: answers does not offer free lookup
return answers[category];
// Temporarily change the type of answers, so the lookup can work
return (answers as unknown as Record<string, number>)[category];
}
export const UnstyledButton = styled.button<React.HTMLProps<HTMLButtonElement>>`
background-color: transparent;
border-radius: 0;
// ...
`;
// We made a module to reduce headaches
export type CommonHTMLProps =
React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
export type CommonDivProps =
React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
export type CommonInputProps =
React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
export type CommonButtonProps =
React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>;
export type CommonImageProps =
React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>;
React? You may need to learn some crazy React types!
// If we don't specify the fields then
// setState expects us to pass all of them!
this.setState({ [key]: value });
// We can tell it which field we are passing
this.setState<'count'>({ count: value });
// Solution: tell it we are guaranteeing nothing
this.setState<never>({ [key]: value });
setState wants you to tell it what fields you will pass
Slides:
https://slides.com/joeytwiddle/loopback4
Contact:
joeytwiddle @ Twitter, Dwitter, GitHub, GMail, ...