@imqueue/pg-sequelize 4.2.0
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/CHANGELOG.md +50 -0
- package/CONTRIBUTING.md +58 -0
- package/CONTRIBUTION-TERMS.md +79 -0
- package/LICENSE +585 -0
- package/README.md +94 -0
- package/SECURITY.md +41 -0
- package/index.d.ts +86 -0
- package/index.js +87 -0
- package/package.json +75 -0
- package/src/BaseModel.d.ts +695 -0
- package/src/BaseModel.js +917 -0
- package/src/Graph.d.ts +215 -0
- package/src/Graph.js +257 -0
- package/src/decorators/AssociatedWith.d.ts +94 -0
- package/src/decorators/AssociatedWith.js +71 -0
- package/src/decorators/ColumnIndex.d.ts +206 -0
- package/src/decorators/ColumnIndex.js +98 -0
- package/src/decorators/CreatedBy.d.ts +27 -0
- package/src/decorators/CreatedBy.js +84 -0
- package/src/decorators/DeletedBy.d.ts +30 -0
- package/src/decorators/DeletedBy.js +89 -0
- package/src/decorators/DynamicView.d.ts +124 -0
- package/src/decorators/DynamicView.js +113 -0
- package/src/decorators/Emittable.d.ts +39 -0
- package/src/decorators/Emittable.js +42 -0
- package/src/decorators/NullableIndex.d.ts +77 -0
- package/src/decorators/NullableIndex.js +64 -0
- package/src/decorators/UpdatedBy.d.ts +27 -0
- package/src/decorators/UpdatedBy.js +105 -0
- package/src/decorators/View.d.ts +87 -0
- package/src/decorators/View.js +93 -0
- package/src/decorators/index.d.ts +32 -0
- package/src/decorators/index.js +33 -0
- package/src/helpers/index.d.ts +24 -0
- package/src/helpers/index.js +25 -0
- package/src/helpers/js.d.ts +61 -0
- package/src/helpers/js.js +88 -0
- package/src/helpers/query.d.ts +445 -0
- package/src/helpers/query.js +1095 -0
- package/src/index.d.ts +162 -0
- package/src/index.js +223 -0
- package/src/types/DataPage.d.ts +52 -0
- package/src/types/DataPage.js +2 -0
- package/src/types/FieldsInput.d.ts +41 -0
- package/src/types/FieldsInput.js +75 -0
- package/src/types/FilterInput.d.ts +136 -0
- package/src/types/FilterInput.js +291 -0
- package/src/types/JsonObject.d.ts +16 -0
- package/src/types/JsonObject.js +50 -0
- package/src/types/OrderByInput.d.ts +45 -0
- package/src/types/OrderByInput.js +80 -0
- package/src/types/PaginationInput.d.ts +44 -0
- package/src/types/PaginationInput.js +90 -0
- package/src/types/index.d.ts +30 -0
- package/src/types/index.js +31 -0
- package/src/types/ranges/DateRange.d.ts +27 -0
- package/src/types/ranges/DateRange.js +69 -0
- package/src/types/ranges/IRange.d.ts +47 -0
- package/src/types/ranges/IRange.js +2 -0
- package/src/types/ranges/NumericRange.d.ts +19 -0
- package/src/types/ranges/NumericRange.js +61 -0
- package/src/types/ranges/index.d.ts +26 -0
- package/src/types/ranges/index.js +27 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* I'm Queue Software Project
|
|
3
|
+
* Copyright (C) 2025 imqueue.com <support@imqueue.com>
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU General Public License
|
|
16
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*
|
|
18
|
+
* If you want to use this code in a closed source (commercial) project, you can
|
|
19
|
+
* purchase a proprietary commercial license. Please contact us at
|
|
20
|
+
* <support@imqueue.com> to get commercial licensing options.
|
|
21
|
+
*/
|
|
22
|
+
import 'reflect-metadata';
|
|
23
|
+
import { addOptions, setModelName } from 'sequelize-typescript';
|
|
24
|
+
import {} from './View.js';
|
|
25
|
+
/**
|
|
26
|
+
* The placeholder pattern as a source string: `@{name}`, where the name is letters,
|
|
27
|
+
* digits and underscores.
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* Exported so a service can compile its own matcher — to validate a definition of its
|
|
31
|
+
* own, say — rather than hard-coding the syntax a second time.
|
|
32
|
+
*/
|
|
33
|
+
export const MATCHER = '@\\{([a-z0-9_]+?)\\}';
|
|
34
|
+
/** {@link MATCHER} compiled to find every placeholder in a definition. */
|
|
35
|
+
export const RX_MATCHER = new RegExp(MATCHER, 'gi');
|
|
36
|
+
/** {@link MATCHER} compiled to pull the name out of a single placeholder. */
|
|
37
|
+
export const RX_NAME_MATCHER = new RegExp(MATCHER, 'i');
|
|
38
|
+
/**
|
|
39
|
+
* Declares a model to be a view whose definition is parameterised per query.
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* Everything `View` does, plus placeholders. The definition may carry `@{name}`
|
|
43
|
+
* markers, every finder accepts `viewParams` to fill them, and the select-query
|
|
44
|
+
* generator substitutes them and splices the resulting statement into the query — as
|
|
45
|
+
* the `FROM` target, or as a joined subquery when the view is reached through an
|
|
46
|
+
* `include`. One model then serves a family of views that differ only by a constant,
|
|
47
|
+
* which is the alternative to defining one view per variant in a migration.
|
|
48
|
+
*
|
|
49
|
+
* Every placeholder must have a default in `viewParams`. That is checked while the
|
|
50
|
+
* class is being defined, so a missing one is an error at import rather than a
|
|
51
|
+
* malformed statement at query time.
|
|
52
|
+
*
|
|
53
|
+
* Values are escaped, so a parameter can carry a caller's input. Anything that is not
|
|
54
|
+
* a number or a string becomes `NULL`.
|
|
55
|
+
*
|
|
56
|
+
* @param options - Model options carrying the definition and the parameter defaults.
|
|
57
|
+
* @returns A class decorator.
|
|
58
|
+
* @throws TypeError when the definition is missing or blank, or when it names a
|
|
59
|
+
* placeholder that `viewParams` does not.
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* @DynamicView({
|
|
63
|
+
* viewDefinition: `
|
|
64
|
+
* CREATE OR REPLACE VIEW "ProductRevenue" AS
|
|
65
|
+
* SELECT "productId" AS "id", SUM("payment") AS "revenue"
|
|
66
|
+
* FROM "Order"
|
|
67
|
+
* WHERE "currency" = @{currency}
|
|
68
|
+
* GROUP BY "productId"
|
|
69
|
+
* `,
|
|
70
|
+
* viewParams: { currency: 'USD' },
|
|
71
|
+
* freezeTableName: true,
|
|
72
|
+
* timestamps: false,
|
|
73
|
+
* })
|
|
74
|
+
* export class ProductRevenue extends BaseModel<ProductRevenue> {
|
|
75
|
+
* @PrimaryKey
|
|
76
|
+
* @Column(DataType.BIGINT)
|
|
77
|
+
* declare public id: number;
|
|
78
|
+
* }
|
|
79
|
+
*
|
|
80
|
+
* // the same model, read in another currency
|
|
81
|
+
* const rows = await ProductRevenue.findAll({
|
|
82
|
+
* viewParams: { currency: 'EUR' },
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export function DynamicView(options) {
|
|
87
|
+
if (!options || !options.viewDefinition || !options.viewDefinition.trim()) {
|
|
88
|
+
throw new TypeError('View definition is missing!');
|
|
89
|
+
}
|
|
90
|
+
// we are dynamic, no choice here!
|
|
91
|
+
options.isDynamicView = true;
|
|
92
|
+
const viewDef = options.viewDefinition || '';
|
|
93
|
+
const viewParams = options.viewParams || {};
|
|
94
|
+
(viewDef.match(RX_MATCHER) || []).forEach(param => {
|
|
95
|
+
const [, name] = param.match(RX_NAME_MATCHER) || ['', ''];
|
|
96
|
+
if (typeof viewParams[name] !== 'string') {
|
|
97
|
+
throw new TypeError(`View definition contains param '${name}', but it was not provided`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return (target) => annotate(target, options);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Registers the model under its own name and records the view options on it.
|
|
104
|
+
*
|
|
105
|
+
* @param target - Model class being decorated.
|
|
106
|
+
* @param options - View definition options, marked as a view in place.
|
|
107
|
+
*/
|
|
108
|
+
function annotate(target, options) {
|
|
109
|
+
Object.assign(options, { treatAsView: true });
|
|
110
|
+
setModelName(target.prototype, options.modelName || target.name);
|
|
111
|
+
addOptions(target.prototype, options);
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=DynamicView.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @imqueue/pg-sequelize - Sequelize ORM refines for @imqueue
|
|
3
|
+
*
|
|
4
|
+
* I'm Queue Software Project
|
|
5
|
+
* Copyright (C) 2025 imqueue.com <support@imqueue.com>
|
|
6
|
+
*
|
|
7
|
+
* This program is free software: you can redistribute it and/or modify
|
|
8
|
+
* it under the terms of the GNU General Public License as published by
|
|
9
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
* (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* This program is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
* GNU General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* You should have received a copy of the GNU General Public License
|
|
18
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
*
|
|
20
|
+
* If you want to use this code in a closed source (commercial) project, you can
|
|
21
|
+
* purchase a proprietary commercial license. Please contact us at
|
|
22
|
+
* <support@imqueue.com> to get commercial licensing options.
|
|
23
|
+
*/
|
|
24
|
+
import 'reflect-metadata';
|
|
25
|
+
/**
|
|
26
|
+
* Placeholder for change notifications, which are not implemented.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* The intent was to attach a trigger that publishes row changes through Postgres
|
|
30
|
+
* `NOTIFY`. The body is empty: applying this to a model does nothing whatever, and
|
|
31
|
+
* nothing about a model changes by carrying it. It is unfinished work that happens to
|
|
32
|
+
* be exported, not a switch that is off.
|
|
33
|
+
*
|
|
34
|
+
* What exists today: `@imqueue/pg-pubsub` is the `LISTEN`/`NOTIFY` client, and
|
|
35
|
+
* `@imqueue/pg-prisma` ships the change-notify triggers for the Prisma stack.
|
|
36
|
+
*
|
|
37
|
+
* @param _target - The model class, which is ignored.
|
|
38
|
+
*/
|
|
39
|
+
export declare function Emittable(_target: any): void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @imqueue/pg-sequelize - Sequelize ORM refines for @imqueue
|
|
3
|
+
*
|
|
4
|
+
* I'm Queue Software Project
|
|
5
|
+
* Copyright (C) 2025 imqueue.com <support@imqueue.com>
|
|
6
|
+
*
|
|
7
|
+
* This program is free software: you can redistribute it and/or modify
|
|
8
|
+
* it under the terms of the GNU General Public License as published by
|
|
9
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
* (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* This program is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
* GNU General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* You should have received a copy of the GNU General Public License
|
|
18
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
*
|
|
20
|
+
* If you want to use this code in a closed source (commercial) project, you can
|
|
21
|
+
* purchase a proprietary commercial license. Please contact us at
|
|
22
|
+
* <support@imqueue.com> to get commercial licensing options.
|
|
23
|
+
*/
|
|
24
|
+
import 'reflect-metadata';
|
|
25
|
+
/**
|
|
26
|
+
* Placeholder for change notifications, which are not implemented.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* The intent was to attach a trigger that publishes row changes through Postgres
|
|
30
|
+
* `NOTIFY`. The body is empty: applying this to a model does nothing whatever, and
|
|
31
|
+
* nothing about a model changes by carrying it. It is unfinished work that happens to
|
|
32
|
+
* be exported, not a switch that is off.
|
|
33
|
+
*
|
|
34
|
+
* What exists today: `@imqueue/pg-pubsub` is the `LISTEN`/`NOTIFY` client, and
|
|
35
|
+
* `@imqueue/pg-prisma` ships the change-notify triggers for the Prisma stack.
|
|
36
|
+
*
|
|
37
|
+
* @param _target - The model class, which is ignored.
|
|
38
|
+
*/
|
|
39
|
+
export function Emittable(_target) {
|
|
40
|
+
// todo: implement
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=Emittable.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @imqueue/pg-sequelize - Sequelize ORM refines for @imqueue
|
|
3
|
+
*
|
|
4
|
+
* I'm Queue Software Project
|
|
5
|
+
* Copyright (C) 2025 imqueue.com <support@imqueue.com>
|
|
6
|
+
*
|
|
7
|
+
* This program is free software: you can redistribute it and/or modify
|
|
8
|
+
* it under the terms of the GNU General Public License as published by
|
|
9
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
* (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* This program is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
* GNU General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* You should have received a copy of the GNU General Public License
|
|
18
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
*
|
|
20
|
+
* If you want to use this code in a closed source (commercial) project, you can
|
|
21
|
+
* purchase a proprietary commercial license. Please contact us at
|
|
22
|
+
* <support@imqueue.com> to get commercial licensing options.
|
|
23
|
+
*/
|
|
24
|
+
import { type ColumnIndexOptions, type FunctionType } from './ColumnIndex.js';
|
|
25
|
+
/**
|
|
26
|
+
* What {@link (NullableIndex:1) | NullableIndex} accepts: every {@link ColumnIndexOptions} field except
|
|
27
|
+
* `expression`, which it sets itself.
|
|
28
|
+
*/
|
|
29
|
+
export type NullableColumnIndexOptions = Omit<ColumnIndexOptions, 'expression'>;
|
|
30
|
+
/**
|
|
31
|
+
* Declares a pair of partial indices on a nullable column, one for the rows where it
|
|
32
|
+
* is null and one for the rows where it is not.
|
|
33
|
+
*
|
|
34
|
+
* @remarks
|
|
35
|
+
* Two narrow indices instead of one wide one. Each covers only its half of the table,
|
|
36
|
+
* so `WHERE "col" IS NULL` and `WHERE "col" IS NOT NULL` each get an index sized to
|
|
37
|
+
* the rows they actually match — which is the point on a column where one of the two
|
|
38
|
+
* halves is much smaller than the other.
|
|
39
|
+
*
|
|
40
|
+
* Usable bare or as a factory, exactly like {@link (ColumnIndex:1) | ColumnIndex}. The factory form used
|
|
41
|
+
* to build both halves from one options object that it mutated in place, and
|
|
42
|
+
* {@link (ColumnIndex:1) | ColumnIndex} stores that object by reference — so both declarations ended up
|
|
43
|
+
* pointing at the same object, both saying `IS NOT NULL`, and neither carrying a
|
|
44
|
+
* predicate at all. What you got was two identical non-partial indices. It now builds
|
|
45
|
+
* a fresh options object per half, keeps a predicate of your own by combining it with
|
|
46
|
+
* the null test, and suffixes a name of your own so the two halves cannot collide.
|
|
47
|
+
*
|
|
48
|
+
* @param args - Nothing, when used bare. The options, when used as a factory.
|
|
49
|
+
* @returns A property decorator, when used as a factory.
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* @Table
|
|
53
|
+
* export class Lead extends BaseModel<Lead> {
|
|
54
|
+
* @NullableIndex
|
|
55
|
+
* @Column(DataType.DATE)
|
|
56
|
+
* public closedAt: Date;
|
|
57
|
+
*
|
|
58
|
+
* @NullableIndex({ name: 'lead_assignee', concurrently: true })
|
|
59
|
+
* @Column(DataType.BIGINT)
|
|
60
|
+
* public assigneeId: number;
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare function NullableIndex(options: Partial<NullableColumnIndexOptions>): FunctionType;
|
|
65
|
+
/**
|
|
66
|
+
* Declares the pair of partial indices with no options of their own.
|
|
67
|
+
*
|
|
68
|
+
* @remarks
|
|
69
|
+
* The bare form. Each half is a partial index keyed on the null test itself, so it
|
|
70
|
+
* serves `IS NULL` and `IS NOT NULL` lookups; use the factory form to add a method,
|
|
71
|
+
* a name, or a predicate of your own.
|
|
72
|
+
*
|
|
73
|
+
* @param target - Model prototype the property belongs to.
|
|
74
|
+
* @param propertyName - Nullable column to split.
|
|
75
|
+
* @param propertyDescriptor - Unused; present because a decorator receives it.
|
|
76
|
+
*/
|
|
77
|
+
export declare function NullableIndex(target: any, propertyName: string, propertyDescriptor?: PropertyDescriptor): void;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @imqueue/pg-sequelize - Sequelize ORM refines for @imqueue
|
|
3
|
+
*
|
|
4
|
+
* I'm Queue Software Project
|
|
5
|
+
* Copyright (C) 2025 imqueue.com <support@imqueue.com>
|
|
6
|
+
*
|
|
7
|
+
* This program is free software: you can redistribute it and/or modify
|
|
8
|
+
* it under the terms of the GNU General Public License as published by
|
|
9
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
* (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* This program is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
* GNU General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* You should have received a copy of the GNU General Public License
|
|
18
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
*
|
|
20
|
+
* If you want to use this code in a closed source (commercial) project, you can
|
|
21
|
+
* purchase a proprietary commercial license. Please contact us at
|
|
22
|
+
* <support@imqueue.com> to get commercial licensing options.
|
|
23
|
+
*/
|
|
24
|
+
import { ColumnIndex, } from './ColumnIndex.js';
|
|
25
|
+
export function NullableIndex(...args) {
|
|
26
|
+
if (args.length >= 2) {
|
|
27
|
+
const [target, propertyName, propertyDescriptor] = args;
|
|
28
|
+
annotate(target, propertyName, propertyDescriptor);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
return (target, propertyName, propertyDescriptor) => {
|
|
32
|
+
annotate(target, propertyName, propertyDescriptor, args[0]);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Declares the two halves of the pair.
|
|
37
|
+
*
|
|
38
|
+
* @param target - Model prototype the property belongs to.
|
|
39
|
+
* @param propertyName - Column the pair is declared on.
|
|
40
|
+
* @param propertyDescriptor - Passed through to {@link (ColumnIndex:1) | ColumnIndex}.
|
|
41
|
+
* @param options - Options to apply to both halves.
|
|
42
|
+
*/
|
|
43
|
+
function annotate(target, propertyName, propertyDescriptor, options = {}) {
|
|
44
|
+
for (const isNull of [true, false]) {
|
|
45
|
+
const test = `"${propertyName}" IS ${isNull ? '' : 'NOT '}NULL`;
|
|
46
|
+
// A fresh object per half: ColumnIndex keeps what it is given by
|
|
47
|
+
// reference, so two halves sharing one object are two halves with
|
|
48
|
+
// whatever the second one wrote.
|
|
49
|
+
const half = {
|
|
50
|
+
...options,
|
|
51
|
+
expression: test,
|
|
52
|
+
// A predicate of the caller's own is kept rather than replaced —
|
|
53
|
+
// splitting the rows is what this adds, not what it overrides.
|
|
54
|
+
predicate: options.predicate
|
|
55
|
+
? `(${options.predicate}) AND ${test}`
|
|
56
|
+
: test,
|
|
57
|
+
};
|
|
58
|
+
if (options.name) {
|
|
59
|
+
half.name = `${options.name}${isNull ? '' : '_not'}_null`;
|
|
60
|
+
}
|
|
61
|
+
ColumnIndex(half)(target, propertyName, propertyDescriptor);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=NullableIndex.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stamps the decorated column with the acting user id on INSERT and UPDATE,
|
|
3
|
+
* taken from the in-flight IMQ request metadata (`currentMetadata()?.userId`).
|
|
4
|
+
*
|
|
5
|
+
* A property decorator, reusable on any model field. It mirrors `@UpdatedAt`:
|
|
6
|
+
* - on INSERT it is set to the creating actor (just as `updatedAt` is set equal
|
|
7
|
+
* to `createdAt` on insert), without overwriting an explicit value;
|
|
8
|
+
* - on UPDATE it always overwrites with the current actor — "last modified by"
|
|
9
|
+
* must reflect who actually ran the update and not be caller-spoofable.
|
|
10
|
+
*
|
|
11
|
+
* Four hooks are registered, because models are written in four ways:
|
|
12
|
+
* - single INSERT → `beforeCreate` (set-if-empty)
|
|
13
|
+
* - `Model.bulkCreate(records, …)` → `beforeBulkCreate` (set-if-empty on the
|
|
14
|
+
* built instances; a plain `bulkCreate` does not fire `beforeCreate`, so the
|
|
15
|
+
* per-instance hook alone would be bypassed)
|
|
16
|
+
* - instance `save()` / `update()` → `beforeUpdate` (receives the instance)
|
|
17
|
+
* - static `Model.update(values, …)` → `beforeBulkUpdate` (receives options;
|
|
18
|
+
* the values to write live on `options.attributes`, and Sequelize filters the
|
|
19
|
+
* written columns down to `options.fields`, computed before this hook from the
|
|
20
|
+
* caller's values — so an injected field is dropped unless also added there).
|
|
21
|
+
*
|
|
22
|
+
* No-op when there is no acting user (system / unattributed writes).
|
|
23
|
+
*
|
|
24
|
+
* @param target - the decorated model's prototype
|
|
25
|
+
* @param propertyName - the decorated column property name
|
|
26
|
+
*/
|
|
27
|
+
export declare function UpdatedBy(target: any, propertyName: string): void;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @imqueue/pg-sequelize - Sequelize ORM refines for @imqueue
|
|
3
|
+
*
|
|
4
|
+
* I'm Queue Software Project
|
|
5
|
+
* Copyright (C) 2025 imqueue.com <support@imqueue.com>
|
|
6
|
+
*
|
|
7
|
+
* This program is free software: you can redistribute it and/or modify
|
|
8
|
+
* it under the terms of the GNU General Public License as published by
|
|
9
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
* (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* This program is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
* GNU General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* You should have received a copy of the GNU General Public License
|
|
18
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
*
|
|
20
|
+
* If you want to use this code in a closed source (commercial) project, you can
|
|
21
|
+
* purchase a proprietary commercial license. Please contact us at
|
|
22
|
+
* <support@imqueue.com> to get commercial licensing options.
|
|
23
|
+
*/
|
|
24
|
+
import { currentMetadata } from '@imqueue/rpc';
|
|
25
|
+
import { BeforeBulkCreate, BeforeBulkUpdate, BeforeCreate, BeforeUpdate, } from 'sequelize-typescript';
|
|
26
|
+
/**
|
|
27
|
+
* Stamps the decorated column with the acting user id on INSERT and UPDATE,
|
|
28
|
+
* taken from the in-flight IMQ request metadata (`currentMetadata()?.userId`).
|
|
29
|
+
*
|
|
30
|
+
* A property decorator, reusable on any model field. It mirrors `@UpdatedAt`:
|
|
31
|
+
* - on INSERT it is set to the creating actor (just as `updatedAt` is set equal
|
|
32
|
+
* to `createdAt` on insert), without overwriting an explicit value;
|
|
33
|
+
* - on UPDATE it always overwrites with the current actor — "last modified by"
|
|
34
|
+
* must reflect who actually ran the update and not be caller-spoofable.
|
|
35
|
+
*
|
|
36
|
+
* Four hooks are registered, because models are written in four ways:
|
|
37
|
+
* - single INSERT → `beforeCreate` (set-if-empty)
|
|
38
|
+
* - `Model.bulkCreate(records, …)` → `beforeBulkCreate` (set-if-empty on the
|
|
39
|
+
* built instances; a plain `bulkCreate` does not fire `beforeCreate`, so the
|
|
40
|
+
* per-instance hook alone would be bypassed)
|
|
41
|
+
* - instance `save()` / `update()` → `beforeUpdate` (receives the instance)
|
|
42
|
+
* - static `Model.update(values, …)` → `beforeBulkUpdate` (receives options;
|
|
43
|
+
* the values to write live on `options.attributes`, and Sequelize filters the
|
|
44
|
+
* written columns down to `options.fields`, computed before this hook from the
|
|
45
|
+
* caller's values — so an injected field is dropped unless also added there).
|
|
46
|
+
*
|
|
47
|
+
* No-op when there is no acting user (system / unattributed writes).
|
|
48
|
+
*
|
|
49
|
+
* @param target - the decorated model's prototype
|
|
50
|
+
* @param propertyName - the decorated column property name
|
|
51
|
+
*/
|
|
52
|
+
export function UpdatedBy(target, propertyName) {
|
|
53
|
+
const ctor = target.constructor;
|
|
54
|
+
const createHook = `__stampUpdatedByOnCreate$${propertyName}`;
|
|
55
|
+
const bulkCreateHook = `__stampUpdatedByOnBulkCreate$${propertyName}`;
|
|
56
|
+
const singleHook = `__stampUpdatedBy$${propertyName}`;
|
|
57
|
+
const bulkHook = `__stampBulkUpdatedBy$${propertyName}`;
|
|
58
|
+
if (ctor[singleHook]) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
ctor[createHook] = function (instance) {
|
|
62
|
+
const userId = currentMetadata()?.userId;
|
|
63
|
+
if (userId != null && instance[propertyName] == null) {
|
|
64
|
+
instance[propertyName] = userId;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
ctor[bulkCreateHook] = function (instances, options) {
|
|
68
|
+
const userId = currentMetadata()?.userId;
|
|
69
|
+
if (userId == null || !Array.isArray(instances)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
for (const instance of instances) {
|
|
73
|
+
if (instance && instance[propertyName] == null) {
|
|
74
|
+
instance[propertyName] = userId;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (options &&
|
|
78
|
+
Array.isArray(options.fields) &&
|
|
79
|
+
!options.fields.includes(propertyName)) {
|
|
80
|
+
options.fields.push(propertyName);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
ctor[singleHook] = function (instance) {
|
|
84
|
+
const userId = currentMetadata()?.userId;
|
|
85
|
+
if (userId != null) {
|
|
86
|
+
instance[propertyName] = userId;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
ctor[bulkHook] = function (options) {
|
|
90
|
+
const userId = currentMetadata()?.userId;
|
|
91
|
+
if (userId == null || !options || !options.attributes) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
options.attributes[propertyName] = userId;
|
|
95
|
+
if (Array.isArray(options.fields) &&
|
|
96
|
+
!options.fields.includes(propertyName)) {
|
|
97
|
+
options.fields.push(propertyName);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
BeforeCreate(ctor, createHook);
|
|
101
|
+
BeforeBulkCreate(ctor, bulkCreateHook);
|
|
102
|
+
BeforeUpdate(ctor, singleHook);
|
|
103
|
+
BeforeBulkUpdate(ctor, bulkHook);
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=UpdatedBy.js.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @imqueue/pg-sequelize - Sequelize ORM refines for @imqueue
|
|
3
|
+
*
|
|
4
|
+
* I'm Queue Software Project
|
|
5
|
+
* Copyright (C) 2025 imqueue.com <support@imqueue.com>
|
|
6
|
+
*
|
|
7
|
+
* This program is free software: you can redistribute it and/or modify
|
|
8
|
+
* it under the terms of the GNU General Public License as published by
|
|
9
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
* (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* This program is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
* GNU General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* You should have received a copy of the GNU General Public License
|
|
18
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
*
|
|
20
|
+
* If you want to use this code in a closed source (commercial) project, you can
|
|
21
|
+
* purchase a proprietary commercial license. Please contact us at
|
|
22
|
+
* <support@imqueue.com> to get commercial licensing options.
|
|
23
|
+
*/
|
|
24
|
+
import 'reflect-metadata';
|
|
25
|
+
import { type ModelOptions } from 'sequelize';
|
|
26
|
+
/**
|
|
27
|
+
* Sequelize's model options, plus the SQL that defines the view.
|
|
28
|
+
*/
|
|
29
|
+
export interface IViewDefineOptions extends ModelOptions {
|
|
30
|
+
/**
|
|
31
|
+
* The complete create statement for the view.
|
|
32
|
+
*
|
|
33
|
+
* @remarks
|
|
34
|
+
* Executed exactly as written, so it carries its own keywords — and writing it as
|
|
35
|
+
* `CREATE OR REPLACE VIEW` is what makes `sync({ withoutDrop: true })` an option
|
|
36
|
+
* later, which views that depend on each other need. The name it declares has to
|
|
37
|
+
* match the model's table name; that is checked before anything runs.
|
|
38
|
+
*/
|
|
39
|
+
viewDefinition: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Declares a model to be a database view rather than a table.
|
|
43
|
+
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* Does for a view what `Table` does for a table, and sets the flag the rest of the
|
|
46
|
+
* package keys off: the model is skipped by the table sync, created and dropped with
|
|
47
|
+
* view statements, and has its numeric columns re-cast after every finder, since a
|
|
48
|
+
* view returns them as strings.
|
|
49
|
+
*
|
|
50
|
+
* Two ordinary model options matter more here than they look. `freezeTableName` stops
|
|
51
|
+
* sequelize pluralising the model name, which has to match the name inside the
|
|
52
|
+
* definition. And `timestamps: false` stops it selecting `createdAt` and `updatedAt`,
|
|
53
|
+
* which a view has no reason to have. A view model may also extend another model
|
|
54
|
+
* instead of `BaseModel`, which is the shortest way to reuse a table's column
|
|
55
|
+
* declarations for a view over the same columns.
|
|
56
|
+
*
|
|
57
|
+
* Declared columns are a subset of what the view selects: anything the view returns
|
|
58
|
+
* and the model does not declare is simply not mapped.
|
|
59
|
+
*
|
|
60
|
+
* @param options - The definition SQL alone, or model options carrying it.
|
|
61
|
+
* @returns A class decorator.
|
|
62
|
+
* @throws TypeError when there is no definition, or it is blank. The string form used
|
|
63
|
+
* to skip that check, and an options object without the property threw an unhelpful
|
|
64
|
+
* error from reading it.
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* @View({
|
|
68
|
+
* viewDefinition: `
|
|
69
|
+
* CREATE OR REPLACE VIEW "ProductRevenue" AS
|
|
70
|
+
* SELECT "productId" AS "id", SUM("payment") AS "revenue"
|
|
71
|
+
* FROM "Order"
|
|
72
|
+
* GROUP BY "productId"
|
|
73
|
+
* `,
|
|
74
|
+
* freezeTableName: true,
|
|
75
|
+
* timestamps: false,
|
|
76
|
+
* })
|
|
77
|
+
* export class ProductRevenue extends BaseModel<ProductRevenue> {
|
|
78
|
+
* @PrimaryKey
|
|
79
|
+
* @Column(DataType.BIGINT)
|
|
80
|
+
* declare public id: number;
|
|
81
|
+
*
|
|
82
|
+
* @Column(DataType.DECIMAL(12, 2))
|
|
83
|
+
* declare public revenue: number;
|
|
84
|
+
* }
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export declare function View(options: IViewDefineOptions | string): (target: any) => void;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @imqueue/pg-sequelize - Sequelize ORM refines for @imqueue
|
|
3
|
+
*
|
|
4
|
+
* I'm Queue Software Project
|
|
5
|
+
* Copyright (C) 2025 imqueue.com <support@imqueue.com>
|
|
6
|
+
*
|
|
7
|
+
* This program is free software: you can redistribute it and/or modify
|
|
8
|
+
* it under the terms of the GNU General Public License as published by
|
|
9
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
* (at your option) any later version.
|
|
11
|
+
*
|
|
12
|
+
* This program is distributed in the hope that it will be useful,
|
|
13
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
* GNU General Public License for more details.
|
|
16
|
+
*
|
|
17
|
+
* You should have received a copy of the GNU General Public License
|
|
18
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
*
|
|
20
|
+
* If you want to use this code in a closed source (commercial) project, you can
|
|
21
|
+
* purchase a proprietary commercial license. Please contact us at
|
|
22
|
+
* <support@imqueue.com> to get commercial licensing options.
|
|
23
|
+
*/
|
|
24
|
+
import 'reflect-metadata';
|
|
25
|
+
import {} from 'sequelize';
|
|
26
|
+
import { addOptions, setModelName } from 'sequelize-typescript';
|
|
27
|
+
/**
|
|
28
|
+
* Declares a model to be a database view rather than a table.
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* Does for a view what `Table` does for a table, and sets the flag the rest of the
|
|
32
|
+
* package keys off: the model is skipped by the table sync, created and dropped with
|
|
33
|
+
* view statements, and has its numeric columns re-cast after every finder, since a
|
|
34
|
+
* view returns them as strings.
|
|
35
|
+
*
|
|
36
|
+
* Two ordinary model options matter more here than they look. `freezeTableName` stops
|
|
37
|
+
* sequelize pluralising the model name, which has to match the name inside the
|
|
38
|
+
* definition. And `timestamps: false` stops it selecting `createdAt` and `updatedAt`,
|
|
39
|
+
* which a view has no reason to have. A view model may also extend another model
|
|
40
|
+
* instead of `BaseModel`, which is the shortest way to reuse a table's column
|
|
41
|
+
* declarations for a view over the same columns.
|
|
42
|
+
*
|
|
43
|
+
* Declared columns are a subset of what the view selects: anything the view returns
|
|
44
|
+
* and the model does not declare is simply not mapped.
|
|
45
|
+
*
|
|
46
|
+
* @param options - The definition SQL alone, or model options carrying it.
|
|
47
|
+
* @returns A class decorator.
|
|
48
|
+
* @throws TypeError when there is no definition, or it is blank. The string form used
|
|
49
|
+
* to skip that check, and an options object without the property threw an unhelpful
|
|
50
|
+
* error from reading it.
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* @View({
|
|
54
|
+
* viewDefinition: `
|
|
55
|
+
* CREATE OR REPLACE VIEW "ProductRevenue" AS
|
|
56
|
+
* SELECT "productId" AS "id", SUM("payment") AS "revenue"
|
|
57
|
+
* FROM "Order"
|
|
58
|
+
* GROUP BY "productId"
|
|
59
|
+
* `,
|
|
60
|
+
* freezeTableName: true,
|
|
61
|
+
* timestamps: false,
|
|
62
|
+
* })
|
|
63
|
+
* export class ProductRevenue extends BaseModel<ProductRevenue> {
|
|
64
|
+
* @PrimaryKey
|
|
65
|
+
* @Column(DataType.BIGINT)
|
|
66
|
+
* declare public id: number;
|
|
67
|
+
*
|
|
68
|
+
* @Column(DataType.DECIMAL(12, 2))
|
|
69
|
+
* declare public revenue: number;
|
|
70
|
+
* }
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export function View(options) {
|
|
74
|
+
if (typeof options === 'string') {
|
|
75
|
+
options = { viewDefinition: options };
|
|
76
|
+
}
|
|
77
|
+
if (!options || !options.viewDefinition || !options.viewDefinition.trim()) {
|
|
78
|
+
throw new TypeError('View definition is missing!');
|
|
79
|
+
}
|
|
80
|
+
return (target) => annotate(target, options);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Registers the model under its own name and records the view options on it.
|
|
84
|
+
*
|
|
85
|
+
* @param target - Model class being decorated.
|
|
86
|
+
* @param options - View definition options, marked as a view in place.
|
|
87
|
+
*/
|
|
88
|
+
function annotate(target, options) {
|
|
89
|
+
Object.assign(options, { treatAsView: true });
|
|
90
|
+
setModelName(target.prototype, options.modelName || target.name);
|
|
91
|
+
addOptions(target.prototype, options);
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=View.js.map
|