Add timestamps lifecycles subscriber

This commit is contained in:
Alexandre Bodin 2021-09-20 19:15:50 +02:00
parent 59eb3990dd
commit 82fe2a1b87
17 changed files with 399 additions and 202 deletions

View File

@ -1,49 +0,0 @@
'use strict';
const { prop, defaultsDeep } = require('lodash/fp');
/*
config/database.js
{
connector: '',
connection: {},
migration: {},
seed: {},
schema: {
autoSync: true,
forceSync: true
}
}
*/
class Configuration {
constructor(config) {
this.config = config;
}
static from(config) {
if (config instanceof Configuration) {
return config;
}
return new Configuration(defaultsDeep(config, Configuration.defaults));
}
get(path) {
return prop(path, this.config);
}
}
Configuration.defaults = {
connector: '@strapi/connector-sql',
migration: {
//
},
seed: {
//
},
models: [],
};
module.exports = Configuration;

View File

@ -95,9 +95,8 @@ const createRepository = (uid, db) => {
return db.entityManager.deleteRelations(uid, id);
},
// TODO: add relation API
populate(entity, field, params) {
return db.entityManager.populate(uid, entity, field, params);
populate(entity, populate) {
return db.entityManager.populate(uid, entity, populate);
},
load(entity, field, params) {

View File

@ -1,10 +1,9 @@
import { Attribute } from './schema';
interface Field {
config: {};
toDB(value: any): any;
fromDB(value: any): any;
}
interface Attribute {
type: string
}
export function createField(attribute: Attribute): Field;

View File

@ -1,3 +1,7 @@
import { LifecycleProvider } from './lifecycles';
import { MigrationProvider } from './migrations';
import { SchemaProvideer } from './schema';
type BooleanWhere<T> = {
$and?: WhereParams<T>[];
$or?: WhereParams<T>[];
@ -50,6 +54,43 @@ interface Pagination {
total: number;
}
interface PopulateParams {}
interface EntityManager {
findOne<K extends keyof AllTypes>(uid: K, params: FindParams<AllTypes[K]>): Promise<any>;
findMany<K extends keyof AllTypes>(uid: K, params: FindParams<AllTypes[K]>): Promise<any[]>;
create<K extends keyof AllTypes>(uid: K, params: CreateParams<AllTypes[K]>): Promise<any>;
createMany<K extends keyof AllTypes>(
uid: K,
params: CreateManyParams<AllTypes[K]>
): Promise<{ count: number }>;
update<K extends keyof AllTypes>(uid: K, params: any): Promise<any>;
updateMany<K extends keyof AllTypes>(uid: K, params: any): Promise<{ count: number }>;
delete<K extends keyof AllTypes>(uid: K, params: any): Promise<any>;
deleteMany<K extends keyof AllTypes>(uid: K, params: any): Promise<{ count: number }>;
count<K extends keyof AllTypes>(uid: K, params: any): Promise<number>;
attachRelations<K extends keyof AllTypes>(uid: K, id: ID, data: any): Promise<any>;
updateRelations<K extends keyof AllTypes>(uid: K, id: ID, data: any): Promise<any>;
deleteRelations<K extends keyof AllTypes>(uid: K, id: ID): Promise<any>;
populate<K extends keyof AllTypes, T extends AllTypes[K]>(
uid: K,
entity: T,
populate: PopulateParams
): Promise<T>;
load<K extends keyof AllTypes, T extends AllTypes[K], SK extends keyof T>(
uid: K,
entity: T,
field: SK,
populate: PopulateParams
): Promise<T[SK]>;
}
interface QueryFromContentType<T extends keyof AllTypes> {
findOne(params: FindParams<AllTypes[T]>): Promise<any>;
findMany(params: FindParams<AllTypes[T]>): Promise<any[]>;
@ -70,6 +111,14 @@ interface QueryFromContentType<T extends keyof AllTypes> {
attachRelations(id: ID, data: any): Promise<any>;
updateRelations(id: ID, data: any): Promise<any>;
deleteRelations(id: ID): Promise<any>;
populate<S extends AllTypes[T]>(entity: S, populate: PopulateParams): Promise<S>;
load<S extends AllTypes[T], K extends keyof S>(
entity: S,
field: K,
populate: PopulateParams
): Promise<S[K]>;
}
interface ModelConfig {
@ -83,15 +132,11 @@ interface DatabaseConfig {
connection: ConnectionConfig;
models: ModelConfig[];
}
interface DatabaseSchema {
sync(): Promise<void>;
reset(): Promise<void>;
create(): Promise<void>;
drop(): Promise<void>;
}
export interface Database {
schema: DatabaseSchema;
schema: SchemaProvideer;
lifecycles: LifecycleProvider;
migrations: MigrationProvider;
entityManager: EntityManager;
query<T extends keyof AllTypes>(uid: T): QueryFromContentType<T>;
}

View File

@ -4,10 +4,10 @@ const knex = require('knex');
const { getDialect } = require('./dialects');
const createSchemaProvider = require('./schema');
const createMigrationProvider = require('./migration');
const createMetadata = require('./metadata');
const { createEntityManager } = require('./entity-manager');
const { createLifecyclesManager } = require('./lifecycles');
const { createMigrationsProvider } = require('./migrations');
const { createLifecyclesProvider } = require('./lifecycles');
// TODO: move back into strapi
const { transformContentTypes } = require('./utils/content-types');
@ -26,9 +26,10 @@ class Database {
this.dialect.initialize();
this.schema = createSchemaProvider(this);
this.migration = createMigrationProvider(this);
this.lifecycles = createLifecyclesManager(this);
this.migrations = createMigrationsProvider(this);
this.lifecycles = createLifecyclesProvider(this);
this.entityManager = createEntityManager(this);
}

View File

@ -0,0 +1,50 @@
import { Database } from '../';
import { Model } from '../schema';
import { Subscriber } from './subscribers';
export type Action =
| 'beforeCreate'
| 'afterCreate'
| 'beforeFindOne'
| 'afterFindOne'
| 'beforeFindMany'
| 'afterFindMany'
| 'beforeCount'
| 'afterCount'
| 'beforeCreateMany'
| 'afterCreateMany'
| 'beforeUpdate'
| 'afterUpdate'
| 'beforeUpdateMany'
| 'afterUpdateMany'
| 'beforeDelete'
| 'afterDelete'
| 'beforeDeleteMany'
| 'afterDeleteMany';
export interface Params {
select?: any;
where?: any;
_q?: any;
orderBy?: any;
groupBy?: any;
offset?: any;
limit?: any;
populate?: any;
data?: any;
}
export interface Event {
action: Action;
model: Model;
params: Params;
}
export interface LifecycleProvider {
subscribe(subscriber: Subscriber): () => void;
clear(): void;
run(action: Action, uid: string, properties: any): Promise<void>;
createEvent(action: Action, uid: string, properties: any): Event;
}
export function createLifecyclesProvider(db: Database): LifecycleProvider;

View File

@ -2,22 +2,8 @@
const assert = require('assert').strict;
/**
* @typedef Event
* @property {string} action
* @property {Model} model
*/
/**
* For each model try to run it's lifecycles function if any is defined
* @param {Event} event
*/
const modelLifecyclesSubscriber = async event => {
const { model } = event;
if (event.action in model.lifecycles) {
await model.lifecycles[event.action](event);
}
};
const timestampsLifecyclesSubscriber = require('./subscribers/timestamps');
const modelLifecyclesSubscriber = require('./subscribers/models-lifecycles');
const isValidSubscriber = subscriber => {
return (
@ -25,10 +11,13 @@ const isValidSubscriber = subscriber => {
);
};
const createLifecyclesManager = db => {
let subscribers = [modelLifecyclesSubscriber];
/**
* @type {import('.').createLifecyclesProvider}
*/
const createLifecyclesProvider = db => {
let subscribers = [timestampsLifecyclesSubscriber, modelLifecyclesSubscriber];
const lifecycleManager = {
return {
subscribe(subscriber) {
assert(isValidSubscriber(subscriber), 'Invalid subscriber. Expected function or object');
@ -37,6 +26,10 @@ const createLifecyclesManager = db => {
return () => subscribers.splice(subscribers.indexOf(subscriber), 1);
},
clear() {
subscribers = [];
},
createEvent(action, uid, properties) {
const model = db.metadata.get(uid);
@ -65,15 +58,9 @@ const createLifecyclesManager = db => {
}
}
},
clear() {
subscribers = [];
},
};
return lifecycleManager;
};
module.exports = {
createLifecyclesManager,
createLifecyclesProvider,
};

View File

@ -0,0 +1,9 @@
import { Event, Action } from '../';
type SubscriberFn = (event: Event) => Promise<void> | void;
type SubscriberMap = {
[k in Action]: SubscriberFn;
};
export type Subscriber = SubscriberFn | SubscriberMap;

View File

@ -0,0 +1,19 @@
'use strict';
/**
* @typedef {import(".").Subscriber } Subscriber
*/
/**
* For each model try to run it's lifecycles function if any is defined
* @type {Subscriber}
*/
const modelsLifecyclesSubscriber = async event => {
const { model } = event;
if (event.action in model.lifecycles) {
await model.lifecycles[event.action](event);
}
};
module.exports = modelsLifecyclesSubscriber;

View File

@ -0,0 +1,65 @@
'use strict';
const _ = require('lodash');
/**
* @typedef {import(".").Subscriber } Subscriber
* @typedef { import("../").Event } Event
*/
// NOTE: we could add onCreate & onUpdate on field level to do this instead
/**
* @type {Subscriber}
*/
const timestampsLifecyclesSubscriber = {
/**
* Init created_at & updated_at before create
* @param {Event} event
*/
beforeCreate(event) {
const { data } = event.params;
const now = new Date();
_.defaults(data, { created_at: now, updated_at: now });
},
/**
* Init created_at & updated_at before create
* @param {Event} event
*/
beforeCreateMany(event) {
const { data } = event.params;
const now = new Date();
if (_.isArray(data)) {
data.forEach(data => _.defaults(data, { created_at: now, updated_at: now }));
}
},
/**
* Update updated_at before update
* @param {Event} event
*/
beforeUpdate(event) {
const { data } = event.params;
const now = new Date();
_.assign(data, { updated_at: now });
},
/**
* Update updated_at before update
* @param {Event} event
*/
beforeUpdateMany(event) {
const { data } = event.params;
const now = new Date();
if (_.isArray(data)) {
data.forEach(data => _.assign(data, { updated_at: now }));
}
},
};
module.exports = timestampsLifecyclesSubscriber;

View File

@ -1,105 +0,0 @@
'use strict';
const path = require('path');
const fse = require('fs-extra');
const Umzug = require('umzug');
class CustomStorage {
constructor(opts = {}) {
this.tableName = opts.tableName;
this.knex = opts.db.connection;
}
async logMigration(migrationName) {
await this.knex
.insert({
name: migrationName,
time: new Date(),
})
.into(this.tableName);
}
async unlogMigration(migrationName) {
await this.knex(this.tableName)
.del()
.where({ name: migrationName });
}
async executed() {
if (!(await this.hasMigrationTable())) {
await this.createMigrationTable();
return [];
}
const logs = await this.knex
.select()
.from(this.tableName)
.orderBy('time');
return logs.map(log => log.name);
}
hasMigrationTable() {
return this.knex.schema.hasTable(this.tableName);
}
createMigrationTable() {
return this.knex.schema.createTable(this.tableName, table => {
table.increments('id');
table.string('name');
table.datetime('time', { useTz: false });
});
}
}
const createMigrationProvider = db => {
const migrationDir = path.join(strapi.dir, 'database/migrations');
fse.ensureDirSync(migrationDir);
const migrations = new Umzug({
storage: new CustomStorage({ db, tableName: 'strapi_migrations' }),
migrations: {
path: migrationDir,
pattern: /\.(js|sql)$/,
params: [db],
wrap(fn) {
return db => db.connection.transaction(trx => Promise.resolve(fn(trx)));
},
customResolver(path) {
// if sql file run with knex raw
if (path.match(/\.sql$/)) {
const sql = fse.readFileSync(path, 'utf8');
return {
// TODO: check multiple commands in one sql statement
up: knex => knex.raw(sql),
down() {},
};
}
// NOTE: we can add some ts register if we want to handle ts migration files at some point
return require(path);
},
},
});
// TODO: add internal migrations for core & plugins
// How do we intersperse them
return {
async shouldRun() {
const pending = await migrations.pending();
return pending.length > 0;
},
async up() {
await migrations.up();
},
async down() {
await migrations.down();
},
};
};
module.exports = createMigrationProvider;

View File

@ -0,0 +1,9 @@
import { Database } from '../';
export interface MigrationProvider {
shouldRun(): Promise<boolean>;
up(): Promise<void>;
down(): Promise<void>;
}
export function createMigrationsProvider(db: Database): MigrationProvider;

View File

@ -0,0 +1,69 @@
'use strict';
const path = require('path');
const fse = require('fs-extra');
const Umzug = require('umzug');
const createStorage = require('./storage');
// TODO: check multiple commands in one sql statement
const migrationResolver = path => {
// if sql file run with knex raw
if (path.match(/\.sql$/)) {
const sql = fse.readFileSync(path, 'utf8');
return {
up: knex => knex.raw(sql),
down() {},
};
}
// NOTE: we can add some ts register if we want to handle ts migration files at some point
return require(path);
};
const createUmzugProvider = db => {
const migrationDir = path.join(strapi.dir, 'database/migrations');
fse.ensureDirSync(migrationDir);
const wrapFn = fn => db => db.connection.transaction(trx => Promise.resolve(fn(trx)));
const storage = createStorage({ db, tableName: 'strapi_migrations' });
return new Umzug({
storage,
migrations: {
path: migrationDir,
pattern: /\.(js|sql)$/,
params: [db],
wrap: wrapFn,
customResolver: migrationResolver,
},
});
};
// NOTE: when needed => add internal migrations for core & plugins. How do we overlap them with users migrations ?
/**
* Creates migrations provider
* @type {import('.').createMigrationsProvider}
*/
const createMigrationsProvider = db => {
const migrations = createUmzugProvider(db);
return {
async shouldRun() {
const pending = await migrations.pending();
return pending.length > 0;
},
async up() {
await migrations.up();
},
async down() {
await migrations.down();
},
};
};
module.exports = { createMigrationsProvider };

View File

@ -0,0 +1,49 @@
'use strict';
const createStorage = (opts = {}) => {
const tableName = opts.tableName || 'strapi_migrations';
const knex = opts.db.connection;
const hasMigrationTable = () => knex.schema.hasTable(tableName);
const createMigrationTable = () => {
return knex.schema.createTable(tableName, table => {
table.increments('id');
table.string('name');
table.datetime('time', { useTz: false });
});
};
return {
async logMigration(migrationName) {
await knex
.insert({
name: migrationName,
time: new Date(),
})
.into(tableName);
},
async unlogMigration(migrationName) {
await knex(tableName)
.del()
.where({ name: migrationName });
},
async executed() {
if (!(await hasMigrationTable())) {
await createMigrationTable();
return [];
}
const logs = await knex
.select()
.from(tableName)
.orderBy('time');
return logs.map(log => log.name);
},
};
};
module.exports = createStorage;

View File

@ -0,0 +1,49 @@
import { Database } from '../';
import { Action } from '../lifecycles';
type Type =
| 'string'
| 'text'
| 'richtext'
| 'json'
| 'enumeration'
| 'password'
| 'email'
| 'integer'
| 'biginteger'
| 'float'
| 'decimal'
| 'date'
| 'time'
| 'datetime'
| 'timestamp'
| 'boolean'
| 'relation';
export interface Attribute {
type: Type;
}
export interface Model {
uid: string;
tableName: string;
attributes: {
id: {
type: 'increments';
};
[k: string]: Attribute;
};
lifecycles?: {
[k in Action]: () => void;
};
}
export interface SchemaProvideer {
sync(): Promise<void>;
syncSchema(): Promise<void>;
reset(): Promise<void>;
create(): Promise<void>;
drop(): Promise<void>;
}
export default function(db: Database): SchemaProvideer;

View File

@ -7,6 +7,9 @@ const createSchemaDiff = require('./diff');
const createSchemaStorage = require('./storage');
const { metadataToSchema } = require('./schema');
/**
* @type {import('.').default}
*/
const createSchemaProvider = db => {
const schema = metadataToSchema(db.metadata);
@ -60,9 +63,9 @@ const createSchemaProvider = db => {
// TODO: support option to disable auto migration & run a CLI command instead to avoid doing it at startup
// TODO: Allow keeping extra indexes / extra tables / extra columns (globally or on a per table basis)
async sync() {
if (await db.migration.shouldRun()) {
if (await db.migrations.shouldRun()) {
debug('Found migrations to run');
await db.migration.up();
await db.migrations.up();
return this.syncSchema();
}

View File

@ -68,12 +68,10 @@ const createContentType = (uid, definition) => {
Object.assign(schema.attributes, {
[CREATED_AT_ATTRIBUTE]: {
type: 'datetime',
default: () => new Date(),
},
// TODO: handle on edit set to new date
[UPDATED_AT_ATTRIBUTE]: {
type: 'datetime',
default: () => new Date(),
},
});