@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,206 @@
|
|
|
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
|
+
* The property decorator a decorator factory hands back.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* Loose on purpose: it stands for the decorator returned by {@link (ColumnIndex:1) | ColumnIndex} and
|
|
30
|
+
* {@link (NullableIndex:1) | NullableIndex} when either is called with options, whose exact parameter list
|
|
31
|
+
* differs between TypeScript's legacy and current decorator emit.
|
|
32
|
+
*/
|
|
33
|
+
export type FunctionType = (...args: any[]) => any;
|
|
34
|
+
/**
|
|
35
|
+
* The index methods Postgres offers, for {@link ColumnIndexOptions.method}.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* Postgres uses `BTREE` when nothing is given, and it is the only method with an
|
|
39
|
+
* ordered key — so it is the one to want unless the data has a shape another method
|
|
40
|
+
* is built for.
|
|
41
|
+
*/
|
|
42
|
+
export declare enum IndexMethod {
|
|
43
|
+
/** Balanced tree: equality, ranges and ordering. Postgres's default. */
|
|
44
|
+
BTREE = "BTREE",
|
|
45
|
+
/** Hash: equality only, and smaller than a btree for that alone. */
|
|
46
|
+
HASH = "HASH",
|
|
47
|
+
/** Generalised search tree: geometric types, ranges, full-text. */
|
|
48
|
+
GIST = "GIST",
|
|
49
|
+
/** Space-partitioned GiST: quadtrees, tries and other unbalanced trees. */
|
|
50
|
+
SPGIST = "SPGIST",
|
|
51
|
+
/** Inverted index: containment in arrays, `jsonb` and full-text. */
|
|
52
|
+
GIN = "GIN",
|
|
53
|
+
/** Block range: huge tables whose values track their physical order. */
|
|
54
|
+
BRIN = "BRIN"
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Sort direction of an index key, for {@link ColumnIndexOptions.order}.
|
|
58
|
+
*
|
|
59
|
+
* @remarks
|
|
60
|
+
* Rarely worth setting on its own, since a btree can be read backwards — it earns its
|
|
61
|
+
* keep when several keys sort in mixed directions, which a single-direction index
|
|
62
|
+
* cannot serve.
|
|
63
|
+
*/
|
|
64
|
+
export declare enum SortOrder {
|
|
65
|
+
/** Ascending, which is Postgres's default. */
|
|
66
|
+
ASC = "ASC",
|
|
67
|
+
/** Descending. */
|
|
68
|
+
DESC = "DESC"
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The clauses of the `CREATE INDEX` statement that a {@link (ColumnIndex:1) | ColumnIndex} declaration
|
|
72
|
+
* turns into.
|
|
73
|
+
*
|
|
74
|
+
* @remarks
|
|
75
|
+
* Every field is optional in practice — both decorators take a `Partial` of this, and
|
|
76
|
+
* an empty object declares a plain btree index on the column. The names follow
|
|
77
|
+
* Postgres's own `CREATE INDEX` syntax, and the values of the raw-SQL fields are
|
|
78
|
+
* emitted as written.
|
|
79
|
+
*/
|
|
80
|
+
export interface ColumnIndexOptions {
|
|
81
|
+
/**
|
|
82
|
+
* Name of the index.
|
|
83
|
+
*
|
|
84
|
+
* @remarks
|
|
85
|
+
* Defaults to the table, the column and the declaration's position on the model,
|
|
86
|
+
* so two indices on one column cannot collide. Naming it yourself is what makes
|
|
87
|
+
* it recognisable later in `EXPLAIN` output and in `pg_indexes`.
|
|
88
|
+
*/
|
|
89
|
+
name: string;
|
|
90
|
+
/** Index method. Postgres uses `BTREE` when this is left out. */
|
|
91
|
+
method: IndexMethod;
|
|
92
|
+
/**
|
|
93
|
+
* Builds the index without locking writes out of the table.
|
|
94
|
+
*
|
|
95
|
+
* @remarks
|
|
96
|
+
* Applied to the drop as well as the create. Postgres refuses to run either
|
|
97
|
+
* inside a transaction, and a concurrent build that fails leaves an invalid
|
|
98
|
+
* index behind to be dropped by hand — the price of not blocking a live table.
|
|
99
|
+
*/
|
|
100
|
+
concurrently: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* `true` sorts nulls first, `false` sorts them last, and leaving it out follows
|
|
103
|
+
* Postgres.
|
|
104
|
+
*
|
|
105
|
+
* @remarks
|
|
106
|
+
* Postgres's own default depends on the direction: nulls last for `ASC`, first
|
|
107
|
+
* for `DESC`. Set it only to match an `ORDER BY` that asks for the other one.
|
|
108
|
+
*/
|
|
109
|
+
nullsFirst: boolean;
|
|
110
|
+
/** Sort direction of the index key. */
|
|
111
|
+
order: SortOrder;
|
|
112
|
+
/**
|
|
113
|
+
* Restricts the index to the rows this SQL condition matches.
|
|
114
|
+
*
|
|
115
|
+
* @remarks
|
|
116
|
+
* A partial index: smaller and cheaper to maintain, and used only where the
|
|
117
|
+
* planner can prove a query's own condition implies this one. Raw SQL, emitted
|
|
118
|
+
* as written, so the quoting is yours to get right.
|
|
119
|
+
*/
|
|
120
|
+
predicate: string;
|
|
121
|
+
/**
|
|
122
|
+
* Indexes this SQL expression rather than the column.
|
|
123
|
+
*
|
|
124
|
+
* @remarks
|
|
125
|
+
* For filtering on a computed value — `lower("email")` being the standard case,
|
|
126
|
+
* which nothing but an expression index can serve. Raw SQL, and it replaces the
|
|
127
|
+
* column as the key rather than joining it.
|
|
128
|
+
*/
|
|
129
|
+
expression: string;
|
|
130
|
+
/**
|
|
131
|
+
* Extra columns to carry in the index without indexing them.
|
|
132
|
+
*
|
|
133
|
+
* @remarks
|
|
134
|
+
* A covering index, so the planner can answer from the index alone instead of
|
|
135
|
+
* visiting the table. Btree only. Declared but never emitted in earlier
|
|
136
|
+
* versions.
|
|
137
|
+
*/
|
|
138
|
+
include: string[];
|
|
139
|
+
/** Collation of the index key, where it differs from the column's own. */
|
|
140
|
+
collation: string;
|
|
141
|
+
/**
|
|
142
|
+
* Operator class for the key.
|
|
143
|
+
*
|
|
144
|
+
* @remarks
|
|
145
|
+
* How the method compares values: `text_pattern_ops` to make `LIKE 'x%'`
|
|
146
|
+
* indexable, `jsonb_path_ops` for a narrower GIN index. Emitted as written.
|
|
147
|
+
*/
|
|
148
|
+
opClass: string;
|
|
149
|
+
/** Tablespace to build the index in, instead of the database default. */
|
|
150
|
+
tablespace: string;
|
|
151
|
+
/**
|
|
152
|
+
* Leaves an index that already exists alone rather than rebuilding it.
|
|
153
|
+
*
|
|
154
|
+
* @remarks
|
|
155
|
+
* Turns the pass into `CREATE INDEX IF NOT EXISTS` with no drop in front of it,
|
|
156
|
+
* so an existing index survives even if its declaration has changed since. The
|
|
157
|
+
* default rebuilds on every sync, which is always correct and always costs a
|
|
158
|
+
* full build.
|
|
159
|
+
*/
|
|
160
|
+
safe: boolean;
|
|
161
|
+
/** Rejects duplicate values, enforcing uniqueness through the index. */
|
|
162
|
+
unique: boolean;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Declares an index on a model column, with Postgres's own index options.
|
|
166
|
+
*
|
|
167
|
+
* @remarks
|
|
168
|
+
* `Index` from `sequelize-typescript` is re-exported here and covers the ordinary
|
|
169
|
+
* cases through sequelize's own sync. This declares the index as a `CREATE INDEX`
|
|
170
|
+
* statement instead, which is what makes the Postgres-specific clauses reachable —
|
|
171
|
+
* partial indices, expression keys, operator classes, concurrent builds, covering
|
|
172
|
+
* columns. `Sequelize.sync()` runs them after the tables and views are in place.
|
|
173
|
+
*
|
|
174
|
+
* Declarations accumulate, so a column can carry several, and each is named by its
|
|
175
|
+
* position unless you name it.
|
|
176
|
+
*
|
|
177
|
+
* @param options - The clauses to build into the statement.
|
|
178
|
+
* @returns A property decorator.
|
|
179
|
+
* @example
|
|
180
|
+
* ```typescript
|
|
181
|
+
* @Table
|
|
182
|
+
* export class Lead extends BaseModel<Lead> {
|
|
183
|
+
* // case-insensitive lookups, over live rows only
|
|
184
|
+
* @ColumnIndex({
|
|
185
|
+
* expression: 'lower("email")',
|
|
186
|
+
* predicate: '"deletedAt" IS NULL',
|
|
187
|
+
* unique: true,
|
|
188
|
+
* })
|
|
189
|
+
* @Column(DataType.STRING)
|
|
190
|
+
* public email: string;
|
|
191
|
+
* }
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
export declare function ColumnIndex(options: Partial<ColumnIndexOptions>): FunctionType;
|
|
195
|
+
/**
|
|
196
|
+
* Declares a plain btree index on a model column.
|
|
197
|
+
*
|
|
198
|
+
* @remarks
|
|
199
|
+
* The bare form of the decorator, equivalent to calling it with no options. Use the
|
|
200
|
+
* factory form for anything Postgres-specific.
|
|
201
|
+
*
|
|
202
|
+
* @param target - Model prototype the property belongs to.
|
|
203
|
+
* @param propertyName - Column to index.
|
|
204
|
+
* @param propertyDescriptor - Unused; present because a decorator receives it.
|
|
205
|
+
*/
|
|
206
|
+
export declare function ColumnIndex(target: any, propertyName: string, propertyDescriptor?: PropertyDescriptor): void;
|
|
@@ -0,0 +1,98 @@
|
|
|
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 { addOptions, getOptions } from 'sequelize-typescript';
|
|
26
|
+
/**
|
|
27
|
+
* The index methods Postgres offers, for {@link ColumnIndexOptions.method}.
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* Postgres uses `BTREE` when nothing is given, and it is the only method with an
|
|
31
|
+
* ordered key — so it is the one to want unless the data has a shape another method
|
|
32
|
+
* is built for.
|
|
33
|
+
*/
|
|
34
|
+
export var IndexMethod;
|
|
35
|
+
(function (IndexMethod) {
|
|
36
|
+
/** Balanced tree: equality, ranges and ordering. Postgres's default. */
|
|
37
|
+
IndexMethod["BTREE"] = "BTREE";
|
|
38
|
+
/** Hash: equality only, and smaller than a btree for that alone. */
|
|
39
|
+
IndexMethod["HASH"] = "HASH";
|
|
40
|
+
/** Generalised search tree: geometric types, ranges, full-text. */
|
|
41
|
+
IndexMethod["GIST"] = "GIST";
|
|
42
|
+
/** Space-partitioned GiST: quadtrees, tries and other unbalanced trees. */
|
|
43
|
+
IndexMethod["SPGIST"] = "SPGIST";
|
|
44
|
+
/** Inverted index: containment in arrays, `jsonb` and full-text. */
|
|
45
|
+
IndexMethod["GIN"] = "GIN";
|
|
46
|
+
/** Block range: huge tables whose values track their physical order. */
|
|
47
|
+
IndexMethod["BRIN"] = "BRIN";
|
|
48
|
+
})(IndexMethod || (IndexMethod = {}));
|
|
49
|
+
/**
|
|
50
|
+
* Sort direction of an index key, for {@link ColumnIndexOptions.order}.
|
|
51
|
+
*
|
|
52
|
+
* @remarks
|
|
53
|
+
* Rarely worth setting on its own, since a btree can be read backwards — it earns its
|
|
54
|
+
* keep when several keys sort in mixed directions, which a single-direction index
|
|
55
|
+
* cannot serve.
|
|
56
|
+
*/
|
|
57
|
+
export var SortOrder;
|
|
58
|
+
(function (SortOrder) {
|
|
59
|
+
/** Ascending, which is Postgres's default. */
|
|
60
|
+
SortOrder["ASC"] = "ASC";
|
|
61
|
+
/** Descending. */
|
|
62
|
+
SortOrder["DESC"] = "DESC";
|
|
63
|
+
})(SortOrder || (SortOrder = {}));
|
|
64
|
+
export function ColumnIndex(...args) {
|
|
65
|
+
if (args.length >= 2) {
|
|
66
|
+
const [target, propertyName, propertyDescriptor] = args;
|
|
67
|
+
return annotate(target, propertyName, propertyDescriptor);
|
|
68
|
+
}
|
|
69
|
+
return (target, propertyName, propertyDescriptor) => {
|
|
70
|
+
annotate(target, propertyName, propertyDescriptor, args[0]);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Appends one index declaration to the model's options.
|
|
75
|
+
*
|
|
76
|
+
* @param target - Model prototype the property belongs to.
|
|
77
|
+
* @param propertyName - Column to index.
|
|
78
|
+
* @param propertyDescriptor - Unused; present because a decorator receives it.
|
|
79
|
+
* @param options - The clauses to record for this index.
|
|
80
|
+
*/
|
|
81
|
+
function annotate(target, propertyName, propertyDescriptor, options = {}) {
|
|
82
|
+
// Optional, because there may be no options bag yet. TypeScript applies
|
|
83
|
+
// property decorators BEFORE class decorators, so on an ordinary model this
|
|
84
|
+
// runs before `@Table` has created one, and reading `.indices` off undefined
|
|
85
|
+
// threw at import time — which made this decorator unusable exactly where it
|
|
86
|
+
// was meant to be used.
|
|
87
|
+
const indices = getOptions(target)?.indices || [];
|
|
88
|
+
addOptions(target, {
|
|
89
|
+
indices: [
|
|
90
|
+
...indices,
|
|
91
|
+
{
|
|
92
|
+
column: propertyName,
|
|
93
|
+
options,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=ColumnIndex.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stamps the decorated column with the acting user id on INSERT, taken from the
|
|
3
|
+
* in-flight IMQ request metadata (`currentMetadata()?.userId`) — so the id never
|
|
4
|
+
* travels through method arguments and cannot be spoofed by a caller.
|
|
5
|
+
*
|
|
6
|
+
* A property decorator, reusable on any model field. The hook is a no-op when
|
|
7
|
+
* there is no acting user (system / unattributed writes) and never overwrites a
|
|
8
|
+
* value the application set explicitly.
|
|
9
|
+
*
|
|
10
|
+
* Two hooks are registered, because rows are inserted in two ways:
|
|
11
|
+
* - instance / single `create()` → `beforeCreate` (receives the instance)
|
|
12
|
+
* - static `Model.bulkCreate(records, …)` → `beforeBulkCreate` (receives the
|
|
13
|
+
* built instances; a plain `bulkCreate` does not fire `beforeCreate`, so the
|
|
14
|
+
* per-instance hook alone would be bypassed). Sequelize filters the written
|
|
15
|
+
* columns down to `options.fields` when the caller supplies it, so an injected
|
|
16
|
+
* field is dropped unless also added there.
|
|
17
|
+
*
|
|
18
|
+
* Mechanism: a property decorator receives the prototype, but Sequelize hook
|
|
19
|
+
* decorators must target a static method on the constructor, so generated static
|
|
20
|
+
* methods are attached to `target.constructor` and registered through the public
|
|
21
|
+
* `@BeforeCreate` / `@BeforeBulkCreate`. Hooks are installed by
|
|
22
|
+
* sequelize-typescript's `installHooks` during `Sequelize#addModels`.
|
|
23
|
+
*
|
|
24
|
+
* @param target - the decorated model's prototype
|
|
25
|
+
* @param propertyName - the decorated column property name
|
|
26
|
+
*/
|
|
27
|
+
export declare function CreatedBy(target: any, propertyName: string): void;
|
|
@@ -0,0 +1,84 @@
|
|
|
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, BeforeCreate } from 'sequelize-typescript';
|
|
26
|
+
/**
|
|
27
|
+
* Stamps the decorated column with the acting user id on INSERT, taken from the
|
|
28
|
+
* in-flight IMQ request metadata (`currentMetadata()?.userId`) — so the id never
|
|
29
|
+
* travels through method arguments and cannot be spoofed by a caller.
|
|
30
|
+
*
|
|
31
|
+
* A property decorator, reusable on any model field. The hook is a no-op when
|
|
32
|
+
* there is no acting user (system / unattributed writes) and never overwrites a
|
|
33
|
+
* value the application set explicitly.
|
|
34
|
+
*
|
|
35
|
+
* Two hooks are registered, because rows are inserted in two ways:
|
|
36
|
+
* - instance / single `create()` → `beforeCreate` (receives the instance)
|
|
37
|
+
* - static `Model.bulkCreate(records, …)` → `beforeBulkCreate` (receives the
|
|
38
|
+
* built instances; a plain `bulkCreate` does not fire `beforeCreate`, so the
|
|
39
|
+
* per-instance hook alone would be bypassed). Sequelize filters the written
|
|
40
|
+
* columns down to `options.fields` when the caller supplies it, so an injected
|
|
41
|
+
* field is dropped unless also added there.
|
|
42
|
+
*
|
|
43
|
+
* Mechanism: a property decorator receives the prototype, but Sequelize hook
|
|
44
|
+
* decorators must target a static method on the constructor, so generated static
|
|
45
|
+
* methods are attached to `target.constructor` and registered through the public
|
|
46
|
+
* `@BeforeCreate` / `@BeforeBulkCreate`. Hooks are installed by
|
|
47
|
+
* sequelize-typescript's `installHooks` during `Sequelize#addModels`.
|
|
48
|
+
*
|
|
49
|
+
* @param target - the decorated model's prototype
|
|
50
|
+
* @param propertyName - the decorated column property name
|
|
51
|
+
*/
|
|
52
|
+
export function CreatedBy(target, propertyName) {
|
|
53
|
+
const ctor = target.constructor;
|
|
54
|
+
const hookName = `__stampCreatedBy$${propertyName}`;
|
|
55
|
+
const bulkHook = `__stampCreatedByOnBulkCreate$${propertyName}`;
|
|
56
|
+
if (ctor[hookName]) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
ctor[hookName] = function (instance) {
|
|
60
|
+
const userId = currentMetadata()?.userId;
|
|
61
|
+
if (userId != null && instance[propertyName] == null) {
|
|
62
|
+
instance[propertyName] = userId;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
ctor[bulkHook] = function (instances, options) {
|
|
66
|
+
const userId = currentMetadata()?.userId;
|
|
67
|
+
if (userId == null || !Array.isArray(instances)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
for (const instance of instances) {
|
|
71
|
+
if (instance && instance[propertyName] == null) {
|
|
72
|
+
instance[propertyName] = userId;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (options &&
|
|
76
|
+
Array.isArray(options.fields) &&
|
|
77
|
+
!options.fields.includes(propertyName)) {
|
|
78
|
+
options.fields.push(propertyName);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
BeforeCreate(ctor, hookName);
|
|
82
|
+
BeforeBulkCreate(ctor, bulkHook);
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=CreatedBy.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stamps the decorated column with the acting user id on soft-delete (and clears
|
|
3
|
+
* it on restore), taken from the in-flight IMQ request metadata
|
|
4
|
+
* (`currentMetadata()?.userId`). Mirrors `@DeletedAt`: set on delete, cleared on
|
|
5
|
+
* restore, and a delete touches neither `updatedAt` nor an `@UpdatedBy` column.
|
|
6
|
+
*
|
|
7
|
+
* A property decorator for paranoid models, reusable on any field. Soft-delete
|
|
8
|
+
* runs through `Model.destroy`, which Sequelize turns into a `deletedAt`-only
|
|
9
|
+
* UPDATE whose value hash is built internally and is unreachable from any hook.
|
|
10
|
+
* So instead of mutating that statement, the `beforeBulkDestroy` hook stamps the
|
|
11
|
+
* column with a sibling UPDATE over the same still-live rows (same `where`, same
|
|
12
|
+
* transaction) just before the soft-delete sets `deletedAt`; `beforeBulkRestore`
|
|
13
|
+
* does the inverse, clearing it as `deletedAt` is cleared.
|
|
14
|
+
*
|
|
15
|
+
* The sibling UPDATE runs on `this` — the concrete model the hook fires for,
|
|
16
|
+
* bound by Sequelize at call time (`runHooks` → `hook.apply(model, ...)`) —
|
|
17
|
+
* rather than the class captured at decoration time. That lets the decorator be
|
|
18
|
+
* declared on an abstract base model (e.g. a shared `BaseParanoid`) and still
|
|
19
|
+
* resolve to the real subclass; the captured constructor would be the
|
|
20
|
+
* (unregistered) base and break with "model not initialized".
|
|
21
|
+
*
|
|
22
|
+
* `hooks: false` avoids hook re-entrancy and keeps the delete from bumping an
|
|
23
|
+
* `@UpdatedBy` column; `silent: true` keeps it from bumping `updatedAt`, so the
|
|
24
|
+
* delete writes exactly `deletedBy` + `deletedAt` (like the native paranoid
|
|
25
|
+
* delete writes only `deletedAt`). No-op when there is no acting user.
|
|
26
|
+
*
|
|
27
|
+
* @param target - the decorated model's prototype
|
|
28
|
+
* @param propertyName - the decorated column property name
|
|
29
|
+
*/
|
|
30
|
+
export declare function DeletedBy(target: any, propertyName: string): void;
|
|
@@ -0,0 +1,89 @@
|
|
|
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 { BeforeBulkDestroy, BeforeBulkRestore } from 'sequelize-typescript';
|
|
26
|
+
/**
|
|
27
|
+
* Stamps the decorated column with the acting user id on soft-delete (and clears
|
|
28
|
+
* it on restore), taken from the in-flight IMQ request metadata
|
|
29
|
+
* (`currentMetadata()?.userId`). Mirrors `@DeletedAt`: set on delete, cleared on
|
|
30
|
+
* restore, and a delete touches neither `updatedAt` nor an `@UpdatedBy` column.
|
|
31
|
+
*
|
|
32
|
+
* A property decorator for paranoid models, reusable on any field. Soft-delete
|
|
33
|
+
* runs through `Model.destroy`, which Sequelize turns into a `deletedAt`-only
|
|
34
|
+
* UPDATE whose value hash is built internally and is unreachable from any hook.
|
|
35
|
+
* So instead of mutating that statement, the `beforeBulkDestroy` hook stamps the
|
|
36
|
+
* column with a sibling UPDATE over the same still-live rows (same `where`, same
|
|
37
|
+
* transaction) just before the soft-delete sets `deletedAt`; `beforeBulkRestore`
|
|
38
|
+
* does the inverse, clearing it as `deletedAt` is cleared.
|
|
39
|
+
*
|
|
40
|
+
* The sibling UPDATE runs on `this` — the concrete model the hook fires for,
|
|
41
|
+
* bound by Sequelize at call time (`runHooks` → `hook.apply(model, ...)`) —
|
|
42
|
+
* rather than the class captured at decoration time. That lets the decorator be
|
|
43
|
+
* declared on an abstract base model (e.g. a shared `BaseParanoid`) and still
|
|
44
|
+
* resolve to the real subclass; the captured constructor would be the
|
|
45
|
+
* (unregistered) base and break with "model not initialized".
|
|
46
|
+
*
|
|
47
|
+
* `hooks: false` avoids hook re-entrancy and keeps the delete from bumping an
|
|
48
|
+
* `@UpdatedBy` column; `silent: true` keeps it from bumping `updatedAt`, so the
|
|
49
|
+
* delete writes exactly `deletedBy` + `deletedAt` (like the native paranoid
|
|
50
|
+
* delete writes only `deletedAt`). No-op when there is no acting user.
|
|
51
|
+
*
|
|
52
|
+
* @param target - the decorated model's prototype
|
|
53
|
+
* @param propertyName - the decorated column property name
|
|
54
|
+
*/
|
|
55
|
+
export function DeletedBy(target, propertyName) {
|
|
56
|
+
const ctor = target.constructor;
|
|
57
|
+
const bulkHook = `__stampBulkDeletedBy$${propertyName}`;
|
|
58
|
+
const restoreHook = `__clearBulkDeletedBy$${propertyName}`;
|
|
59
|
+
if (ctor[bulkHook]) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
ctor[bulkHook] = async function (options) {
|
|
63
|
+
const userId = currentMetadata()?.userId;
|
|
64
|
+
if (userId == null || !options || !options.where) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
await this.update({ [propertyName]: userId }, {
|
|
68
|
+
where: options.where,
|
|
69
|
+
transaction: options.transaction,
|
|
70
|
+
hooks: false,
|
|
71
|
+
silent: true,
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
ctor[restoreHook] = async function (options) {
|
|
75
|
+
if (!options || !options.where) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
await this.update({ [propertyName]: null }, {
|
|
79
|
+
where: options.where,
|
|
80
|
+
transaction: options.transaction,
|
|
81
|
+
paranoid: false,
|
|
82
|
+
hooks: false,
|
|
83
|
+
silent: true,
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
BeforeBulkDestroy(ctor, bulkHook);
|
|
87
|
+
BeforeBulkRestore(ctor, restoreHook);
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=DeletedBy.js.map
|
|
@@ -0,0 +1,124 @@
|
|
|
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 { type IViewDefineOptions } from './View.js';
|
|
24
|
+
/**
|
|
25
|
+
* Values for a dynamic view's placeholders, keyed by placeholder name.
|
|
26
|
+
*
|
|
27
|
+
* @remarks
|
|
28
|
+
* Values are typed as strings and rendered as quoted SQL constants, so a numeric
|
|
29
|
+
* placeholder arrives as a quoted number and relies on Postgres casting it. They are
|
|
30
|
+
* escaped on the way in, which is what makes it safe for one to carry a caller's
|
|
31
|
+
* input.
|
|
32
|
+
*/
|
|
33
|
+
export interface ViewParams {
|
|
34
|
+
[name: string]: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Options for a view whose definition is parameterised.
|
|
38
|
+
*/
|
|
39
|
+
export interface IDynamicViewDefineOptions extends IViewDefineOptions {
|
|
40
|
+
/**
|
|
41
|
+
* A value for every placeholder the definition names.
|
|
42
|
+
*
|
|
43
|
+
* @remarks
|
|
44
|
+
* These are the defaults. A query may override any of them through
|
|
45
|
+
* `FindOptions.viewParams`, and what it does not override falls back to here.
|
|
46
|
+
* Every placeholder must appear in this map, which is checked when the class is
|
|
47
|
+
* defined.
|
|
48
|
+
*/
|
|
49
|
+
viewParams: ViewParams;
|
|
50
|
+
/**
|
|
51
|
+
* The create statement, with placeholders in it.
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* Same as the static case except that `@{name}` placeholders are substituted
|
|
55
|
+
* before the statement is used.
|
|
56
|
+
*/
|
|
57
|
+
viewDefinition: string;
|
|
58
|
+
/**
|
|
59
|
+
* Marks the model as a dynamic view. Set by the decorator, not by you.
|
|
60
|
+
*/
|
|
61
|
+
isDynamicView?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The placeholder pattern as a source string: `@{name}`, where the name is letters,
|
|
65
|
+
* digits and underscores.
|
|
66
|
+
*
|
|
67
|
+
* @remarks
|
|
68
|
+
* Exported so a service can compile its own matcher — to validate a definition of its
|
|
69
|
+
* own, say — rather than hard-coding the syntax a second time.
|
|
70
|
+
*/
|
|
71
|
+
export declare const MATCHER = "@\\{([a-z0-9_]+?)\\}";
|
|
72
|
+
/** {@link MATCHER} compiled to find every placeholder in a definition. */
|
|
73
|
+
export declare const RX_MATCHER: RegExp;
|
|
74
|
+
/** {@link MATCHER} compiled to pull the name out of a single placeholder. */
|
|
75
|
+
export declare const RX_NAME_MATCHER: RegExp;
|
|
76
|
+
/**
|
|
77
|
+
* Declares a model to be a view whose definition is parameterised per query.
|
|
78
|
+
*
|
|
79
|
+
* @remarks
|
|
80
|
+
* Everything `View` does, plus placeholders. The definition may carry `@{name}`
|
|
81
|
+
* markers, every finder accepts `viewParams` to fill them, and the select-query
|
|
82
|
+
* generator substitutes them and splices the resulting statement into the query — as
|
|
83
|
+
* the `FROM` target, or as a joined subquery when the view is reached through an
|
|
84
|
+
* `include`. One model then serves a family of views that differ only by a constant,
|
|
85
|
+
* which is the alternative to defining one view per variant in a migration.
|
|
86
|
+
*
|
|
87
|
+
* Every placeholder must have a default in `viewParams`. That is checked while the
|
|
88
|
+
* class is being defined, so a missing one is an error at import rather than a
|
|
89
|
+
* malformed statement at query time.
|
|
90
|
+
*
|
|
91
|
+
* Values are escaped, so a parameter can carry a caller's input. Anything that is not
|
|
92
|
+
* a number or a string becomes `NULL`.
|
|
93
|
+
*
|
|
94
|
+
* @param options - Model options carrying the definition and the parameter defaults.
|
|
95
|
+
* @returns A class decorator.
|
|
96
|
+
* @throws TypeError when the definition is missing or blank, or when it names a
|
|
97
|
+
* placeholder that `viewParams` does not.
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* @DynamicView({
|
|
101
|
+
* viewDefinition: `
|
|
102
|
+
* CREATE OR REPLACE VIEW "ProductRevenue" AS
|
|
103
|
+
* SELECT "productId" AS "id", SUM("payment") AS "revenue"
|
|
104
|
+
* FROM "Order"
|
|
105
|
+
* WHERE "currency" = @{currency}
|
|
106
|
+
* GROUP BY "productId"
|
|
107
|
+
* `,
|
|
108
|
+
* viewParams: { currency: 'USD' },
|
|
109
|
+
* freezeTableName: true,
|
|
110
|
+
* timestamps: false,
|
|
111
|
+
* })
|
|
112
|
+
* export class ProductRevenue extends BaseModel<ProductRevenue> {
|
|
113
|
+
* @PrimaryKey
|
|
114
|
+
* @Column(DataType.BIGINT)
|
|
115
|
+
* declare public id: number;
|
|
116
|
+
* }
|
|
117
|
+
*
|
|
118
|
+
* // the same model, read in another currency
|
|
119
|
+
* const rows = await ProductRevenue.findAll({
|
|
120
|
+
* viewParams: { currency: 'EUR' },
|
|
121
|
+
* });
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
export declare function DynamicView(options: IDynamicViewDefineOptions): (target: any) => void;
|