@bobbykim/manguito-cms-api 0.8.0 → 0.8.1
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/dist/index.js +183 -108
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +183 -108
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1255,7 +1255,7 @@ function createVersionedPaths(prefix, version) {
|
|
|
1255
1255
|
|
|
1256
1256
|
// src/app.ts
|
|
1257
1257
|
var import_hono = require("hono");
|
|
1258
|
-
var
|
|
1258
|
+
var import_drizzle_orm12 = require("drizzle-orm");
|
|
1259
1259
|
|
|
1260
1260
|
// src/middleware/cors.ts
|
|
1261
1261
|
function createCorsMiddleware(corsConfig) {
|
|
@@ -2270,7 +2270,7 @@ function registerPublicMediaRoutes(app, mediaRepo, paths, listRateLimit) {
|
|
|
2270
2270
|
}
|
|
2271
2271
|
|
|
2272
2272
|
// src/routes/admin/content.ts
|
|
2273
|
-
var
|
|
2273
|
+
var import_drizzle_orm5 = require("drizzle-orm");
|
|
2274
2274
|
|
|
2275
2275
|
// src/references-in-use.ts
|
|
2276
2276
|
var import_drizzle_orm2 = require("drizzle-orm");
|
|
@@ -2358,6 +2358,7 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
|
|
|
2358
2358
|
init_relations();
|
|
2359
2359
|
|
|
2360
2360
|
// src/relation-input.ts
|
|
2361
|
+
var import_drizzle_orm4 = require("drizzle-orm");
|
|
2361
2362
|
var import_manguito_cms_core3 = require("@bobbykim/manguito-cms-core");
|
|
2362
2363
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2363
2364
|
function isUuid(v) {
|
|
@@ -2402,16 +2403,92 @@ function checkRelationInput(fields, body, registry, path = "") {
|
|
|
2402
2403
|
}
|
|
2403
2404
|
return errors;
|
|
2404
2405
|
}
|
|
2406
|
+
function paragraphItemsIn(fields, body, registry, path) {
|
|
2407
|
+
const out = [];
|
|
2408
|
+
for (const field of fields) {
|
|
2409
|
+
if (field.field_type !== "paragraph" || !(field.name in body)) continue;
|
|
2410
|
+
const comp = field.ui_component;
|
|
2411
|
+
const pType = comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
|
|
2412
|
+
if (!pType) continue;
|
|
2413
|
+
const value = body[field.name];
|
|
2414
|
+
const name = `${path}${field.name}`;
|
|
2415
|
+
if ((0, import_manguito_cms_core3.relationCardinality)(field) === "one") {
|
|
2416
|
+
if (isPlainObject(value)) out.push({ item: value, fields: pType.fields, prefix: `${name}.` });
|
|
2417
|
+
} else if (Array.isArray(value)) {
|
|
2418
|
+
value.forEach((item, i) => {
|
|
2419
|
+
if (isPlainObject(item)) out.push({ item, fields: pType.fields, prefix: `${name}[${i}].` });
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
return out;
|
|
2424
|
+
}
|
|
2425
|
+
function isMissing(value) {
|
|
2426
|
+
if (value === null || value === void 0) return true;
|
|
2427
|
+
if (typeof value === "string" && value.trim() === "") return true;
|
|
2428
|
+
if (Array.isArray(value) && value.length === 0) return true;
|
|
2429
|
+
return false;
|
|
2430
|
+
}
|
|
2431
|
+
function checkRequiredInParagraphItems(fields, body, registry, path = "") {
|
|
2432
|
+
const errors = [];
|
|
2433
|
+
for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
|
|
2434
|
+
for (const f of itemFields) {
|
|
2435
|
+
if (!f.required || f.removed === true || !f.db_column || f.db_column.junction) continue;
|
|
2436
|
+
if (isMissing(item[f.name])) {
|
|
2437
|
+
errors.push({ field: `${prefix}${f.name}`, message: `${prefix}${f.name} is required.` });
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
errors.push(...checkRequiredInParagraphItems(itemFields, item, registry, prefix));
|
|
2441
|
+
}
|
|
2442
|
+
return errors;
|
|
2443
|
+
}
|
|
2444
|
+
function quoteIdent3(name) {
|
|
2445
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
2446
|
+
return `"${name}"`;
|
|
2447
|
+
}
|
|
2448
|
+
function collectReferences(fields, body, registry, path, out) {
|
|
2449
|
+
for (const f of fields) {
|
|
2450
|
+
if (!(f.name in body) || !f.db_column) continue;
|
|
2451
|
+
const table = f.db_column.junction ? f.db_column.junction.right_table : f.db_column.foreign_key?.table;
|
|
2452
|
+
if (!table) continue;
|
|
2453
|
+
const value = body[f.name];
|
|
2454
|
+
const name = `${path}${f.name}`;
|
|
2455
|
+
if (typeof value === "string") {
|
|
2456
|
+
out.push({ table, id: value, field: name });
|
|
2457
|
+
} else if (Array.isArray(value)) {
|
|
2458
|
+
value.forEach((v, i) => {
|
|
2459
|
+
if (typeof v === "string") out.push({ table, id: v, field: `${name}[${i}]` });
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
|
|
2464
|
+
collectReferences(itemFields, item, registry, prefix, out);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
async function findMissingReferences(db, fields, body, registry) {
|
|
2468
|
+
const uses = [];
|
|
2469
|
+
collectReferences(fields, body, registry, "", uses);
|
|
2470
|
+
if (uses.length === 0) return [];
|
|
2471
|
+
const lookups = [...new Set(uses.map((u) => u.table))].map(async (table) => {
|
|
2472
|
+
const ids = [...new Set(uses.filter((u) => u.table === table && isUuid(u.id)).map((u) => u.id.toLowerCase()))];
|
|
2473
|
+
if (ids.length === 0) return [table, /* @__PURE__ */ new Set()];
|
|
2474
|
+
const result = await db.execute(
|
|
2475
|
+
import_drizzle_orm4.sql`SELECT id FROM ${import_drizzle_orm4.sql.raw(quoteIdent3(table))} WHERE id IN (${import_drizzle_orm4.sql.join(ids.map((id) => import_drizzle_orm4.sql`${id}`), import_drizzle_orm4.sql`, `)})`
|
|
2476
|
+
);
|
|
2477
|
+
return [table, new Set(result.rows.map((r) => String(r.id).toLowerCase()))];
|
|
2478
|
+
});
|
|
2479
|
+
const found = new Map(await Promise.all(lookups));
|
|
2480
|
+
return uses.filter((u) => !found.get(u.table).has(u.id.toLowerCase())).map((u) => ({ field: u.field, message: `${u.field} refers to an item that does not exist.` }));
|
|
2481
|
+
}
|
|
2405
2482
|
|
|
2406
2483
|
// src/routes/admin/content.ts
|
|
2407
2484
|
init_field_keys();
|
|
2408
|
-
function
|
|
2485
|
+
function quoteIdent4(name) {
|
|
2409
2486
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
2410
2487
|
return `"${name}"`;
|
|
2411
2488
|
}
|
|
2412
2489
|
async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
|
|
2413
2490
|
const result = await db.execute(
|
|
2414
|
-
|
|
2491
|
+
import_drizzle_orm5.sql`SELECT * FROM ${import_drizzle_orm5.sql.raw(quoteIdent4(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC`
|
|
2415
2492
|
);
|
|
2416
2493
|
const rows = result.rows;
|
|
2417
2494
|
for (const nf of pType.fields) {
|
|
@@ -2431,27 +2508,25 @@ async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
|
|
|
2431
2508
|
}
|
|
2432
2509
|
async function lookupBasePathId(db, pathOrName) {
|
|
2433
2510
|
const r = await db.execute(
|
|
2434
|
-
|
|
2511
|
+
import_drizzle_orm5.sql`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
|
|
2435
2512
|
);
|
|
2436
2513
|
return r.rows[0]?.id ?? null;
|
|
2437
2514
|
}
|
|
2438
2515
|
var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2439
|
-
function isEmpty(value) {
|
|
2440
|
-
if (value === null || value === void 0) return true;
|
|
2441
|
-
if (typeof value === "string" && value.trim() === "") return true;
|
|
2442
|
-
if (Array.isArray(value) && value.length === 0) return true;
|
|
2443
|
-
return false;
|
|
2444
|
-
}
|
|
2445
2516
|
function checkRequiredFields(fields, data) {
|
|
2446
|
-
return fields.filter((f) => f.required &&
|
|
2447
|
-
}
|
|
2448
|
-
function
|
|
2449
|
-
const
|
|
2450
|
-
|
|
2451
|
-
return
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
)
|
|
2517
|
+
return fields.filter((f) => f.required && isMissing(data[f.name])).map((f) => ({ field: f.name, message: `${f.label} is required` }));
|
|
2518
|
+
}
|
|
2519
|
+
async function relationInputError(c, fields, body, registry, db) {
|
|
2520
|
+
const refuse = (message, details) => c.json({ ok: false, error: { code: "VALIDATION_ERROR", message, details } }, 422);
|
|
2521
|
+
const shape = checkRelationInput(fields, body, registry);
|
|
2522
|
+
if (shape.length > 0) return refuse("Relation fields have the wrong shape", shape);
|
|
2523
|
+
const required = checkRequiredInParagraphItems(fields, body, registry);
|
|
2524
|
+
if (required.length > 0) return refuse("Required fields are missing", required);
|
|
2525
|
+
if (db) {
|
|
2526
|
+
const missing = await findMissingReferences(db, fields, body, registry);
|
|
2527
|
+
if (missing.length > 0) return refuse("Relation fields refer to items that do not exist", missing);
|
|
2528
|
+
}
|
|
2529
|
+
return null;
|
|
2455
2530
|
}
|
|
2456
2531
|
function parseListQuery(url, schemaFieldNames, relationFieldNames, columnFor) {
|
|
2457
2532
|
const searchParams = new URL(url).searchParams;
|
|
@@ -2598,7 +2673,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
2598
2673
|
} else if (f.db_column.junction) {
|
|
2599
2674
|
const j = f.db_column.junction;
|
|
2600
2675
|
const r = await db.execute(
|
|
2601
|
-
|
|
2676
|
+
import_drizzle_orm5.sql`SELECT ${import_drizzle_orm5.sql.raw(quoteIdent4(j.right_column))} FROM ${import_drizzle_orm5.sql.raw(quoteIdent4(j.table_name))} WHERE ${import_drizzle_orm5.sql.raw(quoteIdent4(j.left_column))} = ${id}`
|
|
2602
2677
|
);
|
|
2603
2678
|
row[f.name] = r.rows.map(
|
|
2604
2679
|
(row2) => row2[j.right_column]
|
|
@@ -2674,8 +2749,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
2674
2749
|
}
|
|
2675
2750
|
);
|
|
2676
2751
|
const writeNewItem = async (c, body) => {
|
|
2677
|
-
const
|
|
2678
|
-
if (
|
|
2752
|
+
const inputError = await relationInputError(c, contentType.fields, body, registry, db);
|
|
2753
|
+
if (inputError) return inputError;
|
|
2679
2754
|
const storageBody = fieldKeys.toStorage(body);
|
|
2680
2755
|
const fieldErrors = checkRequiredFields(requiredFields, body);
|
|
2681
2756
|
if (fieldErrors.length > 0) {
|
|
@@ -2810,8 +2885,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
2810
2885
|
}
|
|
2811
2886
|
);
|
|
2812
2887
|
const writeExistingItem = async (c, id, existing, body) => {
|
|
2813
|
-
const
|
|
2814
|
-
if (
|
|
2888
|
+
const inputError = await relationInputError(c, contentType.fields, body, registry, db);
|
|
2889
|
+
if (inputError) return inputError;
|
|
2815
2890
|
const storageBody = fieldKeys.toStorage(body);
|
|
2816
2891
|
if (body["published"] === true) {
|
|
2817
2892
|
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
@@ -3009,8 +3084,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
3009
3084
|
requirePermission("content:create"),
|
|
3010
3085
|
async (c) => {
|
|
3011
3086
|
const body = await c.req.json();
|
|
3012
|
-
const
|
|
3013
|
-
if (
|
|
3087
|
+
const inputError = await relationInputError(c, taxonomyType.fields, body, registry, db);
|
|
3088
|
+
if (inputError) return inputError;
|
|
3014
3089
|
const storageBody = fieldKeys.toStorage(body);
|
|
3015
3090
|
const fieldErrors = checkRequiredFields(requiredFields, body);
|
|
3016
3091
|
if (fieldErrors.length > 0) {
|
|
@@ -3073,8 +3148,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
3073
3148
|
404
|
|
3074
3149
|
);
|
|
3075
3150
|
}
|
|
3076
|
-
const
|
|
3077
|
-
if (
|
|
3151
|
+
const inputError = await relationInputError(c, taxonomyType.fields, body, registry, db);
|
|
3152
|
+
if (inputError) return inputError;
|
|
3078
3153
|
if (body["published"] === true) {
|
|
3079
3154
|
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
3080
3155
|
});
|
|
@@ -3628,7 +3703,7 @@ function registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, ma
|
|
|
3628
3703
|
|
|
3629
3704
|
// src/routes/admin/auth.ts
|
|
3630
3705
|
var import_cookie3 = require("hono/cookie");
|
|
3631
|
-
var
|
|
3706
|
+
var import_drizzle_orm6 = require("drizzle-orm");
|
|
3632
3707
|
|
|
3633
3708
|
// src/auth/password.ts
|
|
3634
3709
|
var import_manguito_cms_core5 = require("@bobbykim/manguito-cms-core");
|
|
@@ -3694,7 +3769,7 @@ function registerAuthRoutes(app, db) {
|
|
|
3694
3769
|
);
|
|
3695
3770
|
}
|
|
3696
3771
|
const userResult = await db.execute(
|
|
3697
|
-
|
|
3772
|
+
import_drizzle_orm6.sql`SELECT id, email, password_hash, role_id, token_version
|
|
3698
3773
|
FROM users WHERE email = ${email} LIMIT 1`
|
|
3699
3774
|
);
|
|
3700
3775
|
const user = userResult.rows[0];
|
|
@@ -3707,7 +3782,7 @@ function registerAuthRoutes(app, db) {
|
|
|
3707
3782
|
return c.json(invalidCredentials, 401);
|
|
3708
3783
|
}
|
|
3709
3784
|
const roleResult = await db.execute(
|
|
3710
|
-
|
|
3785
|
+
import_drizzle_orm6.sql`SELECT r.name AS name, u.must_change_password
|
|
3711
3786
|
FROM roles r
|
|
3712
3787
|
JOIN users u ON u.role_id = r.id
|
|
3713
3788
|
WHERE r.id = ${user.role_id} AND u.id = ${user.id}
|
|
@@ -3767,7 +3842,7 @@ function registerAuthRoutes(app, db) {
|
|
|
3767
3842
|
);
|
|
3768
3843
|
}
|
|
3769
3844
|
const rowResult = await db.execute(
|
|
3770
|
-
|
|
3845
|
+
import_drizzle_orm6.sql`SELECT u.token_version, r.name AS role
|
|
3771
3846
|
FROM users u
|
|
3772
3847
|
JOIN roles r ON r.id = u.role_id
|
|
3773
3848
|
WHERE u.id = ${payload.user_id}
|
|
@@ -3791,7 +3866,7 @@ function registerAuthRoutes(app, db) {
|
|
|
3791
3866
|
app.post("/admin/api/auth/logout", authMiddleware, async (c) => {
|
|
3792
3867
|
const user = c.get("user");
|
|
3793
3868
|
await db.execute(
|
|
3794
|
-
|
|
3869
|
+
import_drizzle_orm6.sql`UPDATE users SET token_version = token_version + 1 WHERE id = ${user.id}`
|
|
3795
3870
|
);
|
|
3796
3871
|
clearAuthCookie(c, "auth_token");
|
|
3797
3872
|
clearAuthCookie(c, "refresh_token", "/admin/api/auth");
|
|
@@ -3800,7 +3875,7 @@ function registerAuthRoutes(app, db) {
|
|
|
3800
3875
|
}
|
|
3801
3876
|
|
|
3802
3877
|
// src/routes/admin/users.ts
|
|
3803
|
-
var
|
|
3878
|
+
var import_drizzle_orm7 = require("drizzle-orm");
|
|
3804
3879
|
var import_node_crypto2 = require("crypto");
|
|
3805
3880
|
function getActingUser(c) {
|
|
3806
3881
|
return c.get("user");
|
|
@@ -3835,7 +3910,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
3835
3910
|
);
|
|
3836
3911
|
}
|
|
3837
3912
|
const hashResult = await db.execute(
|
|
3838
|
-
|
|
3913
|
+
import_drizzle_orm7.sql`SELECT password_hash FROM users WHERE id = ${actingUser.id} LIMIT 1`
|
|
3839
3914
|
);
|
|
3840
3915
|
const hashRow = hashResult.rows[0];
|
|
3841
3916
|
if (!hashRow || !await (0, import_manguito_cms_core5.verifyPassword)(currentPassword, hashRow.password_hash)) {
|
|
@@ -3846,7 +3921,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
3846
3921
|
}
|
|
3847
3922
|
const newHash = await (0, import_manguito_cms_core5.hashPassword)(newPassword);
|
|
3848
3923
|
await db.execute(
|
|
3849
|
-
|
|
3924
|
+
import_drizzle_orm7.sql`UPDATE users
|
|
3850
3925
|
SET password_hash = ${newHash},
|
|
3851
3926
|
must_change_password = false,
|
|
3852
3927
|
token_version = token_version + 1,
|
|
@@ -3857,7 +3932,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
3857
3932
|
});
|
|
3858
3933
|
app.get("/admin/api/users", requirePermission("users:read"), async (c) => {
|
|
3859
3934
|
const result = await db.execute(
|
|
3860
|
-
|
|
3935
|
+
import_drizzle_orm7.sql`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
3861
3936
|
u.created_at, u.updated_at
|
|
3862
3937
|
FROM users u
|
|
3863
3938
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -3868,7 +3943,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
3868
3943
|
app.get("/admin/api/users/:id", requirePermission("users:read"), async (c) => {
|
|
3869
3944
|
const id = c.req.param("id");
|
|
3870
3945
|
const result = await db.execute(
|
|
3871
|
-
|
|
3946
|
+
import_drizzle_orm7.sql`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
3872
3947
|
u.created_at, u.updated_at
|
|
3873
3948
|
FROM users u
|
|
3874
3949
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -3913,7 +3988,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
3913
3988
|
);
|
|
3914
3989
|
}
|
|
3915
3990
|
const roleResult = await db.execute(
|
|
3916
|
-
|
|
3991
|
+
import_drizzle_orm7.sql`SELECT id FROM roles WHERE name = ${roleName} LIMIT 1`
|
|
3917
3992
|
);
|
|
3918
3993
|
const roleRow = roleResult.rows[0];
|
|
3919
3994
|
if (!roleRow) {
|
|
@@ -3928,7 +4003,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
3928
4003
|
const tempPassword = generateTempPassword();
|
|
3929
4004
|
const passwordHash = await (0, import_manguito_cms_core5.hashPassword)(tempPassword);
|
|
3930
4005
|
const insertResult = await db.execute(
|
|
3931
|
-
|
|
4006
|
+
import_drizzle_orm7.sql`INSERT INTO users
|
|
3932
4007
|
(id, email, password_hash, role_id, must_change_password, token_version,
|
|
3933
4008
|
created_at, updated_at)
|
|
3934
4009
|
VALUES
|
|
@@ -3983,7 +4058,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
3983
4058
|
);
|
|
3984
4059
|
}
|
|
3985
4060
|
const existingResult = await db.execute(
|
|
3986
|
-
|
|
4061
|
+
import_drizzle_orm7.sql`SELECT id, email, role_id FROM users WHERE id = ${id} LIMIT 1`
|
|
3987
4062
|
);
|
|
3988
4063
|
const existing = existingResult.rows[0];
|
|
3989
4064
|
if (!existing) {
|
|
@@ -3997,7 +4072,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
3997
4072
|
let changeRole = false;
|
|
3998
4073
|
if (typeof body.role === "string" && body.role) {
|
|
3999
4074
|
const roleResult = await db.execute(
|
|
4000
|
-
|
|
4075
|
+
import_drizzle_orm7.sql`SELECT id FROM roles WHERE name = ${body.role} LIMIT 1`
|
|
4001
4076
|
);
|
|
4002
4077
|
const roleRow = roleResult.rows[0];
|
|
4003
4078
|
if (!roleRow) {
|
|
@@ -4017,7 +4092,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
4017
4092
|
}
|
|
4018
4093
|
if (changeRole) {
|
|
4019
4094
|
await db.execute(
|
|
4020
|
-
|
|
4095
|
+
import_drizzle_orm7.sql`UPDATE users
|
|
4021
4096
|
SET email = ${newEmail},
|
|
4022
4097
|
role_id = ${newRoleId},
|
|
4023
4098
|
token_version = token_version + 1,
|
|
@@ -4026,7 +4101,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
4026
4101
|
);
|
|
4027
4102
|
} else {
|
|
4028
4103
|
await db.execute(
|
|
4029
|
-
|
|
4104
|
+
import_drizzle_orm7.sql`UPDATE users
|
|
4030
4105
|
SET email = ${newEmail},
|
|
4031
4106
|
role_id = ${newRoleId},
|
|
4032
4107
|
updated_at = NOW()
|
|
@@ -4034,7 +4109,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
4034
4109
|
);
|
|
4035
4110
|
}
|
|
4036
4111
|
const updatedResult = await db.execute(
|
|
4037
|
-
|
|
4112
|
+
import_drizzle_orm7.sql`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
4038
4113
|
u.created_at, u.updated_at
|
|
4039
4114
|
FROM users u
|
|
4040
4115
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -4071,7 +4146,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
4071
4146
|
);
|
|
4072
4147
|
}
|
|
4073
4148
|
const result = await db.execute(
|
|
4074
|
-
|
|
4149
|
+
import_drizzle_orm7.sql`DELETE FROM users WHERE id = ${id} RETURNING id`
|
|
4075
4150
|
);
|
|
4076
4151
|
if (result.rows.length === 0) {
|
|
4077
4152
|
return c.json(
|
|
@@ -4102,7 +4177,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
4102
4177
|
);
|
|
4103
4178
|
}
|
|
4104
4179
|
const existingResult = await db.execute(
|
|
4105
|
-
|
|
4180
|
+
import_drizzle_orm7.sql`SELECT id FROM users WHERE id = ${id} LIMIT 1`
|
|
4106
4181
|
);
|
|
4107
4182
|
const existing = existingResult.rows[0];
|
|
4108
4183
|
if (!existing) {
|
|
@@ -4114,7 +4189,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
4114
4189
|
const tempPassword = generateTempPassword();
|
|
4115
4190
|
const passwordHash = await (0, import_manguito_cms_core5.hashPassword)(tempPassword);
|
|
4116
4191
|
await db.execute(
|
|
4117
|
-
|
|
4192
|
+
import_drizzle_orm7.sql`UPDATE users
|
|
4118
4193
|
SET password_hash = ${passwordHash},
|
|
4119
4194
|
must_change_password = true,
|
|
4120
4195
|
token_version = token_version + 1,
|
|
@@ -4127,10 +4202,10 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
4127
4202
|
}
|
|
4128
4203
|
|
|
4129
4204
|
// src/routes/admin/config.ts
|
|
4130
|
-
var
|
|
4205
|
+
var import_drizzle_orm8 = require("drizzle-orm");
|
|
4131
4206
|
var API_VERSION = (() => {
|
|
4132
4207
|
try {
|
|
4133
|
-
return "0.8.
|
|
4208
|
+
return "0.8.1";
|
|
4134
4209
|
} catch {
|
|
4135
4210
|
return "0.0.0";
|
|
4136
4211
|
}
|
|
@@ -4149,7 +4224,7 @@ function registerConfigRoute(app, config, registry, db) {
|
|
|
4149
4224
|
permissions: r.permissions
|
|
4150
4225
|
}));
|
|
4151
4226
|
const userResult = await db.execute(
|
|
4152
|
-
|
|
4227
|
+
import_drizzle_orm8.sql`SELECT email, must_change_password FROM users WHERE id = ${actingUser.id} LIMIT 1`
|
|
4153
4228
|
);
|
|
4154
4229
|
const userRow = userResult.rows[0];
|
|
4155
4230
|
return c.json({
|
|
@@ -4175,7 +4250,7 @@ function registerConfigRoute(app, config, registry, db) {
|
|
|
4175
4250
|
}
|
|
4176
4251
|
|
|
4177
4252
|
// src/routes/admin/schema.ts
|
|
4178
|
-
var
|
|
4253
|
+
var import_drizzle_orm9 = require("drizzle-orm");
|
|
4179
4254
|
function liveFields(fields) {
|
|
4180
4255
|
return fields.filter((f) => f.removed !== true);
|
|
4181
4256
|
}
|
|
@@ -4215,7 +4290,7 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
4215
4290
|
const data = await Promise.all(
|
|
4216
4291
|
Object.values(registry.content_types).map(async (ct) => {
|
|
4217
4292
|
const result = await db.execute(
|
|
4218
|
-
|
|
4293
|
+
import_drizzle_orm9.sql`SELECT COUNT(*)::int AS count FROM ${import_drizzle_orm9.sql.raw(`"${ct.db.table_name}"`)}`
|
|
4219
4294
|
);
|
|
4220
4295
|
const count = Number(result.rows[0].count);
|
|
4221
4296
|
return { name: ct.name, label: ct.label, only_one: ct.only_one, count };
|
|
@@ -4227,7 +4302,7 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
4227
4302
|
const data = await Promise.all(
|
|
4228
4303
|
Object.values(registry.taxonomy_types).map(async (tt) => {
|
|
4229
4304
|
const result = await db.execute(
|
|
4230
|
-
|
|
4305
|
+
import_drizzle_orm9.sql`SELECT COUNT(*)::int AS count FROM ${import_drizzle_orm9.sql.raw(`"${tt.db.table_name}"`)}`
|
|
4231
4306
|
);
|
|
4232
4307
|
const count = Number(result.rows[0].count);
|
|
4233
4308
|
return { name: tt.name, label: tt.label, count };
|
|
@@ -4239,7 +4314,7 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
4239
4314
|
|
|
4240
4315
|
// src/repositories/content.ts
|
|
4241
4316
|
var import_node_crypto3 = __toESM(require("crypto"));
|
|
4242
|
-
var
|
|
4317
|
+
var import_drizzle_orm10 = require("drizzle-orm");
|
|
4243
4318
|
init_relations();
|
|
4244
4319
|
var SORTABLE_FIELDS2 = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
|
|
4245
4320
|
function codeError(code, message) {
|
|
@@ -4247,7 +4322,7 @@ function codeError(code, message) {
|
|
|
4247
4322
|
err.code = code;
|
|
4248
4323
|
return err;
|
|
4249
4324
|
}
|
|
4250
|
-
function
|
|
4325
|
+
function quoteIdent5(name) {
|
|
4251
4326
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
|
|
4252
4327
|
throw new Error(`Unsafe SQL identifier: ${name}`);
|
|
4253
4328
|
}
|
|
@@ -4262,43 +4337,43 @@ function quoteField(name) {
|
|
|
4262
4337
|
function buildConditions(published_only, filters, search) {
|
|
4263
4338
|
const conds = [];
|
|
4264
4339
|
if (published_only) {
|
|
4265
|
-
conds.push(
|
|
4340
|
+
conds.push(import_drizzle_orm10.sql`published = true`);
|
|
4266
4341
|
}
|
|
4267
4342
|
if (search && search.term.trim() !== "" && search.columns.length > 0) {
|
|
4268
4343
|
const pattern = `%${search.term.trim()}%`;
|
|
4269
|
-
const orConds = search.columns.map((col) =>
|
|
4270
|
-
conds.push(
|
|
4344
|
+
const orConds = search.columns.map((col) => import_drizzle_orm10.sql`${import_drizzle_orm10.sql.raw(quoteField(col))} ILIKE ${pattern}`);
|
|
4345
|
+
conds.push(import_drizzle_orm10.sql`(${import_drizzle_orm10.sql.join(orConds, import_drizzle_orm10.sql` OR `)})`);
|
|
4271
4346
|
}
|
|
4272
4347
|
if (filters) {
|
|
4273
4348
|
for (const [field, value] of Object.entries(filters)) {
|
|
4274
|
-
const col =
|
|
4349
|
+
const col = import_drizzle_orm10.sql.raw(quoteField(field));
|
|
4275
4350
|
if (Array.isArray(value)) {
|
|
4276
4351
|
if (value.length === 0) continue;
|
|
4277
|
-
const inList =
|
|
4278
|
-
value.map((v) =>
|
|
4279
|
-
|
|
4352
|
+
const inList = import_drizzle_orm10.sql.join(
|
|
4353
|
+
value.map((v) => import_drizzle_orm10.sql`${v}`),
|
|
4354
|
+
import_drizzle_orm10.sql`, `
|
|
4280
4355
|
);
|
|
4281
|
-
conds.push(
|
|
4356
|
+
conds.push(import_drizzle_orm10.sql`${col} IN (${inList})`);
|
|
4282
4357
|
} else if (typeof value === "object" && value !== null) {
|
|
4283
4358
|
const op = value;
|
|
4284
|
-
if (op.gt !== void 0) conds.push(
|
|
4285
|
-
if (op.gte !== void 0) conds.push(
|
|
4286
|
-
if (op.lt !== void 0) conds.push(
|
|
4287
|
-
if (op.lte !== void 0) conds.push(
|
|
4359
|
+
if (op.gt !== void 0) conds.push(import_drizzle_orm10.sql`${col} > ${op.gt}`);
|
|
4360
|
+
if (op.gte !== void 0) conds.push(import_drizzle_orm10.sql`${col} >= ${op.gte}`);
|
|
4361
|
+
if (op.lt !== void 0) conds.push(import_drizzle_orm10.sql`${col} < ${op.lt}`);
|
|
4362
|
+
if (op.lte !== void 0) conds.push(import_drizzle_orm10.sql`${col} <= ${op.lte}`);
|
|
4288
4363
|
} else {
|
|
4289
|
-
conds.push(
|
|
4364
|
+
conds.push(import_drizzle_orm10.sql`${col} = ${value}`);
|
|
4290
4365
|
}
|
|
4291
4366
|
}
|
|
4292
4367
|
}
|
|
4293
4368
|
return conds;
|
|
4294
4369
|
}
|
|
4295
4370
|
function whereFragment(conds) {
|
|
4296
|
-
return conds.length > 0 ?
|
|
4371
|
+
return conds.length > 0 ? import_drizzle_orm10.sql` WHERE ${import_drizzle_orm10.sql.join(conds, import_drizzle_orm10.sql` AND `)}` : import_drizzle_orm10.sql``;
|
|
4297
4372
|
}
|
|
4298
4373
|
function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
4299
4374
|
const { relations = {}, publishedRelations = false, sortableColumns } = options;
|
|
4300
4375
|
function tableRaw() {
|
|
4301
|
-
return
|
|
4376
|
+
return import_drizzle_orm10.sql.raw(quoteIdent5(tableName));
|
|
4302
4377
|
}
|
|
4303
4378
|
async function resolveRows(rows, include, cache) {
|
|
4304
4379
|
if (rows.length === 0) return rows;
|
|
@@ -4341,14 +4416,14 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
4341
4416
|
const offset = (page - 1) * per_page;
|
|
4342
4417
|
const conds = buildConditions(published_only, filters, search);
|
|
4343
4418
|
const where = whereFragment(conds);
|
|
4344
|
-
const sortCol =
|
|
4345
|
-
const sortDir =
|
|
4419
|
+
const sortCol = import_drizzle_orm10.sql.raw(quoteIdent5(sort_by));
|
|
4420
|
+
const sortDir = import_drizzle_orm10.sql.raw(sort_order === "desc" ? "DESC" : "ASC");
|
|
4346
4421
|
const countResult = await db.execute(
|
|
4347
|
-
|
|
4422
|
+
import_drizzle_orm10.sql`SELECT COUNT(*) AS total FROM ${tableRaw()}${where}`
|
|
4348
4423
|
);
|
|
4349
4424
|
const total = Number(countResult.rows[0]?.["total"] ?? 0);
|
|
4350
4425
|
const dataResult = await db.execute(
|
|
4351
|
-
|
|
4426
|
+
import_drizzle_orm10.sql`SELECT * FROM ${tableRaw()}${where} ORDER BY ${sortCol} ${sortDir} LIMIT ${per_page} OFFSET ${offset}`
|
|
4352
4427
|
);
|
|
4353
4428
|
const rows = dataResult.rows;
|
|
4354
4429
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -4369,7 +4444,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
4369
4444
|
},
|
|
4370
4445
|
async findOne(id, include = []) {
|
|
4371
4446
|
const result = await db.execute(
|
|
4372
|
-
|
|
4447
|
+
import_drizzle_orm10.sql`SELECT * FROM ${tableRaw()} WHERE id = ${id} LIMIT 1`
|
|
4373
4448
|
);
|
|
4374
4449
|
if (result.rows.length === 0) return null;
|
|
4375
4450
|
const rows = result.rows;
|
|
@@ -4379,7 +4454,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
4379
4454
|
},
|
|
4380
4455
|
async findBySlug(slug, include = []) {
|
|
4381
4456
|
const result = await db.execute(
|
|
4382
|
-
|
|
4457
|
+
import_drizzle_orm10.sql`SELECT * FROM ${tableRaw()} WHERE slug = ${slug} LIMIT 1`
|
|
4383
4458
|
);
|
|
4384
4459
|
if (result.rows.length === 0) return null;
|
|
4385
4460
|
const rows = result.rows;
|
|
@@ -4395,10 +4470,10 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
4395
4470
|
updated_at: /* @__PURE__ */ new Date()
|
|
4396
4471
|
};
|
|
4397
4472
|
const entries = Object.entries(record).filter(([, v]) => v !== void 0);
|
|
4398
|
-
const colsSql =
|
|
4399
|
-
const valsSql =
|
|
4473
|
+
const colsSql = import_drizzle_orm10.sql.join(entries.map(([k]) => import_drizzle_orm10.sql.raw(quoteIdent5(k))), import_drizzle_orm10.sql`, `);
|
|
4474
|
+
const valsSql = import_drizzle_orm10.sql.join(entries.map(([, v]) => import_drizzle_orm10.sql`${v}`), import_drizzle_orm10.sql`, `);
|
|
4400
4475
|
const result = await db.execute(
|
|
4401
|
-
|
|
4476
|
+
import_drizzle_orm10.sql`INSERT INTO ${tableRaw()} (${colsSql}) VALUES (${valsSql}) RETURNING *`
|
|
4402
4477
|
);
|
|
4403
4478
|
return result.rows[0];
|
|
4404
4479
|
},
|
|
@@ -4411,25 +4486,25 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
4411
4486
|
if (entries.length === 0) {
|
|
4412
4487
|
return this.findOne(id);
|
|
4413
4488
|
}
|
|
4414
|
-
const setClauses =
|
|
4415
|
-
entries.map(([k, v]) =>
|
|
4416
|
-
|
|
4489
|
+
const setClauses = import_drizzle_orm10.sql.join(
|
|
4490
|
+
entries.map(([k, v]) => import_drizzle_orm10.sql`${import_drizzle_orm10.sql.raw(quoteIdent5(k))} = ${v}`),
|
|
4491
|
+
import_drizzle_orm10.sql`, `
|
|
4417
4492
|
);
|
|
4418
4493
|
const result = await db.execute(
|
|
4419
|
-
|
|
4494
|
+
import_drizzle_orm10.sql`UPDATE ${tableRaw()} SET ${setClauses} WHERE id = ${id} RETURNING *`
|
|
4420
4495
|
);
|
|
4421
4496
|
if (result.rows.length === 0) return null;
|
|
4422
4497
|
return result.rows[0];
|
|
4423
4498
|
},
|
|
4424
4499
|
async delete(id) {
|
|
4425
|
-
await db.execute(
|
|
4500
|
+
await db.execute(import_drizzle_orm10.sql`DELETE FROM ${tableRaw()} WHERE id = ${id}`);
|
|
4426
4501
|
},
|
|
4427
4502
|
async findAll(opts = {}) {
|
|
4428
4503
|
const { include = [], published_only } = opts;
|
|
4429
4504
|
const conds = buildConditions(published_only, void 0, void 0);
|
|
4430
4505
|
const where = whereFragment(conds);
|
|
4431
4506
|
const result = await db.execute(
|
|
4432
|
-
|
|
4507
|
+
import_drizzle_orm10.sql`SELECT * FROM ${tableRaw()}${where} ORDER BY created_at ASC`
|
|
4433
4508
|
);
|
|
4434
4509
|
const rows = result.rows;
|
|
4435
4510
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -4444,23 +4519,23 @@ init_relations();
|
|
|
4444
4519
|
|
|
4445
4520
|
// src/repositories/media.ts
|
|
4446
4521
|
var import_node_crypto4 = __toESM(require("crypto"));
|
|
4447
|
-
var
|
|
4522
|
+
var import_drizzle_orm11 = require("drizzle-orm");
|
|
4448
4523
|
function createMediaRepository(db) {
|
|
4449
|
-
const TABLE =
|
|
4524
|
+
const TABLE = import_drizzle_orm11.sql.raw('"media"');
|
|
4450
4525
|
return {
|
|
4451
4526
|
async findMany(opts = {}) {
|
|
4452
4527
|
const { page = 1, per_page = 10, type, orphaned } = opts;
|
|
4453
4528
|
const offset = (page - 1) * per_page;
|
|
4454
4529
|
const conds = [];
|
|
4455
|
-
if (type) conds.push(
|
|
4456
|
-
if (orphaned) conds.push(
|
|
4457
|
-
const where = conds.length > 0 ?
|
|
4458
|
-
const countResult = await db.execute(
|
|
4530
|
+
if (type) conds.push(import_drizzle_orm11.sql`type = ${type}`);
|
|
4531
|
+
if (orphaned) conds.push(import_drizzle_orm11.sql`reference_count = 0`);
|
|
4532
|
+
const where = conds.length > 0 ? import_drizzle_orm11.sql` WHERE ${import_drizzle_orm11.sql.join(conds, import_drizzle_orm11.sql` AND `)}` : import_drizzle_orm11.sql``;
|
|
4533
|
+
const countResult = await db.execute(import_drizzle_orm11.sql`SELECT COUNT(*) AS total FROM ${TABLE}${where}`);
|
|
4459
4534
|
const total = Number(
|
|
4460
4535
|
countResult.rows[0]?.["total"] ?? 0
|
|
4461
4536
|
);
|
|
4462
4537
|
const dataResult = await db.execute(
|
|
4463
|
-
|
|
4538
|
+
import_drizzle_orm11.sql`SELECT * FROM ${TABLE}${where} ORDER BY created_at DESC LIMIT ${per_page} OFFSET ${offset}`
|
|
4464
4539
|
);
|
|
4465
4540
|
const total_pages = Math.ceil(total / per_page);
|
|
4466
4541
|
return {
|
|
@@ -4478,7 +4553,7 @@ function createMediaRepository(db) {
|
|
|
4478
4553
|
},
|
|
4479
4554
|
async findOne(id) {
|
|
4480
4555
|
const result = await db.execute(
|
|
4481
|
-
|
|
4556
|
+
import_drizzle_orm11.sql`SELECT * FROM ${TABLE} WHERE id = ${id} LIMIT 1`
|
|
4482
4557
|
);
|
|
4483
4558
|
if (result.rows.length === 0) return null;
|
|
4484
4559
|
return result.rows[0];
|
|
@@ -4492,10 +4567,10 @@ function createMediaRepository(db) {
|
|
|
4492
4567
|
updated_at: /* @__PURE__ */ new Date()
|
|
4493
4568
|
};
|
|
4494
4569
|
const entries = Object.entries(record).filter(([, v]) => v !== void 0);
|
|
4495
|
-
const colsSql =
|
|
4496
|
-
const valsSql =
|
|
4570
|
+
const colsSql = import_drizzle_orm11.sql.join(entries.map(([k]) => import_drizzle_orm11.sql.raw(`"${k}"`)), import_drizzle_orm11.sql`, `);
|
|
4571
|
+
const valsSql = import_drizzle_orm11.sql.join(entries.map(([, v]) => import_drizzle_orm11.sql`${v}`), import_drizzle_orm11.sql`, `);
|
|
4497
4572
|
const result = await db.execute(
|
|
4498
|
-
|
|
4573
|
+
import_drizzle_orm11.sql`INSERT INTO ${TABLE} (${colsSql}) VALUES (${valsSql}) RETURNING *`
|
|
4499
4574
|
);
|
|
4500
4575
|
return result.rows[0];
|
|
4501
4576
|
},
|
|
@@ -4505,31 +4580,31 @@ function createMediaRepository(db) {
|
|
|
4505
4580
|
([k, v]) => k !== "id" && v !== void 0
|
|
4506
4581
|
);
|
|
4507
4582
|
if (entries.length === 0) return this.findOne(id);
|
|
4508
|
-
const setClauses =
|
|
4509
|
-
entries.map(([k, v]) =>
|
|
4510
|
-
|
|
4583
|
+
const setClauses = import_drizzle_orm11.sql.join(
|
|
4584
|
+
entries.map(([k, v]) => import_drizzle_orm11.sql`${import_drizzle_orm11.sql.raw(`"${k}"`)} = ${v}`),
|
|
4585
|
+
import_drizzle_orm11.sql`, `
|
|
4511
4586
|
);
|
|
4512
4587
|
const result = await db.execute(
|
|
4513
|
-
|
|
4588
|
+
import_drizzle_orm11.sql`UPDATE ${TABLE} SET ${setClauses} WHERE id = ${id} RETURNING *`
|
|
4514
4589
|
);
|
|
4515
4590
|
if (result.rows.length === 0) return null;
|
|
4516
4591
|
return result.rows[0];
|
|
4517
4592
|
},
|
|
4518
4593
|
async delete(id) {
|
|
4519
|
-
await db.execute(
|
|
4594
|
+
await db.execute(import_drizzle_orm11.sql`DELETE FROM ${TABLE} WHERE id = ${id}`);
|
|
4520
4595
|
},
|
|
4521
4596
|
async incrementReferenceCount(ids) {
|
|
4522
4597
|
if (ids.length === 0) return;
|
|
4523
|
-
const inList =
|
|
4598
|
+
const inList = import_drizzle_orm11.sql.join(ids.map((id) => import_drizzle_orm11.sql`${id}`), import_drizzle_orm11.sql`, `);
|
|
4524
4599
|
await db.execute(
|
|
4525
|
-
|
|
4600
|
+
import_drizzle_orm11.sql`UPDATE ${TABLE} SET reference_count = reference_count + 1, updated_at = NOW() WHERE id IN (${inList})`
|
|
4526
4601
|
);
|
|
4527
4602
|
},
|
|
4528
4603
|
async decrementReferenceCount(ids) {
|
|
4529
4604
|
if (ids.length === 0) return;
|
|
4530
|
-
const inList =
|
|
4605
|
+
const inList = import_drizzle_orm11.sql.join(ids.map((id) => import_drizzle_orm11.sql`${id}`), import_drizzle_orm11.sql`, `);
|
|
4531
4606
|
await db.execute(
|
|
4532
|
-
|
|
4607
|
+
import_drizzle_orm11.sql`UPDATE ${TABLE} SET reference_count = GREATEST(reference_count - 1, 0), updated_at = NOW() WHERE id IN (${inList})`
|
|
4533
4608
|
);
|
|
4534
4609
|
}
|
|
4535
4610
|
};
|
|
@@ -4666,7 +4741,7 @@ function createCmsApp(options) {
|
|
|
4666
4741
|
const requirePermission = createPermissionMiddleware(rolesRegistry);
|
|
4667
4742
|
const getUserRole = async (userId) => {
|
|
4668
4743
|
const result = await db.execute(
|
|
4669
|
-
|
|
4744
|
+
import_drizzle_orm12.sql`SELECT r.name AS role
|
|
4670
4745
|
FROM users u
|
|
4671
4746
|
JOIN roles r ON r.id = u.role_id
|
|
4672
4747
|
WHERE u.id = ${userId}
|