@getstrata/core 0.5.97 → 0.5.98
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 +10 -0
- package/dist/core/contracts/container.d.ts +2 -0
- package/dist/core/database/factory.d.ts +31 -4
- package/dist/core/database/index.d.ts +3 -1
- package/dist/core/database/inflection.d.ts +4 -0
- package/dist/core/database/model.d.ts +50 -1
- package/dist/core/database/relationQuery.d.ts +149 -0
- package/dist/core/database/repositoryQuery.d.ts +5 -0
- package/dist/core/database/whereBuilder.d.ts +9 -1
- package/dist/core/events/eventBus.d.ts +2 -0
- package/dist/core/http/resources.d.ts +20 -1
- package/dist/entries/contracts/container.js +6 -0
- package/dist/entries/database/factory.js +116 -5
- package/dist/entries/database/model.js +760 -7
- package/dist/entries/database/query.js +9 -0
- package/dist/entries/database/repositoryQuery.js +26 -0
- package/dist/entries/database/schema.js +9 -0
- package/dist/entries/http/resources.js +62 -1
- package/dist/framework/public-api.d.ts +4 -2
- package/dist/index.js +990 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1497,6 +1497,12 @@ class ServiceContainer {
|
|
|
1497
1497
|
resolve(key) {
|
|
1498
1498
|
return this.get(key);
|
|
1499
1499
|
}
|
|
1500
|
+
make(key) {
|
|
1501
|
+
return this.resolve(key);
|
|
1502
|
+
}
|
|
1503
|
+
instance(key, value) {
|
|
1504
|
+
return this.set(key, value);
|
|
1505
|
+
}
|
|
1500
1506
|
has(key) {
|
|
1501
1507
|
return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
|
|
1502
1508
|
}
|
|
@@ -1525,6 +1531,9 @@ class ConfigStore {
|
|
|
1525
1531
|
class EventBus {
|
|
1526
1532
|
constructor() {}
|
|
1527
1533
|
listeners = new Map;
|
|
1534
|
+
on(event, listener) {
|
|
1535
|
+
return this.listen(event, listener);
|
|
1536
|
+
}
|
|
1528
1537
|
listen(event, listener) {
|
|
1529
1538
|
const handlers = this.listeners.get(event) ?? new Set;
|
|
1530
1539
|
handlers.add(listener);
|
|
@@ -1536,6 +1545,9 @@ class EventBus {
|
|
|
1536
1545
|
}
|
|
1537
1546
|
};
|
|
1538
1547
|
}
|
|
1548
|
+
async emit(event, payload) {
|
|
1549
|
+
await this.dispatch(event, payload);
|
|
1550
|
+
}
|
|
1539
1551
|
async dispatch(event, payload) {
|
|
1540
1552
|
const handlers = this.listeners.get(event);
|
|
1541
1553
|
if (!handlers || handlers.size === 0) {
|
|
@@ -1737,7 +1749,16 @@ function appendWhereParts(tableName, where, params) {
|
|
|
1737
1749
|
}
|
|
1738
1750
|
return clauses.join(" AND ");
|
|
1739
1751
|
}
|
|
1752
|
+
function remapExistsSql(sql, existsParams, params) {
|
|
1753
|
+
const offset = params.length;
|
|
1754
|
+
params.push(...existsParams);
|
|
1755
|
+
return sql.replace(/\$(\d+)/g, (_match, index) => `$${offset + Number(index)}`);
|
|
1756
|
+
}
|
|
1740
1757
|
function buildWhereNodeClause(tableName, node, params) {
|
|
1758
|
+
if ("exists" in node) {
|
|
1759
|
+
const body = remapExistsSql(node.exists.sql, node.exists.params, params);
|
|
1760
|
+
return `${node.exists.not ? "NOT " : ""}EXISTS (${body})`;
|
|
1761
|
+
}
|
|
1741
1762
|
if ("where" in node) {
|
|
1742
1763
|
return appendWhereParts(tableName, node.where, params);
|
|
1743
1764
|
}
|
|
@@ -2255,6 +2276,23 @@ class RepositoryQuery {
|
|
|
2255
2276
|
this.queryOptions = { ...this.queryOptions, limit };
|
|
2256
2277
|
return this;
|
|
2257
2278
|
}
|
|
2279
|
+
whereNull(column) {
|
|
2280
|
+
return this.where({ [column]: null });
|
|
2281
|
+
}
|
|
2282
|
+
whereNotNull(column) {
|
|
2283
|
+
return this.where({ [column]: { isNull: false } });
|
|
2284
|
+
}
|
|
2285
|
+
whereIn(column, values) {
|
|
2286
|
+
return this.where({ [column]: values });
|
|
2287
|
+
}
|
|
2288
|
+
whereExists(sql, params = []) {
|
|
2289
|
+
this.whereNodes.push({ kind: "and", exists: { sql, params } });
|
|
2290
|
+
return this;
|
|
2291
|
+
}
|
|
2292
|
+
whereNotExists(sql, params = []) {
|
|
2293
|
+
this.whereNodes.push({ kind: "and", exists: { sql, params, not: true } });
|
|
2294
|
+
return this;
|
|
2295
|
+
}
|
|
2258
2296
|
offset(offset) {
|
|
2259
2297
|
this.queryOptions = { ...this.queryOptions, offset };
|
|
2260
2298
|
return this;
|
|
@@ -2766,6 +2804,162 @@ function createDatabaseConnection(source) {
|
|
|
2766
2804
|
}
|
|
2767
2805
|
};
|
|
2768
2806
|
}
|
|
2807
|
+
// ../../src/core/database/factory.ts
|
|
2808
|
+
class Factory {
|
|
2809
|
+
quantity = 1;
|
|
2810
|
+
counted = false;
|
|
2811
|
+
sequenceIndex = 0;
|
|
2812
|
+
stateTransforms = [];
|
|
2813
|
+
sequenceItems = [];
|
|
2814
|
+
parentAssociations = [];
|
|
2815
|
+
children = [];
|
|
2816
|
+
afterMakingCallbacks = [];
|
|
2817
|
+
afterCreatingCallbacks = [];
|
|
2818
|
+
definition() {
|
|
2819
|
+
throw new Error("Factory definition must be implemented by subclass.");
|
|
2820
|
+
}
|
|
2821
|
+
clone() {
|
|
2822
|
+
const next = Object.create(Object.getPrototypeOf(this));
|
|
2823
|
+
Object.assign(next, this);
|
|
2824
|
+
next.stateTransforms = [...this.stateTransforms];
|
|
2825
|
+
next.sequenceItems = [...this.sequenceItems];
|
|
2826
|
+
next.parentAssociations = [...this.parentAssociations];
|
|
2827
|
+
next.children = [...this.children];
|
|
2828
|
+
next.afterMakingCallbacks = [...this.afterMakingCallbacks];
|
|
2829
|
+
next.afterCreatingCallbacks = [...this.afterCreatingCallbacks];
|
|
2830
|
+
return next;
|
|
2831
|
+
}
|
|
2832
|
+
count(quantity) {
|
|
2833
|
+
if (!Number.isInteger(quantity) || quantity < 1) {
|
|
2834
|
+
throw new Error("Factory.count() requires a positive integer.");
|
|
2835
|
+
}
|
|
2836
|
+
const next = this.clone();
|
|
2837
|
+
next.quantity = quantity;
|
|
2838
|
+
next.counted = true;
|
|
2839
|
+
return next;
|
|
2840
|
+
}
|
|
2841
|
+
state(state) {
|
|
2842
|
+
const next = this.clone();
|
|
2843
|
+
next.stateTransforms = [...this.stateTransforms, state];
|
|
2844
|
+
return next;
|
|
2845
|
+
}
|
|
2846
|
+
sequence(...items) {
|
|
2847
|
+
if (items.length === 0) {
|
|
2848
|
+
throw new Error("Factory.sequence() requires at least one attribute set.");
|
|
2849
|
+
}
|
|
2850
|
+
const next = this.clone();
|
|
2851
|
+
next.sequenceItems = [...this.sequenceItems, ...items];
|
|
2852
|
+
return next;
|
|
2853
|
+
}
|
|
2854
|
+
for(parent, foreignKey) {
|
|
2855
|
+
if (parent.id === undefined || parent.id === null) {
|
|
2856
|
+
throw new Error("Factory.for() requires a parent with an id.");
|
|
2857
|
+
}
|
|
2858
|
+
const next = this.clone();
|
|
2859
|
+
next.parentAssociations = [...this.parentAssociations, { foreignKey, value: parent.id }];
|
|
2860
|
+
return next;
|
|
2861
|
+
}
|
|
2862
|
+
recycle(parent, foreignKey) {
|
|
2863
|
+
return this.for(parent, foreignKey);
|
|
2864
|
+
}
|
|
2865
|
+
afterMaking(callback) {
|
|
2866
|
+
const next = this.clone();
|
|
2867
|
+
next.afterMakingCallbacks = [...this.afterMakingCallbacks, callback];
|
|
2868
|
+
return next;
|
|
2869
|
+
}
|
|
2870
|
+
afterCreating(callback) {
|
|
2871
|
+
const next = this.clone();
|
|
2872
|
+
next.afterCreatingCallbacks = [...this.afterCreatingCallbacks, callback];
|
|
2873
|
+
return next;
|
|
2874
|
+
}
|
|
2875
|
+
has(factory, foreignKey) {
|
|
2876
|
+
const next = this.clone();
|
|
2877
|
+
next.children = [
|
|
2878
|
+
...this.children,
|
|
2879
|
+
{ factory, foreignKey }
|
|
2880
|
+
];
|
|
2881
|
+
return next;
|
|
2882
|
+
}
|
|
2883
|
+
make(overrides = {}) {
|
|
2884
|
+
if (!this.counted) {
|
|
2885
|
+
return this.makeOne(overrides);
|
|
2886
|
+
}
|
|
2887
|
+
return Array.from({ length: this.quantity }, () => this.makeOne(overrides));
|
|
2888
|
+
}
|
|
2889
|
+
async create(overrides = {}) {
|
|
2890
|
+
if (!this.counted) {
|
|
2891
|
+
return await this.createOne(overrides);
|
|
2892
|
+
}
|
|
2893
|
+
const records = [];
|
|
2894
|
+
for (let index = 0;index < this.quantity; index += 1) {
|
|
2895
|
+
records.push(await this.createOne(overrides));
|
|
2896
|
+
}
|
|
2897
|
+
return records;
|
|
2898
|
+
}
|
|
2899
|
+
makeOne(overrides = {}) {
|
|
2900
|
+
let record = { ...this.definition() };
|
|
2901
|
+
for (const state of this.stateTransforms) {
|
|
2902
|
+
const patch = typeof state === "function" ? state(record) : state;
|
|
2903
|
+
record = { ...record, ...patch };
|
|
2904
|
+
}
|
|
2905
|
+
if (this.sequenceItems.length > 0) {
|
|
2906
|
+
const item = this.sequenceItems[this.sequenceIndex % this.sequenceItems.length];
|
|
2907
|
+
const patch = typeof item === "function" ? item(this.sequenceIndex) : item;
|
|
2908
|
+
record = { ...record, ...patch };
|
|
2909
|
+
this.sequenceIndex += 1;
|
|
2910
|
+
}
|
|
2911
|
+
for (const association of this.parentAssociations) {
|
|
2912
|
+
record[association.foreignKey] = association.value;
|
|
2913
|
+
}
|
|
2914
|
+
const made = {
|
|
2915
|
+
...record,
|
|
2916
|
+
...overrides
|
|
2917
|
+
};
|
|
2918
|
+
for (const callback of this.afterMakingCallbacks) {
|
|
2919
|
+
callback(made);
|
|
2920
|
+
}
|
|
2921
|
+
return made;
|
|
2922
|
+
}
|
|
2923
|
+
async createOne(overrides = {}) {
|
|
2924
|
+
const created = await this.persist(this.insertable(this.makeOne(overrides)));
|
|
2925
|
+
for (const child of this.children) {
|
|
2926
|
+
await child.factory.for(created, child.foreignKey).create();
|
|
2927
|
+
}
|
|
2928
|
+
for (const callback of this.afterCreatingCallbacks) {
|
|
2929
|
+
await callback(created);
|
|
2930
|
+
}
|
|
2931
|
+
return created;
|
|
2932
|
+
}
|
|
2933
|
+
insertable(record) {
|
|
2934
|
+
const values = { ...record };
|
|
2935
|
+
if (values.id === 0 || values.id === undefined || values.id === null) {
|
|
2936
|
+
delete values.id;
|
|
2937
|
+
}
|
|
2938
|
+
return values;
|
|
2939
|
+
}
|
|
2940
|
+
persist(_values) {
|
|
2941
|
+
throw new Error("Factory.persist() must be implemented to use create().");
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
// ../../src/core/database/inflection.ts
|
|
2945
|
+
function singularize(word) {
|
|
2946
|
+
if (word.endsWith("ies") && word.length > 3) {
|
|
2947
|
+
return `${word.slice(0, -3)}y`;
|
|
2948
|
+
}
|
|
2949
|
+
if (/(ses|xes|zes|ches|shes)$/i.test(word)) {
|
|
2950
|
+
return word.slice(0, -2);
|
|
2951
|
+
}
|
|
2952
|
+
if (word.endsWith("s") && !word.endsWith("ss")) {
|
|
2953
|
+
return word.slice(0, -1);
|
|
2954
|
+
}
|
|
2955
|
+
return word;
|
|
2956
|
+
}
|
|
2957
|
+
function foreignKeyFromTable(tableName) {
|
|
2958
|
+
return `${singularize(tableName)}_id`;
|
|
2959
|
+
}
|
|
2960
|
+
function pivotTableName(leftTable, rightTable) {
|
|
2961
|
+
return [singularize(leftTable), singularize(rightTable)].sort().join("_");
|
|
2962
|
+
}
|
|
2769
2963
|
// ../../src/core/database/migrations/advisoryLock.ts
|
|
2770
2964
|
var MIGRATION_LOCK_KEY = 42424242;
|
|
2771
2965
|
async function withMigrationLock(db, callback, lockKey = MIGRATION_LOCK_KEY) {
|
|
@@ -2878,10 +3072,479 @@ async function freshDatabase(db, migrations, options = {}) {
|
|
|
2878
3072
|
}
|
|
2879
3073
|
await runFresh();
|
|
2880
3074
|
}
|
|
3075
|
+
// ../../src/core/database/relationQuery.ts
|
|
3076
|
+
function asWhere(where) {
|
|
3077
|
+
return where;
|
|
3078
|
+
}
|
|
3079
|
+
function ownerId(owner, ownerKey) {
|
|
3080
|
+
if (typeof owner.get === "function") {
|
|
3081
|
+
return owner.get(ownerKey);
|
|
3082
|
+
}
|
|
3083
|
+
if (owner && typeof owner === "object" && "id" in owner && owner.id !== undefined) {
|
|
3084
|
+
return owner.id;
|
|
3085
|
+
}
|
|
3086
|
+
throw new Error("belongsTo.associate() requires a related model or { id }.");
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
class HasManyRelationQuery {
|
|
3090
|
+
parent;
|
|
3091
|
+
related;
|
|
3092
|
+
relation;
|
|
3093
|
+
kind = "hasMany";
|
|
3094
|
+
extraWhere = {};
|
|
3095
|
+
extraOptions = {};
|
|
3096
|
+
constructor(parent, related, relation) {
|
|
3097
|
+
this.parent = parent;
|
|
3098
|
+
this.related = related;
|
|
3099
|
+
this.relation = relation;
|
|
3100
|
+
}
|
|
3101
|
+
where(where) {
|
|
3102
|
+
this.extraWhere = { ...this.extraWhere, ...where };
|
|
3103
|
+
return this;
|
|
3104
|
+
}
|
|
3105
|
+
orderBy(orderBy) {
|
|
3106
|
+
this.extraOptions = { ...this.extraOptions, orderBy };
|
|
3107
|
+
return this;
|
|
3108
|
+
}
|
|
3109
|
+
limit(limit) {
|
|
3110
|
+
this.extraOptions = { ...this.extraOptions, limit };
|
|
3111
|
+
return this;
|
|
3112
|
+
}
|
|
3113
|
+
applyEagerLoad(query, alias) {
|
|
3114
|
+
query.withHasMany(alias, this.relation, this.related.repository(), this.extraOptions);
|
|
3115
|
+
}
|
|
3116
|
+
hydrateEager(row, alias) {
|
|
3117
|
+
const value = row[alias] ?? [];
|
|
3118
|
+
const rows = Array.isArray(value) ? value : [];
|
|
3119
|
+
return rows.map((item) => this.related.newFromRecord(item));
|
|
3120
|
+
}
|
|
3121
|
+
toExistsClause(parentTable) {
|
|
3122
|
+
const childTable = this.related.repository().getTable().name;
|
|
3123
|
+
const extra = buildAdvancedWhereClause(childTable, this.extraWhere, [], []);
|
|
3124
|
+
const extraSql = extra.clause.replace(/^ WHERE /, "");
|
|
3125
|
+
const sql = `SELECT 1 FROM ${quoteIdentifier(childTable)} WHERE ${qualifyColumn(childTable, this.relation.foreignKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
|
|
3126
|
+
return { sql, params: extra.params };
|
|
3127
|
+
}
|
|
3128
|
+
scopedQuery() {
|
|
3129
|
+
const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
|
|
3130
|
+
let query = repository.query(asWhere({
|
|
3131
|
+
[this.relation.foreignKey]: this.parent.get(this.relation.localKey),
|
|
3132
|
+
...this.extraWhere
|
|
3133
|
+
}));
|
|
3134
|
+
if (this.extraOptions.orderBy) {
|
|
3135
|
+
query = query.orderBy(this.extraOptions.orderBy);
|
|
3136
|
+
}
|
|
3137
|
+
if (this.extraOptions.limit !== undefined) {
|
|
3138
|
+
query = query.limit(this.extraOptions.limit);
|
|
3139
|
+
}
|
|
3140
|
+
return query;
|
|
3141
|
+
}
|
|
3142
|
+
async get() {
|
|
3143
|
+
const rows = await this.scopedQuery().get();
|
|
3144
|
+
return rows.map((row) => this.related.newFromRecord(row));
|
|
3145
|
+
}
|
|
3146
|
+
async first() {
|
|
3147
|
+
const rows = await this.limit(1).get();
|
|
3148
|
+
return rows[0] ?? null;
|
|
3149
|
+
}
|
|
3150
|
+
async count() {
|
|
3151
|
+
return (await this.get()).length;
|
|
3152
|
+
}
|
|
3153
|
+
async create(attributes = {}) {
|
|
3154
|
+
return this.related.create(attributes, {
|
|
3155
|
+
[this.relation.foreignKey]: this.parent.get(this.relation.localKey)
|
|
3156
|
+
});
|
|
3157
|
+
}
|
|
3158
|
+
async createMany(records) {
|
|
3159
|
+
const created = [];
|
|
3160
|
+
for (const attributes of records) {
|
|
3161
|
+
created.push(await this.create(attributes));
|
|
3162
|
+
}
|
|
3163
|
+
return created;
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
class HasOneRelationQuery {
|
|
3168
|
+
relation;
|
|
3169
|
+
kind = "hasOne";
|
|
3170
|
+
inner;
|
|
3171
|
+
constructor(parent, related, relation) {
|
|
3172
|
+
this.relation = relation;
|
|
3173
|
+
this.inner = new HasManyRelationQuery(parent, related, {
|
|
3174
|
+
type: "hasMany",
|
|
3175
|
+
name: relation.name,
|
|
3176
|
+
localKey: relation.localKey,
|
|
3177
|
+
foreignKey: relation.foreignKey
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
3180
|
+
where(where) {
|
|
3181
|
+
this.inner.where(where);
|
|
3182
|
+
return this;
|
|
3183
|
+
}
|
|
3184
|
+
orderBy(orderBy) {
|
|
3185
|
+
this.inner.orderBy(orderBy);
|
|
3186
|
+
return this;
|
|
3187
|
+
}
|
|
3188
|
+
applyEagerLoad(query, alias) {
|
|
3189
|
+
this.inner.limit(1).applyEagerLoad(query, alias);
|
|
3190
|
+
}
|
|
3191
|
+
hydrateEager(row, alias) {
|
|
3192
|
+
const hydrated = this.inner.hydrateEager(row, alias);
|
|
3193
|
+
return hydrated[0];
|
|
3194
|
+
}
|
|
3195
|
+
toExistsClause(parentTable) {
|
|
3196
|
+
return this.inner.toExistsClause(parentTable);
|
|
3197
|
+
}
|
|
3198
|
+
async get() {
|
|
3199
|
+
return this.inner.limit(1).first();
|
|
3200
|
+
}
|
|
3201
|
+
async first() {
|
|
3202
|
+
return this.get();
|
|
3203
|
+
}
|
|
3204
|
+
async count() {
|
|
3205
|
+
return await this.get() ? 1 : 0;
|
|
3206
|
+
}
|
|
3207
|
+
async create(attributes = {}) {
|
|
3208
|
+
return this.inner.create(attributes);
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
class BelongsToRelationQuery {
|
|
3213
|
+
parent;
|
|
3214
|
+
related;
|
|
3215
|
+
relation;
|
|
3216
|
+
kind = "belongsTo";
|
|
3217
|
+
extraOptions = {};
|
|
3218
|
+
constructor(parent, related, relation) {
|
|
3219
|
+
this.parent = parent;
|
|
3220
|
+
this.related = related;
|
|
3221
|
+
this.relation = relation;
|
|
3222
|
+
}
|
|
3223
|
+
orderBy(orderBy) {
|
|
3224
|
+
this.extraOptions = { ...this.extraOptions, orderBy };
|
|
3225
|
+
return this;
|
|
3226
|
+
}
|
|
3227
|
+
applyEagerLoad(query, alias) {
|
|
3228
|
+
query.withBelongsTo(alias, this.relation, this.related.repository(), this.extraOptions);
|
|
3229
|
+
}
|
|
3230
|
+
hydrateEager(row, alias) {
|
|
3231
|
+
const value = row[alias];
|
|
3232
|
+
return value ? this.related.newFromRecord(value) : value;
|
|
3233
|
+
}
|
|
3234
|
+
toExistsClause(parentTable) {
|
|
3235
|
+
const relatedTable = this.related.repository().getTable().name;
|
|
3236
|
+
return {
|
|
3237
|
+
sql: `SELECT 1 FROM ${quoteIdentifier(relatedTable)} WHERE ${qualifyColumn(relatedTable, this.relation.ownerKey)} = ${qualifyColumn(parentTable, this.relation.foreignKey)}`,
|
|
3238
|
+
params: []
|
|
3239
|
+
};
|
|
3240
|
+
}
|
|
3241
|
+
async get() {
|
|
3242
|
+
const foreign = this.parent.get(this.relation.foreignKey);
|
|
3243
|
+
if (foreign === null || foreign === undefined) {
|
|
3244
|
+
return null;
|
|
3245
|
+
}
|
|
3246
|
+
const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
|
|
3247
|
+
let query = repository.query(asWhere({ [this.relation.ownerKey]: foreign }));
|
|
3248
|
+
if (this.extraOptions.orderBy) {
|
|
3249
|
+
query = query.orderBy(this.extraOptions.orderBy);
|
|
3250
|
+
}
|
|
3251
|
+
const row = await query.first();
|
|
3252
|
+
return row ? this.related.newFromRecord(row) : null;
|
|
3253
|
+
}
|
|
3254
|
+
async first() {
|
|
3255
|
+
return this.get();
|
|
3256
|
+
}
|
|
3257
|
+
async associate(owner) {
|
|
3258
|
+
await this.parent.getRepository().updateById(this.parent.id, {
|
|
3259
|
+
[this.relation.foreignKey]: ownerId(owner, this.relation.ownerKey)
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3262
|
+
async dissociate() {
|
|
3263
|
+
await this.parent.getRepository().updateById(this.parent.id, {
|
|
3264
|
+
[this.relation.foreignKey]: null
|
|
3265
|
+
});
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
class BelongsToManyRelationQuery {
|
|
3270
|
+
parent;
|
|
3271
|
+
related;
|
|
3272
|
+
relation;
|
|
3273
|
+
kind = "belongsToMany";
|
|
3274
|
+
extraWhere = {};
|
|
3275
|
+
extraOptions = {};
|
|
3276
|
+
constructor(parent, related, relation) {
|
|
3277
|
+
this.parent = parent;
|
|
3278
|
+
this.related = related;
|
|
3279
|
+
this.relation = relation;
|
|
3280
|
+
}
|
|
3281
|
+
where(where) {
|
|
3282
|
+
this.extraWhere = { ...this.extraWhere, ...where };
|
|
3283
|
+
return this;
|
|
3284
|
+
}
|
|
3285
|
+
orderBy(orderBy) {
|
|
3286
|
+
this.extraOptions = { ...this.extraOptions, orderBy };
|
|
3287
|
+
return this;
|
|
3288
|
+
}
|
|
3289
|
+
applyEagerLoad(_query, _alias) {}
|
|
3290
|
+
hydrateEager(row, alias) {
|
|
3291
|
+
const value = row[alias] ?? [];
|
|
3292
|
+
const rows = Array.isArray(value) ? value : [];
|
|
3293
|
+
return rows.map((item) => this.related.newFromRecord(item));
|
|
3294
|
+
}
|
|
3295
|
+
toExistsClause(parentTable) {
|
|
3296
|
+
const relatedTable = this.related.repository().getTable().name;
|
|
3297
|
+
const extra = buildAdvancedWhereClause(relatedTable, this.extraWhere, [], []);
|
|
3298
|
+
const extraSql = extra.clause.replace(/^ WHERE /, "");
|
|
3299
|
+
const sql = `SELECT 1 FROM ${quoteIdentifier(relatedTable)} INNER JOIN ${quoteIdentifier(this.relation.pivotTable)} ON ${qualifyColumn(this.relation.pivotTable, this.relation.relatedPivotKey)} = ${qualifyColumn(relatedTable, this.relation.relatedKey)} WHERE ${qualifyColumn(this.relation.pivotTable, this.relation.foreignPivotKey)} = ${qualifyColumn(parentTable, this.relation.parentKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
|
|
3300
|
+
return { sql, params: extra.params };
|
|
3301
|
+
}
|
|
3302
|
+
connection() {
|
|
3303
|
+
return this.parent.getRepository().getConnection();
|
|
3304
|
+
}
|
|
3305
|
+
async get() {
|
|
3306
|
+
const parentId = this.parent.get(this.relation.parentKey);
|
|
3307
|
+
const pivotRows = await this.connection().unsafe(`SELECT * FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1`, [parentId]);
|
|
3308
|
+
if (pivotRows.length === 0) {
|
|
3309
|
+
return [];
|
|
3310
|
+
}
|
|
3311
|
+
const relatedIds = [
|
|
3312
|
+
...new Set(pivotRows.map((row) => row[this.relation.relatedPivotKey]))
|
|
3313
|
+
];
|
|
3314
|
+
const repository = this.related.repository().withConnection(this.connection());
|
|
3315
|
+
const rows = await repository.findAll({
|
|
3316
|
+
...this.extraOptions,
|
|
3317
|
+
where: asWhere({
|
|
3318
|
+
[this.relation.relatedKey]: relatedIds,
|
|
3319
|
+
...this.extraWhere
|
|
3320
|
+
})
|
|
3321
|
+
});
|
|
3322
|
+
return rows.map((row) => this.related.newFromRecord(row));
|
|
3323
|
+
}
|
|
3324
|
+
async first() {
|
|
3325
|
+
const rows = await this.get();
|
|
3326
|
+
return rows[0] ?? null;
|
|
3327
|
+
}
|
|
3328
|
+
async count() {
|
|
3329
|
+
return (await this.get()).length;
|
|
3330
|
+
}
|
|
3331
|
+
async attach(ids) {
|
|
3332
|
+
const list = Array.isArray(ids) ? ids : [ids];
|
|
3333
|
+
const parentId = this.parent.get(this.relation.parentKey);
|
|
3334
|
+
for (const id of list) {
|
|
3335
|
+
await this.connection().unsafe(`INSERT INTO ${this.relation.pivotTable} (${String(this.relation.foreignPivotKey)}, ${String(this.relation.relatedPivotKey)}) VALUES ($1, $2)`, [parentId, id]);
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
async detach(ids) {
|
|
3339
|
+
const parentId = this.parent.get(this.relation.parentKey);
|
|
3340
|
+
if (ids === undefined) {
|
|
3341
|
+
await this.connection().unsafe(`DELETE FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1`, [parentId]);
|
|
3342
|
+
return;
|
|
3343
|
+
}
|
|
3344
|
+
const list = Array.isArray(ids) ? ids : [ids];
|
|
3345
|
+
await this.connection().unsafe(`DELETE FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1 AND ${String(this.relation.relatedPivotKey)} = ANY($2)`, [parentId, list]);
|
|
3346
|
+
}
|
|
3347
|
+
async sync(ids) {
|
|
3348
|
+
await this.detach();
|
|
3349
|
+
if (ids.length > 0) {
|
|
3350
|
+
await this.attach(ids);
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
async create(attributes = {}) {
|
|
3354
|
+
const related = await this.related.create(attributes);
|
|
3355
|
+
await this.attach(related.id);
|
|
3356
|
+
return related;
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
class MorphManyRelationQuery {
|
|
3361
|
+
parent;
|
|
3362
|
+
related;
|
|
3363
|
+
relation;
|
|
3364
|
+
kind = "morphMany";
|
|
3365
|
+
extraWhere = {};
|
|
3366
|
+
extraOptions = {};
|
|
3367
|
+
constructor(parent, related, relation) {
|
|
3368
|
+
this.parent = parent;
|
|
3369
|
+
this.related = related;
|
|
3370
|
+
this.relation = relation;
|
|
3371
|
+
}
|
|
3372
|
+
where(where) {
|
|
3373
|
+
this.extraWhere = { ...this.extraWhere, ...where };
|
|
3374
|
+
return this;
|
|
3375
|
+
}
|
|
3376
|
+
applyEagerLoad(query, alias) {
|
|
3377
|
+
query.withMorphMany(alias, this.relation, this.related.repository(), this.extraOptions);
|
|
3378
|
+
}
|
|
3379
|
+
hydrateEager(row, alias) {
|
|
3380
|
+
const value = row[alias] ?? [];
|
|
3381
|
+
const rows = Array.isArray(value) ? value : [];
|
|
3382
|
+
return rows.map((item) => this.related.newFromRecord(item));
|
|
3383
|
+
}
|
|
3384
|
+
toExistsClause(parentTable) {
|
|
3385
|
+
const childTable = this.related.repository().getTable().name;
|
|
3386
|
+
const extra = buildAdvancedWhereClause(childTable, {
|
|
3387
|
+
[this.relation.morphTypeKey]: this.relation.morphType,
|
|
3388
|
+
...this.extraWhere
|
|
3389
|
+
}, [], []);
|
|
3390
|
+
const extraSql = extra.clause.replace(/^ WHERE /, "");
|
|
3391
|
+
const sql = `SELECT 1 FROM ${quoteIdentifier(childTable)} WHERE ${qualifyColumn(childTable, this.relation.morphIdKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
|
|
3392
|
+
return { sql, params: extra.params };
|
|
3393
|
+
}
|
|
3394
|
+
async get() {
|
|
3395
|
+
const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
|
|
3396
|
+
const rows = await repository.query(asWhere({
|
|
3397
|
+
[this.relation.morphTypeKey]: this.relation.morphType,
|
|
3398
|
+
[this.relation.morphIdKey]: this.parent.get(this.relation.localKey),
|
|
3399
|
+
...this.extraWhere
|
|
3400
|
+
})).get();
|
|
3401
|
+
return rows.map((row) => this.related.newFromRecord(row));
|
|
3402
|
+
}
|
|
3403
|
+
async first() {
|
|
3404
|
+
const rows = await this.get();
|
|
3405
|
+
return rows[0] ?? null;
|
|
3406
|
+
}
|
|
3407
|
+
async create(attributes = {}) {
|
|
3408
|
+
return this.related.create(attributes, {
|
|
3409
|
+
[this.relation.morphTypeKey]: this.relation.morphType,
|
|
3410
|
+
[this.relation.morphIdKey]: this.parent.get(this.relation.localKey)
|
|
3411
|
+
});
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
class MorphOneRelationQuery {
|
|
3416
|
+
relation;
|
|
3417
|
+
kind = "morphOne";
|
|
3418
|
+
inner;
|
|
3419
|
+
constructor(parent, related, relation) {
|
|
3420
|
+
this.relation = relation;
|
|
3421
|
+
this.inner = new MorphManyRelationQuery(parent, related, {
|
|
3422
|
+
type: "morphMany",
|
|
3423
|
+
name: relation.name,
|
|
3424
|
+
localKey: relation.localKey,
|
|
3425
|
+
morphTypeKey: relation.morphTypeKey,
|
|
3426
|
+
morphIdKey: relation.morphIdKey,
|
|
3427
|
+
morphType: relation.morphType
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
where(where) {
|
|
3431
|
+
this.inner.where(where);
|
|
3432
|
+
return this;
|
|
3433
|
+
}
|
|
3434
|
+
applyEagerLoad(query, alias) {
|
|
3435
|
+
this.inner.applyEagerLoad(query, alias);
|
|
3436
|
+
}
|
|
3437
|
+
hydrateEager(row, alias) {
|
|
3438
|
+
const hydrated = this.inner.hydrateEager(row, alias);
|
|
3439
|
+
return hydrated[0];
|
|
3440
|
+
}
|
|
3441
|
+
toExistsClause(parentTable) {
|
|
3442
|
+
return this.inner.toExistsClause(parentTable);
|
|
3443
|
+
}
|
|
3444
|
+
async get() {
|
|
3445
|
+
return this.inner.first();
|
|
3446
|
+
}
|
|
3447
|
+
async create(attributes = {}) {
|
|
3448
|
+
return this.inner.create(attributes);
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
class MorphToRelationQuery {
|
|
3453
|
+
parent;
|
|
3454
|
+
relatedByType;
|
|
3455
|
+
relation;
|
|
3456
|
+
kind = "morphTo";
|
|
3457
|
+
constructor(parent, relatedByType, relation) {
|
|
3458
|
+
this.parent = parent;
|
|
3459
|
+
this.relatedByType = relatedByType;
|
|
3460
|
+
this.relation = relation;
|
|
3461
|
+
}
|
|
3462
|
+
applyEagerLoad(query, alias) {
|
|
3463
|
+
const repositories = new Map(Object.entries(this.relatedByType).map(([type, model]) => [
|
|
3464
|
+
type,
|
|
3465
|
+
model.repository()
|
|
3466
|
+
]));
|
|
3467
|
+
query.withMorphTo(alias, this.relation, repositories);
|
|
3468
|
+
}
|
|
3469
|
+
hydrateEager(row, alias) {
|
|
3470
|
+
return row[alias];
|
|
3471
|
+
}
|
|
3472
|
+
toExistsClause(parentTable) {
|
|
3473
|
+
const type = String(this.parent.get(this.relation.morphTypeKey) ?? "");
|
|
3474
|
+
const related = this.relatedByType[type];
|
|
3475
|
+
if (!related) {
|
|
3476
|
+
return { sql: "SELECT 1 WHERE 1 = 0", params: [] };
|
|
3477
|
+
}
|
|
3478
|
+
const relatedTable = related.repository().getTable();
|
|
3479
|
+
return {
|
|
3480
|
+
sql: `SELECT 1 FROM ${quoteIdentifier(relatedTable.name)} WHERE ${qualifyColumn(relatedTable.name, relatedTable.primaryKey)} = ${qualifyColumn(parentTable, this.relation.morphIdKey)}`,
|
|
3481
|
+
params: []
|
|
3482
|
+
};
|
|
3483
|
+
}
|
|
3484
|
+
async get() {
|
|
3485
|
+
const type = String(this.parent.get(this.relation.morphTypeKey) ?? "");
|
|
3486
|
+
const id = this.parent.get(this.relation.morphIdKey);
|
|
3487
|
+
const related = this.relatedByType[type];
|
|
3488
|
+
if (!related || id === null || id === undefined) {
|
|
3489
|
+
return null;
|
|
3490
|
+
}
|
|
3491
|
+
const table = related.repository().getTable();
|
|
3492
|
+
const row = await related.repository().withConnection(this.parent.getRepository().getConnection()).query(asWhere({ [table.primaryKey]: id })).first();
|
|
3493
|
+
return row ? related.newFromRecord(row) : null;
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
|
|
2881
3497
|
// ../../src/core/database/model.ts
|
|
2882
3498
|
var modelRepositories = new WeakMap;
|
|
2883
3499
|
var modelGlobalScopes = new WeakMap;
|
|
3500
|
+
var modelObservers = new WeakMap;
|
|
2884
3501
|
var modelBooted = new WeakSet;
|
|
3502
|
+
async function runObservers(model, hook) {
|
|
3503
|
+
const observers = modelObservers.get(model.constructor) ?? [];
|
|
3504
|
+
for (const observer of observers) {
|
|
3505
|
+
const handler = observer[hook];
|
|
3506
|
+
if (handler && await handler(model) === false) {
|
|
3507
|
+
return false;
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
return true;
|
|
3511
|
+
}
|
|
3512
|
+
function accessorName(key) {
|
|
3513
|
+
const pascal = key.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase());
|
|
3514
|
+
return `get${pascal.charAt(0).toUpperCase()}${pascal.slice(1)}Attribute`;
|
|
3515
|
+
}
|
|
3516
|
+
function constrainRelationExists(model, name, constrain, not) {
|
|
3517
|
+
const statics = modelStatics(model);
|
|
3518
|
+
ensureBooted(model);
|
|
3519
|
+
const repository = resolveModelRepository(model);
|
|
3520
|
+
const dummy = statics.newFromRecord({});
|
|
3521
|
+
const method = dummy[name];
|
|
3522
|
+
if (typeof method !== "function") {
|
|
3523
|
+
throw new Error(`${model.name} has no relation method ${name}().`);
|
|
3524
|
+
}
|
|
3525
|
+
const relationQuery = method.call(dummy);
|
|
3526
|
+
constrain?.(relationQuery);
|
|
3527
|
+
const exists = relationQuery.toExistsClause(repository.getTable().name);
|
|
3528
|
+
const query = Model.query.call(model);
|
|
3529
|
+
return not ? query.whereNotExists(exists.sql, exists.params) : query.whereExists(exists.sql, exists.params);
|
|
3530
|
+
}
|
|
3531
|
+
async function loadNested(model, path) {
|
|
3532
|
+
const [head, ...rest] = path.split(".");
|
|
3533
|
+
if (!head) {
|
|
3534
|
+
return;
|
|
3535
|
+
}
|
|
3536
|
+
await model.load(head);
|
|
3537
|
+
if (rest.length === 0) {
|
|
3538
|
+
return;
|
|
3539
|
+
}
|
|
3540
|
+
const loaded = model.loaded(head);
|
|
3541
|
+
const children = Array.isArray(loaded) ? loaded : loaded ? [loaded] : [];
|
|
3542
|
+
for (const child of children) {
|
|
3543
|
+
if (child && typeof child === "object" && typeof child.load === "function") {
|
|
3544
|
+
await loadNested(child, rest.join("."));
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
2885
3548
|
function resolveModelRepository(model) {
|
|
2886
3549
|
const repository = modelRepositories.get(model);
|
|
2887
3550
|
if (!repository) {
|
|
@@ -2996,12 +3659,22 @@ class Model {
|
|
|
2996
3659
|
static $guarded;
|
|
2997
3660
|
static $casts = {};
|
|
2998
3661
|
static $timestamps = true;
|
|
3662
|
+
static $hidden;
|
|
3663
|
+
static $visible;
|
|
3664
|
+
static $appends;
|
|
2999
3665
|
_exists;
|
|
3666
|
+
loadedRelations = {};
|
|
3667
|
+
hiddenOverrides = [];
|
|
3668
|
+
visibleOverrides = [];
|
|
3669
|
+
appended = [];
|
|
3000
3670
|
constructor(attributes, repository, exists = true) {
|
|
3001
3671
|
this.attributes = attributes;
|
|
3002
3672
|
this.repository = repository;
|
|
3003
3673
|
this._exists = exists;
|
|
3004
3674
|
}
|
|
3675
|
+
getRepository() {
|
|
3676
|
+
return this.repository;
|
|
3677
|
+
}
|
|
3005
3678
|
get $exists() {
|
|
3006
3679
|
return this._exists;
|
|
3007
3680
|
}
|
|
@@ -3014,6 +3687,49 @@ class Model {
|
|
|
3014
3687
|
toObject() {
|
|
3015
3688
|
return { ...this.attributes };
|
|
3016
3689
|
}
|
|
3690
|
+
toArray() {
|
|
3691
|
+
const ModelClass = modelStatics(this.constructor);
|
|
3692
|
+
const hidden = new Set([...ModelClass.$hidden ?? [], ...this.hiddenOverrides]);
|
|
3693
|
+
const visible = this.visibleOverrides.length > 0 ? this.visibleOverrides : ModelClass.$visible;
|
|
3694
|
+
const data = { ...this.attributes };
|
|
3695
|
+
if (visible && visible.length > 0) {
|
|
3696
|
+
for (const key of Object.keys(data)) {
|
|
3697
|
+
if (!visible.includes(key)) {
|
|
3698
|
+
delete data[key];
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
for (const key of hidden) {
|
|
3703
|
+
delete data[key];
|
|
3704
|
+
}
|
|
3705
|
+
for (const key of [...ModelClass.$appends ?? [], ...this.appended]) {
|
|
3706
|
+
const accessor = this[accessorName(key)];
|
|
3707
|
+
if (typeof accessor === "function") {
|
|
3708
|
+
data[key] = accessor.call(this);
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
for (const [name, value] of Object.entries(this.loadedRelations)) {
|
|
3712
|
+
if (!hidden.has(name) && (!visible || visible.includes(name) || this.appended.includes(name))) {
|
|
3713
|
+
data[name] = value;
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
return data;
|
|
3717
|
+
}
|
|
3718
|
+
toJSON() {
|
|
3719
|
+
return this.toArray();
|
|
3720
|
+
}
|
|
3721
|
+
makeHidden(...keys) {
|
|
3722
|
+
this.hiddenOverrides.push(...keys);
|
|
3723
|
+
return this;
|
|
3724
|
+
}
|
|
3725
|
+
makeVisible(...keys) {
|
|
3726
|
+
this.visibleOverrides.push(...keys);
|
|
3727
|
+
return this;
|
|
3728
|
+
}
|
|
3729
|
+
append(...keys) {
|
|
3730
|
+
this.appended.push(...keys);
|
|
3731
|
+
return this;
|
|
3732
|
+
}
|
|
3017
3733
|
primaryKey() {
|
|
3018
3734
|
throw new Error(`${this.constructor.name}.primaryKey() is not implemented.`);
|
|
3019
3735
|
}
|
|
@@ -3034,6 +3750,10 @@ class Model {
|
|
|
3034
3750
|
return new statics(hydrated, repository, exists);
|
|
3035
3751
|
}
|
|
3036
3752
|
static boot() {}
|
|
3753
|
+
static observe(observer) {
|
|
3754
|
+
const existing = modelObservers.get(this) ?? [];
|
|
3755
|
+
modelObservers.set(this, [...existing, observer]);
|
|
3756
|
+
}
|
|
3037
3757
|
static addGlobalScope(_name, scope) {
|
|
3038
3758
|
ensureBooted(this);
|
|
3039
3759
|
const existing = modelGlobalScopes.get(this) ?? [];
|
|
@@ -3054,17 +3774,89 @@ class Model {
|
|
|
3054
3774
|
}
|
|
3055
3775
|
return query;
|
|
3056
3776
|
}
|
|
3057
|
-
static
|
|
3777
|
+
static newFromRecord(record, exists = true) {
|
|
3778
|
+
const repository = resolveModelRepository(this);
|
|
3779
|
+
return modelStatics(this).fromRecord(record, repository, exists);
|
|
3780
|
+
}
|
|
3781
|
+
static async create(attributes, forced = {}) {
|
|
3058
3782
|
const statics = modelStatics(this);
|
|
3059
3783
|
ensureBooted(this);
|
|
3060
3784
|
const repository = resolveModelRepository(this);
|
|
3061
3785
|
const table = repository.getTable();
|
|
3062
3786
|
const timestamps = statics.$timestamps ?? true;
|
|
3063
|
-
const assignable =
|
|
3787
|
+
const assignable = {
|
|
3788
|
+
...filterMassAssignable(statics.$fillable, statics.$guarded, attributes),
|
|
3789
|
+
...forced
|
|
3790
|
+
};
|
|
3064
3791
|
const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
|
|
3065
3792
|
const payload = statics.dehydrateAttributes(withTimestamps);
|
|
3793
|
+
const pending = statics.newFromRecord({ ...payload }, false);
|
|
3794
|
+
if (await runObservers(pending, "creating") === false) {
|
|
3795
|
+
throw new Error(`${this.name}.create() was cancelled by an observer.`);
|
|
3796
|
+
}
|
|
3066
3797
|
const record = await repository.create(payload);
|
|
3067
|
-
|
|
3798
|
+
const created = statics.fromRecord(record, repository, true);
|
|
3799
|
+
await runObservers(created, "created");
|
|
3800
|
+
return created;
|
|
3801
|
+
}
|
|
3802
|
+
static with(...relations) {
|
|
3803
|
+
const statics = modelStatics(this);
|
|
3804
|
+
ensureBooted(this);
|
|
3805
|
+
const repository = resolveModelRepository(this);
|
|
3806
|
+
const dummy = statics.fromRecord({}, repository, false);
|
|
3807
|
+
const resolved = relations.map((path) => {
|
|
3808
|
+
const name = path.split(".")[0] ?? path;
|
|
3809
|
+
const method = dummy[name];
|
|
3810
|
+
if (typeof method !== "function") {
|
|
3811
|
+
throw new Error(`${this.name} has no relation method ${name}().`);
|
|
3812
|
+
}
|
|
3813
|
+
const relationQuery = method.call(dummy);
|
|
3814
|
+
return { name, path, relationQuery };
|
|
3815
|
+
});
|
|
3816
|
+
const query = Model.query.call(this);
|
|
3817
|
+
for (const { name, relationQuery } of resolved) {
|
|
3818
|
+
if (relationQuery.kind !== "belongsToMany") {
|
|
3819
|
+
relationQuery.applyEagerLoad(query, name);
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
return {
|
|
3823
|
+
async get() {
|
|
3824
|
+
const rows = await query.get();
|
|
3825
|
+
const models = [];
|
|
3826
|
+
for (const row of rows) {
|
|
3827
|
+
const model = statics.fromRecord(row, repository, true);
|
|
3828
|
+
for (const { name, path, relationQuery } of resolved) {
|
|
3829
|
+
if (relationQuery.kind === "belongsToMany") {
|
|
3830
|
+
await model.load(path.includes(".") ? path : name);
|
|
3831
|
+
continue;
|
|
3832
|
+
}
|
|
3833
|
+
model.setLoaded(name, relationQuery.hydrateEager(row, name));
|
|
3834
|
+
const nested = path.split(".").slice(1).join(".");
|
|
3835
|
+
if (nested) {
|
|
3836
|
+
await loadNested(model, path);
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
models.push(model);
|
|
3840
|
+
}
|
|
3841
|
+
return models;
|
|
3842
|
+
},
|
|
3843
|
+
async first() {
|
|
3844
|
+
const [model] = await this.get();
|
|
3845
|
+
return model ?? null;
|
|
3846
|
+
}
|
|
3847
|
+
};
|
|
3848
|
+
}
|
|
3849
|
+
static whereHas(name, constrain) {
|
|
3850
|
+
return constrainRelationExists(this, name, constrain, false);
|
|
3851
|
+
}
|
|
3852
|
+
static has(name) {
|
|
3853
|
+
return constrainRelationExists(this, name, undefined, false);
|
|
3854
|
+
}
|
|
3855
|
+
static doesntHave(name) {
|
|
3856
|
+
return constrainRelationExists(this, name, undefined, true);
|
|
3857
|
+
}
|
|
3858
|
+
static whereDoesntHave(name, constrain) {
|
|
3859
|
+
return constrainRelationExists(this, name, constrain, true);
|
|
3068
3860
|
}
|
|
3069
3861
|
static async find(id) {
|
|
3070
3862
|
const statics = modelStatics(this);
|
|
@@ -3093,6 +3885,9 @@ class Model {
|
|
|
3093
3885
|
const rows = await query.get();
|
|
3094
3886
|
return rows.map((row) => statics.fromRecord(row, repository, true));
|
|
3095
3887
|
}
|
|
3888
|
+
static where(where) {
|
|
3889
|
+
return Model.query.call(this).where(where);
|
|
3890
|
+
}
|
|
3096
3891
|
static async firstWhere(where, options = {}) {
|
|
3097
3892
|
const statics = modelStatics(this);
|
|
3098
3893
|
const repository = resolveModelRepository(this);
|
|
@@ -3103,15 +3898,41 @@ class Model {
|
|
|
3103
3898
|
const record = await query.first();
|
|
3104
3899
|
return record ? statics.fromRecord(record, repository, true) : null;
|
|
3105
3900
|
}
|
|
3901
|
+
static async firstOrNew(where, values = {}) {
|
|
3902
|
+
const existing = await Model.firstWhere.call(this, where);
|
|
3903
|
+
if (existing) {
|
|
3904
|
+
return existing;
|
|
3905
|
+
}
|
|
3906
|
+
return modelStatics(this).newFromRecord({ ...where, ...values }, false);
|
|
3907
|
+
}
|
|
3908
|
+
static async firstOrCreate(where, values = {}) {
|
|
3909
|
+
const existing = await Model.firstWhere.call(this, where);
|
|
3910
|
+
if (existing) {
|
|
3911
|
+
return existing;
|
|
3912
|
+
}
|
|
3913
|
+
return Model.create.call(this, { ...where, ...values });
|
|
3914
|
+
}
|
|
3915
|
+
static async updateOrCreate(where, values = {}) {
|
|
3916
|
+
const existing = await Model.firstWhere.call(this, where);
|
|
3917
|
+
if (existing) {
|
|
3918
|
+
return existing.update(values);
|
|
3919
|
+
}
|
|
3920
|
+
return Model.create.call(this, { ...where, ...values });
|
|
3921
|
+
}
|
|
3106
3922
|
async save() {
|
|
3107
3923
|
const ModelClass = modelStatics(this.constructor);
|
|
3108
3924
|
const timestamps = ModelClass.$timestamps ?? true;
|
|
3109
3925
|
const casts = ModelClass.$casts ?? {};
|
|
3110
3926
|
const table = this.repository.getTable();
|
|
3111
|
-
|
|
3927
|
+
const updating = this.$exists;
|
|
3928
|
+
if (await runObservers(this, updating ? "updating" : "creating") === false) {
|
|
3929
|
+
return this;
|
|
3930
|
+
}
|
|
3931
|
+
if (updating) {
|
|
3112
3932
|
const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
|
|
3113
3933
|
const record2 = await this.repository.updateByIdOrThrow(this.id, changes);
|
|
3114
3934
|
this.attributes = ModelClass.hydrateAttributes(record2);
|
|
3935
|
+
await runObservers(this, "updated");
|
|
3115
3936
|
return this;
|
|
3116
3937
|
}
|
|
3117
3938
|
const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, this.attributes);
|
|
@@ -3120,6 +3941,7 @@ class Model {
|
|
|
3120
3941
|
const record = await this.repository.create(payload);
|
|
3121
3942
|
this.attributes = ModelClass.hydrateAttributes(record);
|
|
3122
3943
|
this._exists = true;
|
|
3944
|
+
await runObservers(this, "created");
|
|
3123
3945
|
return this;
|
|
3124
3946
|
}
|
|
3125
3947
|
async update(changes) {
|
|
@@ -3129,10 +3951,14 @@ class Model {
|
|
|
3129
3951
|
return await this.save();
|
|
3130
3952
|
}
|
|
3131
3953
|
async delete() {
|
|
3132
|
-
if (
|
|
3133
|
-
return
|
|
3954
|
+
if (await runObservers(this, "deleting") === false) {
|
|
3955
|
+
return false;
|
|
3134
3956
|
}
|
|
3135
|
-
|
|
3957
|
+
const deleted = resolveSoftDeleteColumn(this.repository.getTable()) ? await this.repository.deleteById(this.id) : await this.repository.forceDeleteById(this.id);
|
|
3958
|
+
if (deleted) {
|
|
3959
|
+
await runObservers(this, "deleted");
|
|
3960
|
+
}
|
|
3961
|
+
return deleted;
|
|
3136
3962
|
}
|
|
3137
3963
|
async forceDelete() {
|
|
3138
3964
|
return await this.repository.forceDeleteById(this.id);
|
|
@@ -3181,6 +4007,91 @@ class Model {
|
|
|
3181
4007
|
const loaded = grouped.get(parentId) ?? [];
|
|
3182
4008
|
return Object.assign(this, { [as]: loaded });
|
|
3183
4009
|
}
|
|
4010
|
+
hasMany(related, foreignKey, localKey) {
|
|
4011
|
+
const table = this.repository.getTable();
|
|
4012
|
+
return new HasManyRelationQuery(this, related, hasMany({
|
|
4013
|
+
name: related.repository().getTable().name,
|
|
4014
|
+
localKey: localKey ?? table.primaryKey,
|
|
4015
|
+
foreignKey: foreignKey ?? foreignKeyFromTable(table.name)
|
|
4016
|
+
}));
|
|
4017
|
+
}
|
|
4018
|
+
hasOne(related, foreignKey, localKey) {
|
|
4019
|
+
const table = this.repository.getTable();
|
|
4020
|
+
return new HasOneRelationQuery(this, related, hasOne({
|
|
4021
|
+
name: related.repository().getTable().name,
|
|
4022
|
+
localKey: localKey ?? table.primaryKey,
|
|
4023
|
+
foreignKey: foreignKey ?? foreignKeyFromTable(table.name)
|
|
4024
|
+
}));
|
|
4025
|
+
}
|
|
4026
|
+
belongsTo(related, foreignKey, ownerKey) {
|
|
4027
|
+
const relatedTable = related.repository().getTable();
|
|
4028
|
+
return new BelongsToRelationQuery(this, related, belongsTo({
|
|
4029
|
+
name: relatedTable.name,
|
|
4030
|
+
foreignKey: foreignKey ?? foreignKeyFromTable(relatedTable.name),
|
|
4031
|
+
ownerKey: ownerKey ?? relatedTable.primaryKey
|
|
4032
|
+
}));
|
|
4033
|
+
}
|
|
4034
|
+
belongsToMany(related, pivotTable, foreignPivotKey, relatedPivotKey) {
|
|
4035
|
+
const table = this.repository.getTable();
|
|
4036
|
+
const relatedTable = related.repository().getTable();
|
|
4037
|
+
return new BelongsToManyRelationQuery(this, related, belongsToMany({
|
|
4038
|
+
name: relatedTable.name,
|
|
4039
|
+
pivotTable: pivotTable ?? pivotTableName(table.name, relatedTable.name),
|
|
4040
|
+
parentKey: table.primaryKey,
|
|
4041
|
+
relatedKey: relatedTable.primaryKey,
|
|
4042
|
+
foreignPivotKey: foreignPivotKey ?? foreignKeyFromTable(table.name),
|
|
4043
|
+
relatedPivotKey: relatedPivotKey ?? foreignKeyFromTable(relatedTable.name)
|
|
4044
|
+
}));
|
|
4045
|
+
}
|
|
4046
|
+
morphMany(related, morphName, typeKey, idKey) {
|
|
4047
|
+
const table = this.repository.getTable();
|
|
4048
|
+
return new MorphManyRelationQuery(this, related, morphMany({
|
|
4049
|
+
name: morphName,
|
|
4050
|
+
localKey: table.primaryKey,
|
|
4051
|
+
morphTypeKey: typeKey ?? `${morphName}_type`,
|
|
4052
|
+
morphIdKey: idKey ?? `${morphName}_id`,
|
|
4053
|
+
morphType: table.name
|
|
4054
|
+
}));
|
|
4055
|
+
}
|
|
4056
|
+
morphOne(related, morphName, typeKey, idKey) {
|
|
4057
|
+
const table = this.repository.getTable();
|
|
4058
|
+
return new MorphOneRelationQuery(this, related, morphOne({
|
|
4059
|
+
name: morphName,
|
|
4060
|
+
localKey: table.primaryKey,
|
|
4061
|
+
morphTypeKey: typeKey ?? `${morphName}_type`,
|
|
4062
|
+
morphIdKey: idKey ?? `${morphName}_id`,
|
|
4063
|
+
morphType: table.name
|
|
4064
|
+
}));
|
|
4065
|
+
}
|
|
4066
|
+
morphTo(relatedByType, morphName = "imageable", typeKey, idKey) {
|
|
4067
|
+
return new MorphToRelationQuery(this, relatedByType, morphTo({
|
|
4068
|
+
name: morphName,
|
|
4069
|
+
morphTypeKey: typeKey ?? `${morphName}_type`,
|
|
4070
|
+
morphIdKey: idKey ?? `${morphName}_id`
|
|
4071
|
+
}));
|
|
4072
|
+
}
|
|
4073
|
+
async load(...names) {
|
|
4074
|
+
for (const name of names) {
|
|
4075
|
+
if (name.includes(".")) {
|
|
4076
|
+
await loadNested(this, name);
|
|
4077
|
+
continue;
|
|
4078
|
+
}
|
|
4079
|
+
const method = this[name];
|
|
4080
|
+
if (typeof method !== "function") {
|
|
4081
|
+
throw new Error(`${this.constructor.name} has no relation method ${name}().`);
|
|
4082
|
+
}
|
|
4083
|
+
const relationQuery = method.call(this);
|
|
4084
|
+
this.loadedRelations[name] = await relationQuery.get();
|
|
4085
|
+
}
|
|
4086
|
+
return this;
|
|
4087
|
+
}
|
|
4088
|
+
loaded(name) {
|
|
4089
|
+
return this.loadedRelations[name];
|
|
4090
|
+
}
|
|
4091
|
+
setLoaded(name, value) {
|
|
4092
|
+
this.loadedRelations[name] = value;
|
|
4093
|
+
return this;
|
|
4094
|
+
}
|
|
3184
4095
|
mergeAttributes(patch) {
|
|
3185
4096
|
Object.assign(this.attributes, patch);
|
|
3186
4097
|
return this;
|
|
@@ -5079,6 +5990,64 @@ function createRequireAuthMiddleware(auth2) {
|
|
|
5079
5990
|
function serializeDate(value) {
|
|
5080
5991
|
return value instanceof Date ? value.toISOString() : value;
|
|
5081
5992
|
}
|
|
5993
|
+
function whenLoaded(model, relation, transform) {
|
|
5994
|
+
const value = model.loaded(relation);
|
|
5995
|
+
if (value === undefined) {
|
|
5996
|
+
return;
|
|
5997
|
+
}
|
|
5998
|
+
return transform ? transform(value) : value;
|
|
5999
|
+
}
|
|
6000
|
+
|
|
6001
|
+
class JsonResource {
|
|
6002
|
+
resource;
|
|
6003
|
+
static wrap = "data";
|
|
6004
|
+
extra = {};
|
|
6005
|
+
constructor(resource) {
|
|
6006
|
+
this.resource = resource;
|
|
6007
|
+
}
|
|
6008
|
+
static make(resource) {
|
|
6009
|
+
return new JsonResource(resource);
|
|
6010
|
+
}
|
|
6011
|
+
static collection(items) {
|
|
6012
|
+
return new ResourceCollection(items);
|
|
6013
|
+
}
|
|
6014
|
+
additional(data) {
|
|
6015
|
+
this.extra = { ...this.extra, ...data };
|
|
6016
|
+
return this;
|
|
6017
|
+
}
|
|
6018
|
+
when(condition, value) {
|
|
6019
|
+
return condition ? value : undefined;
|
|
6020
|
+
}
|
|
6021
|
+
whenLoaded(relation, transform) {
|
|
6022
|
+
const model = this.resource;
|
|
6023
|
+
if (typeof model.loaded !== "function") {
|
|
6024
|
+
return;
|
|
6025
|
+
}
|
|
6026
|
+
return whenLoaded(model, relation, transform);
|
|
6027
|
+
}
|
|
6028
|
+
toArray() {
|
|
6029
|
+
if (this.resource && typeof this.resource === "object" && "toArray" in this.resource) {
|
|
6030
|
+
return this.resource.toArray();
|
|
6031
|
+
}
|
|
6032
|
+
return { ...this.resource };
|
|
6033
|
+
}
|
|
6034
|
+
toResponse() {
|
|
6035
|
+
const wrap = this.constructor.wrap;
|
|
6036
|
+
const payload = this.toArray();
|
|
6037
|
+
if (wrap === null) {
|
|
6038
|
+
return { ...payload, ...this.extra };
|
|
6039
|
+
}
|
|
6040
|
+
return { [wrap]: payload, ...this.extra };
|
|
6041
|
+
}
|
|
6042
|
+
}
|
|
6043
|
+
|
|
6044
|
+
class ResourceCollection extends JsonResource {
|
|
6045
|
+
toArray() {
|
|
6046
|
+
return {
|
|
6047
|
+
data: this.resource.map((item) => item instanceof JsonResource ? item.toArray() : { ...item })
|
|
6048
|
+
};
|
|
6049
|
+
}
|
|
6050
|
+
}
|
|
5082
6051
|
function toResourceCollection(items, transformer) {
|
|
5083
6052
|
return items.map(transformer);
|
|
5084
6053
|
}
|
|
@@ -7161,6 +8130,8 @@ export {
|
|
|
7161
8130
|
AuthManager,
|
|
7162
8131
|
BadRequestError,
|
|
7163
8132
|
baseRepository_default as BaseRepository,
|
|
8133
|
+
BelongsToManyRelationQuery,
|
|
8134
|
+
BelongsToRelationQuery,
|
|
7164
8135
|
Blueprint,
|
|
7165
8136
|
CACHE_TAGS,
|
|
7166
8137
|
repository_default as CacheRepository,
|
|
@@ -7173,19 +8144,26 @@ export {
|
|
|
7173
8144
|
DatabaseTokenGuard,
|
|
7174
8145
|
EtaViewEngine,
|
|
7175
8146
|
EventBus,
|
|
8147
|
+
Factory,
|
|
7176
8148
|
failedJobRepository_default as FailedJobRepository,
|
|
7177
8149
|
failedJobService_default as FailedJobService,
|
|
7178
8150
|
ForbiddenError,
|
|
7179
8151
|
ForeignIdColumnDefinition,
|
|
7180
8152
|
FormRequest,
|
|
7181
8153
|
GuestGuard,
|
|
8154
|
+
HasManyRelationQuery,
|
|
8155
|
+
HasOneRelationQuery,
|
|
7182
8156
|
HttpError,
|
|
7183
8157
|
Job,
|
|
8158
|
+
JsonResource,
|
|
7184
8159
|
LocalStorageDriver,
|
|
7185
8160
|
LogMailDriver,
|
|
7186
8161
|
Mailer,
|
|
7187
8162
|
membershipService_default as MembershipService,
|
|
7188
8163
|
Model,
|
|
8164
|
+
MorphManyRelationQuery,
|
|
8165
|
+
MorphOneRelationQuery,
|
|
8166
|
+
MorphToRelationQuery,
|
|
7189
8167
|
MySqlGrammar,
|
|
7190
8168
|
NotFoundError,
|
|
7191
8169
|
Notification,
|
|
@@ -7200,6 +8178,7 @@ export {
|
|
|
7200
8178
|
RedisQueue,
|
|
7201
8179
|
RepositoryQuery,
|
|
7202
8180
|
ResilientQueue,
|
|
8181
|
+
ResourceCollection,
|
|
7203
8182
|
Schedule,
|
|
7204
8183
|
Schema,
|
|
7205
8184
|
ServiceContainer,
|
|
@@ -7297,6 +8276,7 @@ export {
|
|
|
7297
8276
|
eventBus,
|
|
7298
8277
|
events,
|
|
7299
8278
|
filterMassAssignable,
|
|
8279
|
+
foreignKeyFromTable,
|
|
7300
8280
|
formatAdminValue,
|
|
7301
8281
|
freshDatabase,
|
|
7302
8282
|
generateCspNonce,
|
|
@@ -7355,6 +8335,7 @@ export {
|
|
|
7355
8335
|
parseMultipartUpload,
|
|
7356
8336
|
parsePaginationQuery,
|
|
7357
8337
|
parsePositiveIntParam,
|
|
8338
|
+
pivotTableName,
|
|
7358
8339
|
policyGate,
|
|
7359
8340
|
prometheusRegistry,
|
|
7360
8341
|
queue,
|
|
@@ -7423,6 +8404,7 @@ export {
|
|
|
7423
8404
|
serverHtmxContentSecurityPolicy,
|
|
7424
8405
|
setActiveApplicationContext,
|
|
7425
8406
|
signedUrl,
|
|
8407
|
+
singularize,
|
|
7426
8408
|
spaContentSecurityPolicy,
|
|
7427
8409
|
storageFacade as storage,
|
|
7428
8410
|
strictApiContentSecurityPolicy,
|
|
@@ -7436,6 +8418,7 @@ export {
|
|
|
7436
8418
|
trustForwardedFor,
|
|
7437
8419
|
validateObject,
|
|
7438
8420
|
verifyCsrfToken,
|
|
8421
|
+
whenLoaded,
|
|
7439
8422
|
withErrorHandling,
|
|
7440
8423
|
withMiddleware,
|
|
7441
8424
|
withMigrationLock,
|