Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@imqueue/sequelize",
"version": "3.1.1",
"version": "3.1.2",
"description": "Sequelize ORM refines for @imqueue",
"main": "index.js",
"scripts": {
Expand Down
47 changes: 38 additions & 9 deletions src/decorators/CreatedBy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,31 @@
* <support@imqueue.com> to get commercial licensing options.
*/
import { currentMetadata } from '@imqueue/rpc';
import { BeforeCreate } from 'sequelize-typescript';
import { BeforeBulkCreate, BeforeCreate } from 'sequelize-typescript';

// noinspection JSUnusedGlobalSymbols
/**
* Stamps the decorated column with the acting user id on INSERT, taken from the
* in-flight IMQ request metadata (`currentMetadata()?.userId`) — so the id never
* travels through method arguments and cannot be spoofed by a caller.
*
* A property decorator, reusable on any model field. It registers a per-field
* `beforeCreate` hook on the owning model. The hook is a no-op when there is no
* acting user (system / unattributed writes) and never overwrites a value the
* application set explicitly.
* A property decorator, reusable on any model field. The hook is a no-op when
* there is no acting user (system / unattributed writes) and never overwrites a
* value the application set explicitly.
*
* Two hooks are registered, because rows are inserted in two ways:
* - instance / single `create()` -> `beforeCreate` (receives the instance)
* - static `Model.bulkCreate(records, …)` -> `beforeBulkCreate` (receives the
* built instances; a plain `bulkCreate` does not fire `beforeCreate`, so the
* per-instance hook alone would be bypassed). Sequelize filters the written
* columns down to `options.fields` when the caller supplies it, so an injected
* field is dropped unless also added there.
*
* Mechanism: a property decorator receives the prototype, but Sequelize hook
* decorators must target a static method on the constructor, so a generated
* static method is attached to `target.constructor` and registered through the
* public `@BeforeCreate`. Hooks are installed by sequelize-typescript's
* `installHooks` during `Sequelize#addModels`.
* decorators must target a static method on the constructor, so generated static
* methods are attached to `target.constructor` and registered through the public
* `@BeforeCreate` / `@BeforeBulkCreate`. Hooks are installed by
* sequelize-typescript's `installHooks` during `Sequelize#addModels`.
*
* @param {any} target - the decorated model's prototype
* @param {string} propertyName - the decorated column property name
Expand All @@ -48,6 +55,7 @@ import { BeforeCreate } from 'sequelize-typescript';
export function CreatedBy(target: any, propertyName: string): void {
const ctor = target.constructor;
const hookName = `__stampCreatedBy$${propertyName}`;
const bulkHook = `__stampCreatedByOnBulkCreate$${propertyName}`;

if (ctor[hookName]) {
return;
Expand All @@ -61,5 +69,26 @@ export function CreatedBy(target: any, propertyName: string): void {
}
};

ctor[bulkHook] = function (instances: any[], options: any): void {
const userId = currentMetadata()?.userId;

if (userId == null || !Array.isArray(instances)) {
return;
}

for (const instance of instances) {
if (instance && instance[propertyName] == null) {
instance[propertyName] = userId;
}
}

if (options && Array.isArray(options.fields)
&& !options.fields.includes(propertyName)
) {
options.fields.push(propertyName);
}
};

BeforeCreate(ctor, hookName);
BeforeBulkCreate(ctor, bulkHook);
}
30 changes: 28 additions & 2 deletions src/decorators/UpdatedBy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/
import { currentMetadata } from '@imqueue/rpc';
import {
BeforeBulkCreate,
BeforeBulkUpdate,
BeforeCreate,
BeforeUpdate,
Expand All @@ -39,8 +40,11 @@ import {
* - on UPDATE it always overwrites with the current actor — "last modified by"
* must reflect who actually ran the update and not be caller-spoofable.
*
* Three hooks are registered, because models are written in three ways:
* - INSERT -> `beforeCreate` (set-if-empty)
* Four hooks are registered, because models are written in four ways:
* - single INSERT -> `beforeCreate` (set-if-empty)
* - `Model.bulkCreate(records, …)` -> `beforeBulkCreate` (set-if-empty on the
* built instances; a plain `bulkCreate` does not fire `beforeCreate`, so the
* per-instance hook alone would be bypassed)
* - instance `save()` / `update()` -> `beforeUpdate` (receives the instance)
* - static `Model.update(values, …)` -> `beforeBulkUpdate` (receives options;
* the values to write live on `options.attributes`, and Sequelize filters the
Expand All @@ -56,6 +60,7 @@ import {
export function UpdatedBy(target: any, propertyName: string): void {
const ctor = target.constructor;
const createHook = `__stampUpdatedByOnCreate$${propertyName}`;
const bulkCreateHook = `__stampUpdatedByOnBulkCreate$${propertyName}`;
const singleHook = `__stampUpdatedBy$${propertyName}`;
const bulkHook = `__stampBulkUpdatedBy$${propertyName}`;

Expand All @@ -71,6 +76,26 @@ export function UpdatedBy(target: any, propertyName: string): void {
}
};

ctor[bulkCreateHook] = function (instances: any[], options: any): void {
const userId = currentMetadata()?.userId;

if (userId == null || !Array.isArray(instances)) {
return;
}

for (const instance of instances) {
if (instance && instance[propertyName] == null) {
instance[propertyName] = userId;
}
}

if (options && Array.isArray(options.fields)
&& !options.fields.includes(propertyName)
) {
options.fields.push(propertyName);
}
};

ctor[singleHook] = function (instance: any): void {
const userId = currentMetadata()?.userId;

Expand All @@ -96,6 +121,7 @@ export function UpdatedBy(target: any, propertyName: string): void {
};

BeforeCreate(ctor, createHook);
BeforeBulkCreate(ctor, bulkCreateHook);
BeforeUpdate(ctor, singleHook);
BeforeBulkUpdate(ctor, bulkHook);
}
Loading