From f5254f75711dc45cc9375a9f96ee8756b733b0df Mon Sep 17 00:00:00 2001 From: Kenneth Obsequio Date: Mon, 13 Jul 2026 12:46:38 +0800 Subject: [PATCH] test again Signed-off-by: Kenneth Obsequio --- controllers/admin/assets.controller.js | 32 +- controllers/admin/dashboard.controller.js | 4 - controllers/admin/user_activity.controller.js | 10 +- controllers/admin/users.controller.js | 62 +- models/users/users.attributes.js | 1 + package-lock.json | 671 +++++++----------- package.json | 1 + routes/admin/assets.routes.js | 1 + services/s3.service.js | 29 +- services/uploadProgress.service.js | 52 ++ utils/buildQuery.util.js | 23 +- utils/paginate.util.js | 16 +- 12 files changed, 441 insertions(+), 461 deletions(-) create mode 100644 services/uploadProgress.service.js diff --git a/controllers/admin/assets.controller.js b/controllers/admin/assets.controller.js index 6396085..7b4151a 100644 --- a/controllers/admin/assets.controller.js +++ b/controllers/admin/assets.controller.js @@ -7,6 +7,7 @@ const Asset = require("../../models/assets/assets.mdl"); const chibi = require("../../services/chibisafe.service"); const s3 = require("../../services/s3.service"); const mediaToken = require("../../services/mediaToken.service"); +const uploadProgress = require("../../services/uploadProgress.service"); const { extractVideoMeta } = require("../../services/ffprobe.service"); const R = require('../../utils/response.util'); const { paginate } = require("../../utils/paginate.util"); @@ -328,7 +329,7 @@ exports.getAsset = async (req, res) => { // per-file thumbnail step, so videos land with thumbnail_url null and pick // one up later via the existing "thumbnail-only" path in updateAsset(). // -async function createAssetFromUpload({ file, thumbFile, body, user, requireVideoThumbnail = true }) { +async function createAssetFromUpload({ file, thumbFile, body, user, requireVideoThumbnail = true, uploadId }) { const uploadedFiles = []; // [{ key, provider }] try { @@ -367,11 +368,24 @@ async function createAssetFromUpload({ file, thumbFile, body, user, requireVideo if (usesProvider) { const svc = getProvider(storage_provider); + // Real Express -> Garage progress, main file only (not the thumbnail — + // it's small enough that tracking it wouldn't add anything useful). + // Relayed live to the browser over SSE; see uploadProgress.service.js. + const onProgress = (storage_provider === "s3" && uploadId) + ? ({ loaded, total }) => uploadProgress.publish(uploadId, { + phase: "storing", + loaded, + total, + pct: total ? Math.round((loaded / total) * 100) : 0, + }) + : undefined; + const result = await svc.uploadFile({ buffer: file.buffer, originalname: file.originalname, mimetype: mime_type, ownerType: file_type, + onProgress, }); file_url = result.url; storage_key_resolved = result.uuid; @@ -497,21 +511,35 @@ async function createAssetFromUpload({ file, thumbFile, body, user, requireVideo } exports.uploadAsset = async (req, res) => { + const { uploadId } = req.body; try { const file = req.files?.file?.[0]; const thumbFile = req.files?.thumbnail?.[0]; - const asset = await createAssetFromUpload({ file, thumbFile, body: req.body, user: req.user }); + const asset = await createAssetFromUpload({ file, thumbFile, body: req.body, user: req.user, uploadId }); invalidateListCache(); + if (uploadId) uploadProgress.complete(uploadId, { phase: "done", pct: 100 }); return R.success(res, "Asset uploaded.", { data: asset }, 201); } catch (err) { console.error("[ASSET][UPLOAD]", err); + if (uploadId) uploadProgress.complete(uploadId, { phase: "error" }); if (err.status) return R.error(res, err.message, err.status, { detail: err.chibiBody }); return R.error(res, "Internal server error.", 500); } }; +// ─── UPLOAD PROGRESS (SSE) ────────────────────────────────────────────────── +// +// Client opens this before POSTing the file, correlated by a client-generated +// uploadId sent as a form field on the upload request itself. Streams the +// real Express -> Garage httpUploadProgress events from s3.service.js's +// Upload — not a simulated or estimated number. +// +exports.streamUploadProgress = (req, res) => { + uploadProgress.subscribe(req.params.uploadId, res); +}; + // ─── UPLOAD (batch) ───────────────────────────────────────────────────────── // // Accepts multiple files under the "files" field in one multipart request, diff --git a/controllers/admin/dashboard.controller.js b/controllers/admin/dashboard.controller.js index 3824b03..9355a44 100644 --- a/controllers/admin/dashboard.controller.js +++ b/controllers/admin/dashboard.controller.js @@ -17,14 +17,12 @@ exports.getUsersDashboard = async (req, res) => { const [ totalUsers, activeUsers, - verifiedUsers, archivedUsers, accTypeBreakdown, regTypeBreakdown, ] = await Promise.all([ mdl_Users.count({ paranoid: false }), mdl_Users.count({ where: { is_active: true } }), - mdl_Users.count({ where: { is_verified: true } }), mdl_Users.count({ where: { deletedAt: { [Op.ne]: null } }, paranoid: false }), mdl_Users.findAll({ attributes: ['acc_type', [fn('COUNT', col('user_id')), 'count']], @@ -46,8 +44,6 @@ exports.getUsersDashboard = async (req, res) => { { key: 'total', label: 'Total Users', value: totalUsers }, { key: 'active', label: 'Active Users', value: activeUsers }, { key: 'inactive', label: 'Inactive Users', value: totalUsers - archivedUsers - activeUsers }, - { key: 'verified', label: 'Verified', value: verifiedUsers }, - { key: 'archived', label: 'Archived', value: archivedUsers }, ], breakdowns: [ { diff --git a/controllers/admin/user_activity.controller.js b/controllers/admin/user_activity.controller.js index 7f7a94d..9a79e64 100644 --- a/controllers/admin/user_activity.controller.js +++ b/controllers/admin/user_activity.controller.js @@ -30,7 +30,15 @@ function buildWhere(query, extraWhere = {}) { if (query.from || query.to) { where.created_at = {}; if (query.from) where.created_at[Op.gte] = new Date(query.from); - if (query.to) where.created_at[Op.lte] = new Date(query.to); + if (query.to) { + // `to` arrives as a date-only string (e.g. "2026-06-04"), which parses + // to that day's UTC midnight — an Op.lte against midnight excludes + // every event that happened later the same day. Push it to the last + // instant of that calendar day instead. + const to = new Date(query.to); + to.setUTCHours(23, 59, 59, 999); + where.created_at[Op.lte] = to; + } } return where; diff --git a/controllers/admin/users.controller.js b/controllers/admin/users.controller.js index 26cf720..e5fcb18 100644 --- a/controllers/admin/users.controller.js +++ b/controllers/admin/users.controller.js @@ -34,10 +34,48 @@ const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas, computedAtt const EXCLUDED = ['password', 'otp_code', 'otp_expires_at', 'must_change_password', 'password_expires_at']; const auditByFields = ['createdBy', 'updatedBy', 'deletedBy']; +// ─── GROUPS FILTER ──────────────────────────────────────────────────────────── +// +// "groups" is a M2M association, not a real column on `users` — buildQuery's +// generic Sequelize.col()-based filtering can't touch it. Pull any `groups` +// filter out of the request's filter list (so paginate() never sees a field +// it can't resolve) and turn it into an EXISTS subquery instead. EXISTS keeps +// the LEFT JOIN group data on each row intact (all of a user's groups still +// show up), while only matching users who belong to at least one of the +// selected groups. +function extractGroupsFilter(req) { + let filters = []; + try { filters = JSON.parse(req.query.filters || '[]'); } catch { filters = []; } + + const groupsFilter = filters.find((f) => f.id === 'groups'); + const remaining = filters.filter((f) => f.id !== 'groups'); + req.query.filters = JSON.stringify(remaining); + + if (!groupsFilter?.value) return null; + + const groupIds = (Array.isArray(groupsFilter.value) ? groupsFilter.value : [groupsFilter.value]) + .map((v) => parseInt(v, 10)) + .filter((v) => !Number.isNaN(v)); + + return groupIds.length ? groupIds : null; +} + +function groupsExistsWhere(groupIds) { + if (!groupIds) return undefined; + return Sequelize.literal(`EXISTS ( + SELECT 1 FROM "user_group_members" ugm + WHERE ugm.user_id = "User"."user_id" + AND ugm."deletedAt" IS NULL + AND ugm.group_id IN (${groupIds.join(',')}) + )`); +} + // ─── GET ALL ────────────────────────────────────────────────────────────────── exports.getUsers = async (req, res) => { try { + const groupIds = extractGroupsFilter(req); + const result = await paginate(mdl_Users, req, { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas, @@ -46,6 +84,7 @@ exports.getUsers = async (req, res) => { context: "list", auditOptions: { mdl_Users, parentAlias: 'User' }, findOptions: { + where: groupsExistsWhere(groupIds), include: [{ model: mdl_UserGroups, as: 'groups', @@ -470,18 +509,37 @@ exports.terminateSession = async (req, res) => { // ─── FIELD VALUES ───────────────────────────────────────────────────────────── -exports.getUserFieldValues = getFieldValues(mdl_Users, "USER", { +const getUserFieldValuesBase = getFieldValues(mdl_Users, "USER", { blockedFields: ["password", "otp_code", "otp_expires_at", "personal_info"], extraDateFields: ["modifiedAt", "ban_expires_at"], allowJsonb: true, selfJoin: true, }); +// "groups" isn't a Users model attribute, so the generic getFieldValues() +// helper can't resolve it — special-case it here, delegate everything else. +exports.getUserFieldValues = async (req, res) => { + if (req.query.field === 'groups') { + const groups = await mdl_UserGroups.findAll({ + attributes: [['group_id', 'value'], ['name', 'label']], + order: [['name', 'ASC']], + raw: true, + }); + return R.success(res, 'Field values retrieved.', groups); + } + + return getUserFieldValuesBase(req, res); +}; + // ─── ARCHIVED ───────────────────────────────────────────────────────────────── // ─── GET ARCHIVED USERS ─────────────────────────────────────────────────────── exports.getArchivedUsers = async (req, res) => { try { + const groupIds = extractGroupsFilter(req); + const archivedWhere = { deletedAt: { [Op.ne]: null }, is_active: false }; + const groupsWhere = groupsExistsWhere(groupIds); + const result = await paginate(mdl_Users, req, { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas, @@ -491,7 +549,7 @@ exports.getArchivedUsers = async (req, res) => { auditOptions: { mdl_Users, parentAlias: 'User' }, findOptions: { paranoid: false, - where: { deletedAt: { [Op.ne]: null }, is_active: false }, + where: groupsWhere ? { [Op.and]: [archivedWhere, groupsWhere] } : archivedWhere, include: [{ model: mdl_UserGroups, as: 'groups', diff --git a/models/users/users.attributes.js b/models/users/users.attributes.js index 4c46111..80148e1 100644 --- a/models/users/users.attributes.js +++ b/models/users/users.attributes.js @@ -52,6 +52,7 @@ const computedAttributes = [ label: "Groups", type: "array", // tells the paginator this is a pre-joined association array order: 5, + filterable: true, // handled specially in the controller — see extractGroupsFilter() }, ]; diff --git a/package-lock.json b/package-lock.json index 85ee9a3..b6bbdb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@aws-sdk/client-s3": "^3.1068.0", + "@aws-sdk/lib-storage": "^3.1085.0", "@aws-sdk/s3-request-presigner": "^3.1068.0", "axios": "^1.16.0", "bcryptjs": "^2.4.3", @@ -46,107 +47,16 @@ "sequelize-cli": "^6.6.5" } }, - "node_modules/@aws-crypto/crc32": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", - "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-crypto/crc32c": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", - "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha1-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", - "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha256-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", - "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-crypto/supports-web-crypto": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, "node_modules/@aws-sdk/checksums": { - "version": "3.1000.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1000.5.tgz", - "integrity": "sha512-zOXUUnilC6lgCsQtp77p/QNPmRlTES9Xi6tlDwbR6kfC/kz5PCzZckgHWm5z+8DskdwuMAbFDq61x3zr10GEEQ==", + "version": "3.1000.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1000.16.tgz", + "integrity": "sha512-EKnvkXSmz3IpA99tCNuI+dLFXyZyClSm8zns9sB/elvkU+MTuomAs6toJMPMBf98/fICG/urXDkzGz0/c3yyAQ==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@aws-crypto/crc32c": "5.2.0", - "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "^3.974.20", - "@aws-sdk/types": "^3.973.12", - "@smithy/core": "^3.24.6", - "@smithy/types": "^4.14.3", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -154,24 +64,21 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.1068.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1068.0.tgz", - "integrity": "sha512-lFgaIpxZvloNbJvQ337YPdMXhzI2zJdDw13nATVGnkAGNoNPx4ksD84AQAcuW75hsaaMaIuNmXU9sSx6+FTirA==", + "version": "3.1085.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1085.0.tgz", + "integrity": "sha512-O0xe8sR50AYkwxlvRRsV0qytEO2dtXQTQ1CF3YBBdE5xtVkbu27H0vGa1mjQi1/+fbYM80AWEIPai5jZmXyubw==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha1-browser": "5.2.0", - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.20", - "@aws-sdk/credential-provider-node": "^3.972.55", - "@aws-sdk/middleware-flexible-checksums": "^3.974.30", - "@aws-sdk/middleware-sdk-s3": "^3.972.51", - "@aws-sdk/signature-v4-multi-region": "^3.996.34", - "@aws-sdk/types": "^3.973.12", - "@smithy/core": "^3.24.6", - "@smithy/fetch-http-handler": "^5.4.6", - "@smithy/node-http-handler": "^4.7.6", - "@smithy/types": "^4.14.3", + "@aws-sdk/checksums": "^3.1000.16", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/credential-provider-node": "^3.972.66", + "@aws-sdk/middleware-sdk-s3": "^3.972.62", + "@aws-sdk/signature-v4-multi-region": "^3.996.39", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/fetch-http-handler": "^5.6.4", + "@smithy/node-http-handler": "^4.9.4", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -179,17 +86,17 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.974.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.20.tgz", - "integrity": "sha512-7sDi2B2N3mc3nf1nz6FyEx/FCrJ1N1QnBmraHHQNabFaeAh2IaOOLml48/rHOD1bICHgTRkbBgNTvUzEr5Z35g==", + "version": "3.975.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.975.1.tgz", + "integrity": "sha512-8qh/6EYb7hl/ZwVfQufhbMEZs1gQIc7GbdrIf4eprQJ7cv042+74nE6l3YDfyWNzb9iPXb8fRyYSHkNIk5eE6Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.12", - "@aws-sdk/xml-builder": "^3.972.29", - "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/core": "^3.24.6", - "@smithy/signature-v4": "^5.4.6", - "@smithy/types": "^4.14.3", + "@aws-sdk/types": "^3.974.0", + "@aws-sdk/xml-builder": "^3.972.34", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.29.2", + "@smithy/signature-v4": "^5.6.3", + "@smithy/types": "^4.16.0", "bowser": "^2.11.0", "tslib": "^2.6.2" }, @@ -198,15 +105,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.46", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.46.tgz", - "integrity": "sha512-+GPXVS2srMOlH74S+SmC1gVuP2TvUZ0siuC0onKO93q+udP+M72dmY8wJfVQ5CX9z/9X5A1HHwz5yRIGBtskvQ==", + "version": "3.972.57", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.57.tgz", + "integrity": "sha512-1RfJaF7SW1TOnvNGU7kaYjwUf5H3sfm+synGH1bHhRlqcnxCt3szebH3dmKEyY4tuGcbQ6ffzUT89cRitBV8OQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.20", - "@aws-sdk/types": "^3.973.12", - "@smithy/core": "^3.24.6", - "@smithy/types": "^4.14.3", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -214,17 +121,17 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.48", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.48.tgz", - "integrity": "sha512-fA5loSdlocacRxyUXtpoHSMuk5rsIKRDzQYVMnMxjcmFeZshaJlJ8lymy/hYKji6sne/UmNGj5pxuEs6kq/Qcg==", + "version": "3.972.59", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.59.tgz", + "integrity": "sha512-sRCkpTiFnCdQvuaRVjQ6SVoHu6i7RUpurVo1c4F81HWhPvUJ7Wdp5MNtSdX1O29CNXc8em3O5m52hCjVtAD9SA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.20", - "@aws-sdk/types": "^3.973.12", - "@smithy/core": "^3.24.6", - "@smithy/fetch-http-handler": "^5.4.6", - "@smithy/node-http-handler": "^4.7.6", - "@smithy/types": "^4.14.3", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/fetch-http-handler": "^5.6.4", + "@smithy/node-http-handler": "^4.9.4", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -232,23 +139,23 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.53", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.53.tgz", - "integrity": "sha512-ZfdhIOR41q8TcWEnUac+gCOb+O2LBWdHLmjedXpXz4IEFW2ppNuFcm6p0sMTavpM+zD5TYfpH5Gp7guRyqSgsQ==", + "version": "3.973.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.1.tgz", + "integrity": "sha512-6d8H6ZAh3ZPKZ6fe1nG2OWeZEZPtt9ravoD1dezPdPtsSkJRoxGAnFSHwKT3E/Te6fHE30zRzjV6TD12rvF6yQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.20", - "@aws-sdk/credential-provider-env": "^3.972.46", - "@aws-sdk/credential-provider-http": "^3.972.48", - "@aws-sdk/credential-provider-login": "^3.972.52", - "@aws-sdk/credential-provider-process": "^3.972.46", - "@aws-sdk/credential-provider-sso": "^3.972.52", - "@aws-sdk/credential-provider-web-identity": "^3.972.52", - "@aws-sdk/nested-clients": "^3.997.20", - "@aws-sdk/types": "^3.973.12", - "@smithy/core": "^3.24.6", - "@smithy/credential-provider-imds": "^4.3.7", - "@smithy/types": "^4.14.3", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/credential-provider-env": "^3.972.57", + "@aws-sdk/credential-provider-http": "^3.972.59", + "@aws-sdk/credential-provider-login": "^3.972.63", + "@aws-sdk/credential-provider-process": "^3.972.57", + "@aws-sdk/credential-provider-sso": "^3.973.1", + "@aws-sdk/credential-provider-web-identity": "^3.972.63", + "@aws-sdk/nested-clients": "^3.997.31", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/credential-provider-imds": "^4.4.7", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -256,16 +163,16 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.52", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.52.tgz", - "integrity": "sha512-9hu2oR0qH7Fst5Tzdx+UWxm+w5zCXtErTLtOOW5hwwQc170CLwOeniRxyFY6s9mHfGEfC5zFukNBdKBwJR8mhQ==", + "version": "3.972.63", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.63.tgz", + "integrity": "sha512-GREWRrMj0XnNKMaVa/Mauoaui26qBEHu71WWqXbwZOu/jFQOnPZjTf7u0KtGKC8VGa6VUs9kDWGgocrKNLS9vw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.20", - "@aws-sdk/nested-clients": "^3.997.20", - "@aws-sdk/types": "^3.973.12", - "@smithy/core": "^3.24.6", - "@smithy/types": "^4.14.3", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/nested-clients": "^3.997.31", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -273,21 +180,21 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.55", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.55.tgz", - "integrity": "sha512-zMGLa/dhESVqmCD7mmIFFKSwSFrJGScvCXcjvBZEVOOMauFS5JRQvLTMukFpMEFWiV6dTAlsen2ATDBulLPtbg==", + "version": "3.972.66", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.66.tgz", + "integrity": "sha512-f+qjRXZpz7sgzbc4QB+6nLKfyKFgRRXzWdXbsKPv/VhVRyHsDyq4yBWC/B75BAJpFIcUeI2XR/3gdWJ677zB4A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.46", - "@aws-sdk/credential-provider-http": "^3.972.48", - "@aws-sdk/credential-provider-ini": "^3.972.53", - "@aws-sdk/credential-provider-process": "^3.972.46", - "@aws-sdk/credential-provider-sso": "^3.972.52", - "@aws-sdk/credential-provider-web-identity": "^3.972.52", - "@aws-sdk/types": "^3.973.12", - "@smithy/core": "^3.24.6", - "@smithy/credential-provider-imds": "^4.3.7", - "@smithy/types": "^4.14.3", + "@aws-sdk/credential-provider-env": "^3.972.57", + "@aws-sdk/credential-provider-http": "^3.972.59", + "@aws-sdk/credential-provider-ini": "^3.973.1", + "@aws-sdk/credential-provider-process": "^3.972.57", + "@aws-sdk/credential-provider-sso": "^3.973.1", + "@aws-sdk/credential-provider-web-identity": "^3.972.63", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/credential-provider-imds": "^4.4.7", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -295,15 +202,15 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.46", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.46.tgz", - "integrity": "sha512-VUoNFBIjWrUN8NbFiQiuxQEgFjvziAlBRPK+ddh27aj65gk0BYu6bLZnrdrNZwpW6vAihtSUtEMQ1PUJ32QRPA==", + "version": "3.972.57", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.57.tgz", + "integrity": "sha512-TiVQhuU0pbhIZAUZacbPHMyzrIdiH+lnx+PMY/Pu/b93dJrq3wdZwzUJ0TPpvNxaqbHsxJvQZW3/h/beLiKq7Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.20", - "@aws-sdk/types": "^3.973.12", - "@smithy/core": "^3.24.6", - "@smithy/types": "^4.14.3", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -311,17 +218,17 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.52", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.52.tgz", - "integrity": "sha512-nb2/n4o/HQf+FVpVbZe9vCTFngmuDoIsltMgLAtjixaKzvzhB4J8WSDFyWgnErgLHk55ctWH+I4PU+LIHhyffg==", + "version": "3.973.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.1.tgz", + "integrity": "sha512-3foTZUJ4821Ij60X7K3NJroygiZLnbBmarN+T//O2cjkISan90zElN3NBmgSlDrTQ7Gs6z/yO8V7h60QNcDZHQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.20", - "@aws-sdk/nested-clients": "^3.997.20", - "@aws-sdk/token-providers": "3.1066.0", - "@aws-sdk/types": "^3.973.12", - "@smithy/core": "^3.24.6", - "@smithy/types": "^4.14.3", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/nested-clients": "^3.997.31", + "@aws-sdk/token-providers": "3.1083.0", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -329,46 +236,53 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.52", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.52.tgz", - "integrity": "sha512-lKj6aRSGbqLmpYmM24bY7a1Xmfcq2vkE3hv8CSPYfc1yCu0BPu/XEJ1L4Fm61MsU6ULLNSG8UGsffNoFUBjESA==", + "version": "3.972.63", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.63.tgz", + "integrity": "sha512-8qZLFhM69eKcS37m459ctPR05Qimycm/74OPVioe6wNZabMT54GYhwBju0+J656RkMasNSawWQu+c8CmBe3TUQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.20", - "@aws-sdk/nested-clients": "^3.997.20", - "@aws-sdk/types": "^3.973.12", - "@smithy/core": "^3.24.6", - "@smithy/types": "^4.14.3", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/nested-clients": "^3.997.31", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.974.30", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.30.tgz", - "integrity": "sha512-OaIhub+3yTgfFWPzKO8OzOZFIMUoJaiS5v67y3spQg7SoULGoMx4jKVBbE+uhnzkiZXQ+rEDS0RqrK4/aD1yJw==", + "node_modules/@aws-sdk/lib-storage": { + "version": "3.1085.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1085.0.tgz", + "integrity": "sha512-S+yCGIMxQ5Zs2A5ZDdYfXwOxu5kKjBaCQVOxp6+mnwCQYXUNkBD/aKCnW1eniMTavzz3YidhD5eTFpDlcUv2gQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/checksums": "^3.1000.5", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "buffer": "5.6.0", + "events": "3.3.0", + "stream-browserify": "3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.1085.0" } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.51", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.51.tgz", - "integrity": "sha512-keQgcIUTcHL0Qn7guhsuLaxQU36r9norCrxgaPH4DNCwon4TPtXdI/UdYuycl9vj3Dlwc3YR1dfL3U+6iIwJ6w==", + "version": "3.972.62", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.62.tgz", + "integrity": "sha512-k8JJwYXVYlOOjWnPZDThQS1xDFJgi5Dokt73qFlDtrZAbdcint5aIdjB9XgJAAQVP5OoqcefQmh1FYXiPpvsvw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.20", - "@aws-sdk/signature-v4-multi-region": "^3.996.34", - "@aws-sdk/types": "^3.973.12", - "@smithy/core": "^3.24.6", - "@smithy/types": "^4.14.3", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/signature-v4-multi-region": "^3.996.39", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -376,20 +290,18 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.20.tgz", - "integrity": "sha512-IYJuLpXp2DEILVQpQOy0PMpkftv0AHEOCn52o0atyOaumA0CdWQ3klPyXdViGYLbNpESsVFMVybvHUeZAuiGxA==", + "version": "3.997.31", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.31.tgz", + "integrity": "sha512-BDHTpwcsZHEBNEJzOg/B1BkFYJxAXY50dau/NyVWs3d51F0WgIUGSWZot/Os+N3KpDhXeaXnz37mWffAvduREw==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.20", - "@aws-sdk/signature-v4-multi-region": "^3.996.34", - "@aws-sdk/types": "^3.973.12", - "@smithy/core": "^3.24.6", - "@smithy/fetch-http-handler": "^5.4.6", - "@smithy/node-http-handler": "^4.7.6", - "@smithy/types": "^4.14.3", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/signature-v4-multi-region": "^3.996.39", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/fetch-http-handler": "^5.6.4", + "@smithy/node-http-handler": "^4.9.4", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -414,14 +326,14 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.34", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.34.tgz", - "integrity": "sha512-mx1L5qlumSOt/nKM3BFaHE2HVkWwz0i4Bw0pyYO42FfX/FeLlo8YI6csC0gSPprEk6fTIqI+CZN9RwUwKd5krQ==", + "version": "3.996.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.39.tgz", + "integrity": "sha512-8+srXqYIF8KYMLC4FxMLEM5Ek7kUNibJu1R4m8/fUhhNYIZZz26oGtKkCr8I/HiG2fFQxBvaGgQZT4/mqRCSnA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.12", - "@smithy/signature-v4": "^5.4.6", - "@smithy/types": "^4.14.3", + "@aws-sdk/types": "^3.974.0", + "@smithy/signature-v4": "^5.6.3", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -429,16 +341,16 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1066.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1066.0.tgz", - "integrity": "sha512-UqEUJq7dqa44hneLDUcX7UJy95cg8YqEWyakRpvIPnrNS3Mq+UlQHgCDGu5pvwAPtlIW4qcYbvW6reG6++FyvA==", + "version": "3.1083.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1083.0.tgz", + "integrity": "sha512-s0woKnxuHrExLc5L2ArIH5BMkbonHPtt+5hSBM8oknp9M6QTuUmmAmJ2E0EdzCGONrO+8+ADPqvv6UX0nNcc7A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.20", - "@aws-sdk/nested-clients": "^3.997.20", - "@aws-sdk/types": "^3.973.12", - "@smithy/core": "^3.24.6", - "@smithy/types": "^4.14.3", + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/nested-clients": "^3.997.31", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -446,24 +358,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.12", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.12.tgz", - "integrity": "sha512-43ajd1NF0RMgX5k0hxCNUyEdrtFUsb2aHT2QvpktSC/2Eyb2Jr/JPVqdp0XIoaHWikZJq5tNWSLO6kB5q2eMCA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.3", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/util-locate-window": { - "version": "3.965.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.7.tgz", - "integrity": "sha512-M0D6oIpohdNHjc7udzTHEQyot0+0iuA36jc2I9Hps+f/GtKi2HO/pyijQnCnNcwZqLB5+rtn81z3eZK/GyjAmA==", + "version": "3.974.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.0.tgz", + "integrity": "sha512-QIBrw90CDm4O0UaIIzkU6DrFdeJzEb2Va5EPEVpyldj6sHJxB6cshhStJuhZxk3wR3PmjJlYsjPmY1kNb+KGBg==", "license": "Apache-2.0", "dependencies": { + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -471,13 +371,12 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.29.tgz", - "integrity": "sha512-fk0niuGFxfi8yIJuMVM4mhwObkiQSuwZFj3tAPrLVx64Pk3BkrEIpqjzHKY4hKoEBUD6Jg/S74Zj9jy+5F3DnQ==", + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.34.tgz", + "integrity": "sha512-wHhWL1y7sN3enBA8POrPpQM5jCcmu2ozyhbRei4c8OjVcEaEs6yLucLa/pla457ggS/ysuy7bosagz3HaJkZXA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.3", - "fast-xml-parser": "5.7.3", + "@smithy/types": "^4.16.0", "tslib": "^2.6.2" }, "engines": { @@ -485,9 +384,9 @@ } }, "node_modules/@aws/lambda-invoke-store": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", - "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", "license": "Apache-2.0", "engines": { "node": ">=18.0.0" @@ -1565,18 +1464,6 @@ "@emnapi/runtime": "^1.7.1" } }, - "node_modules/@nodable/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/nodable" - } - ], - "license": "MIT" - }, "node_modules/@one-ini/wasm": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", @@ -1695,13 +1582,12 @@ } }, "node_modules/@smithy/core": { - "version": "3.24.7", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.7.tgz", - "integrity": "sha512-KoUi4M1f3BG6kzN1FnCwL7oyFptTbyBJKjR6yhSib+JHRdUmM1o+VwsFtJ66NZCkCzVfJMWRHJNo0R0jznp0Pg==", + "version": "3.29.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.29.3.tgz", + "integrity": "sha512-L+Ys6ecjk5vwPMAKHBpPKlJ3DkqwNcnfEISXBZIsVvWG/XKXfsAP8mwIYlTeLcd2ElHdesPI8OuOmJSFAPhm6A==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.14.4", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -1709,13 +1595,13 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.3.9", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.9.tgz", - "integrity": "sha512-ZlfJ/4Fa3jYb+3eaohPfG9utX9HmdhFNcFtpoGAhUhdynAOmGXtmigbi7eEiONKM+ykHw8RwKuDEb85Lx7t7fA==", + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.8.tgz", + "integrity": "sha512-q9J7JTiXrAhB8sDp4px97uEPT7CwKH61Co78grdNQvU8QZAdiuaSRhP0tUVf2ogy36RZTrlMU1rBmDEH+cnkiA==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.24.7", - "@smithy/types": "^4.14.4", + "@smithy/core": "^3.29.3", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -1723,39 +1609,27 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.4.7", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.7.tgz", - "integrity": "sha512-NslaM2ir0N2hisDmzXLstPaVINZheh8SokyOC++kzFPloZucL2R7Y7bS57mSzx/1Fc/fqmn7twjkeezTTrV0EA==", + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.5.tgz", + "integrity": "sha512-SuqeisTyPoiIPtIYru/sGxGyXzmZ+8nnFOhC+qRPglt06Ebd1yH//CDltZB2J/3WBNVhwfUaZ0EtHB3cm2X32g==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.24.7", - "@smithy/types": "^4.14.4", + "@smithy/core": "^3.29.3", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@smithy/node-http-handler": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.8.tgz", - "integrity": "sha512-f+DbsWUwSbtMu1a/j8Y93KiU1SRg9nyzfjereqn1BJ33QOTUXxdlYvVXMhAYl1vuR1Kmna5aIJe09KSIfyFNYw==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.5.tgz", + "integrity": "sha512-bNqdxTQTxmLbomSmlkZFz8L6B/feQ2HHzw4L2zY7Ecp2XffYAZq2uzdWDdxJHJFbEvqd+SRuluJso0P8+xPdbw==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.24.7", - "@smithy/types": "^4.14.4", + "@smithy/core": "^3.29.3", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -1763,13 +1637,13 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.4.7", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.7.tgz", - "integrity": "sha512-LwQZazFayImv+IOm0S0enoLeUJwmAlhGC5O6YCcLWezyu08dF46GOxPOq35OpBIHkgd7OvNvBStIFwVNyrvoBw==", + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.4.tgz", + "integrity": "sha512-B89bpf2t/y/wia6LZ+4JfHXYQT9PnVftsH05rgJKKIStS7r/4XSs9HOjtPoLtgcA6HCW9jVqX5DBbq7E0PAkiQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.24.7", - "@smithy/types": "^4.14.4", + "@smithy/core": "^3.29.3", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -1777,9 +1651,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.14.4", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.4.tgz", - "integrity": "sha512-B2S9+UGm1+/pHkcx3ZoLVX1a+pmSk8rqxRR+ZsNqZaJ5q9FWX9AFGQVM4qG5+OBeQUZVy99HY8HqW8gK/wgXzQ==", + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.16.1.tgz", + "integrity": "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1788,32 +1662,6 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", @@ -2389,18 +2237,6 @@ "node": ">= 8" } }, - "node_modules/anynum": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.0.tgz", - "integrity": "sha512-xjR9/zBVnUOP6ztMIIgShjsxui80nQUQH+5xJnvrYLs+90bF25/KJqaAi8mk+B4RDtX1Nspi6fmp4YTEts8SfA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -2746,6 +2582,16 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -3708,6 +3554,15 @@ "node": ">= 0.6" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -3880,43 +3735,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-xml-builder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", - "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "path-expression-matcher": "^1.5.0", - "xml-naming": "^0.1.0" - } - }, - "node_modules/fast-xml-parser": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", - "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "@nodable/entities": "^2.1.0", - "fast-xml-builder": "^1.1.7", - "path-expression-matcher": "^1.5.0", - "strnum": "^2.2.3" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -4539,6 +4357,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -6445,21 +6283,6 @@ "node": ">=8" } }, - "node_modules/path-expression-matcher": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", - "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -7376,6 +7199,16 @@ "node": ">= 0.8" } }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -7567,21 +7400,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strnum": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.0.tgz", - "integrity": "sha512-sHrVyWWdq28RbhjuJdZsA1SnGRJV6NiXbk6AXBxDOsgAcA+lmpUZCYjOdLBxkXMwis6RRe7dlZt4VlIWFVzkmg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "anynum": "^1.0.0" - } - }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -8225,21 +8043,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/xml-naming": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", - "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 4874b71..78cf112 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.1068.0", + "@aws-sdk/lib-storage": "^3.1085.0", "@aws-sdk/s3-request-presigner": "^3.1068.0", "axios": "^1.16.0", "bcryptjs": "^2.4.3", diff --git a/routes/admin/assets.routes.js b/routes/admin/assets.routes.js index 262fecb..9e56fe8 100644 --- a/routes/admin/assets.routes.js +++ b/routes/admin/assets.routes.js @@ -12,6 +12,7 @@ router.get('/field-values', controller.getAssetFieldValues); router.delete('/bulk', sensitiveOpsLimiter, controller.archiveAssets); router.delete('/bulk/permanent', sensitiveOpsLimiter, controller.permanentlyDeleteAssets); router.patch('/bulk-restore', controller.restoreAssets); +router.get('/upload-progress/:uploadId', controller.streamUploadProgress); // ─── Collection ─────────────────────────────────────────────────────────────── router.get('/', controller.getAssets); diff --git a/services/s3.service.js b/services/s3.service.js index 0024c86..9e5a43d 100644 --- a/services/s3.service.js +++ b/services/s3.service.js @@ -22,7 +22,8 @@ // whenever S3_ENDPOINT isn't reachable from this machine — // see resolvePublicHost() below) -const { S3Client, PutObjectCommand, DeleteObjectCommand, GetObjectCommand, HeadBucketCommand } = require("@aws-sdk/client-s3"); +const { S3Client, DeleteObjectCommand, GetObjectCommand, HeadBucketCommand } = require("@aws-sdk/client-s3"); +const { Upload } = require("@aws-sdk/lib-storage"); const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); const crypto = require("crypto"); const path = require("path"); @@ -127,7 +128,13 @@ async function buildPublicUrl(key, bucket = DEFAULT_BUCKET) { // input: { buffer, originalname, mimetype, ownerType? } // output: { url, uuid } ← uuid = the S3 key, stored as storage_key in DB // -async function uploadFile({ buffer, originalname, mimetype, ownerType = "image" }) { +// onProgress?: ({ loaded, total }) => void — real bytes-sent-to-Garage events, +// straight from the AWS SDK's own httpUploadProgress, not simulated. A plain +// PutObjectCommand (what this used to be) has no progress API at all; Upload +// auto-splits into multipart above its ~5MB partSize threshold, so large +// files (the case this actually matters for) report genuine incremental +// progress per part, while small ones just jump from 0 to 100 immediately. +async function uploadFile({ buffer, originalname, mimetype, ownerType = "image", onProgress }) { if (!buffer) { throw Object.assign(new Error("File buffer is required for S3 uploads."), { status: 400 }); } @@ -135,12 +142,18 @@ async function uploadFile({ buffer, originalname, mimetype, ownerType = "image" const bucket = DEFAULT_BUCKET; const key = buildKey(originalname, ownerType); - await s3.send(new PutObjectCommand({ - Bucket: bucket, - Key: key, - Body: buffer, - ContentType: mimetype, - })); + const uploader = new Upload({ + client: s3, + params: { Bucket: bucket, Key: key, Body: buffer, ContentType: mimetype }, + }); + + if (onProgress) { + uploader.on("httpUploadProgress", (progress) => { + onProgress({ loaded: progress.loaded ?? 0, total: progress.total ?? buffer.length }); + }); + } + + await uploader.done(); return { url: await buildPublicUrl(key, bucket), diff --git a/services/uploadProgress.service.js b/services/uploadProgress.service.js new file mode 100644 index 0000000..2e5c788 --- /dev/null +++ b/services/uploadProgress.service.js @@ -0,0 +1,52 @@ +/*********************************************************************************************************************************************************************** + * File Name: uploadProgress.service.js + * Type of Program: Service + * Description: In-memory SSE broadcaster for real upload progress on the + * Express -> S3 (Garage) leg of an asset upload. Keyed by a + * client-generated uploadId so the browser can open the stream + * before the upload request itself is even sent. + * + * No Redis — single-process only, same tradeoff already made by + * mediaToken.service.js's in-memory token cache. Fine for one + * instance; a second app instance would just never see progress + * for uploads routed to the other process. + * + * Author: Kenneth Obsequio (@lash0000) + ***********************************************************************************************************************************************************************/ +"use strict"; + +const clients = new Map(); // uploadId -> Response + +function subscribe(uploadId, res) { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache, no-transform", + Connection: "keep-alive", + "X-Accel-Buffering": "no", // disable proxy-side buffering (nginx-style intermediaries) + }); + res.write(": connected\n\n"); + clients.set(uploadId, res); + + res.on("close", () => { + if (clients.get(uploadId) === res) clients.delete(uploadId); + }); +} + +// No-ops if nobody's subscribed (client never opened the stream, or already +// disconnected) — progress is a best-effort visual, never load-bearing for +// the actual upload. +function publish(uploadId, data) { + const res = clients.get(uploadId); + if (!res) return; + res.write(`data: ${JSON.stringify(data)}\n\n`); +} + +function complete(uploadId, data = {}) { + const res = clients.get(uploadId); + if (!res) return; + res.write(`data: ${JSON.stringify({ ...data, done: true })}\n\n`); + res.end(); + clients.delete(uploadId); +} + +module.exports = { subscribe, publish, complete }; diff --git a/utils/buildQuery.util.js b/utils/buildQuery.util.js index acdbd64..f806b5b 100644 --- a/utils/buildQuery.util.js +++ b/utils/buildQuery.util.js @@ -18,9 +18,15 @@ const AUDIT_ID_FIELDS = new Set(["createdBy", "updatedBy", "deletedBy"]); * Builds a Sequelize `where` clause from an array of filters. * * @param {Array<{ id: string, value: any }>} filters + * @param {Set} allowedFields + * @param {string} [parentAlias] - main model's table alias (e.g. "User"). Needed + * to qualify Sequelize.col() references — unqualified col() refs throw + * "column reference is ambiguous" as soon as a query joins another table + * that happens to share a column name (e.g. Users + UserGroups both have + * is_active/createdAt/updatedAt/deletedAt). * @returns {Object} Sequelize where clause */ -function buildWhere(filters = [], allowedFields = new Set()) { +function buildWhere(filters = [], allowedFields = new Set(), parentAlias = null) { const where = []; for (const { id, value } of filters) { @@ -34,6 +40,8 @@ function buildWhere(filters = [], allowedFields = new Set()) { continue; } + const qualifiedCol = parentAlias ? `${parentAlias}.${id}` : id; + const conditions = values.map((v) => id.startsWith("personal_info.") ? Sequelize.where( @@ -41,7 +49,7 @@ function buildWhere(filters = [], allowedFields = new Set()) { { [Op.iLike]: `%${v}%` } ) : Sequelize.where( - Sequelize.cast(Sequelize.col(id), "TEXT"), + Sequelize.cast(Sequelize.col(qualifiedCol), "TEXT"), { [Op.iLike]: `%${v}%` } ) ); @@ -59,8 +67,9 @@ function buildWhere(filters = [], allowedFields = new Set()) { * @param {Array<{ id: string, desc: boolean }>} sort * @returns {Array} Sequelize order clause */ -function buildOrder(sort = [], allowedFields = new Set(), computedFields = new Set()) { +function buildOrder(sort = [], allowedFields = new Set(), computedFields = new Set(), parentAlias = null) { const order = []; + const qualifiedId = (id) => (parentAlias ? `"${parentAlias}"."${id}"` : `"${id}"`); for (const { id, desc } of sort) { if (!id) continue; @@ -83,7 +92,7 @@ function buildOrder(sort = [], allowedFields = new Set(), computedFields = new S .map((val, i) => `WHEN '${val}' THEN ${i}`) .join(" "); - order.push([Sequelize.literal(`CASE "${id}" ${caseExpr} END`)]); + order.push([Sequelize.literal(`CASE ${qualifiedId(id)} ${caseExpr} END`)]); continue; } @@ -107,7 +116,7 @@ function buildOrder(sort = [], allowedFields = new Set(), computedFields = new S * @param {Array} sort * @returns {{ where: Object, order: Array }} */ -function buildQuery(filters = [], sort = [], allowedFields = [], computedFields = []) { +function buildQuery(filters = [], sort = [], allowedFields = [], computedFields = [], parentAlias = null) { const fieldSet = new Set(allowedFields); const computedSet = new Set(computedFields); const orderFieldSet = new Set([...allowedFields, ...computedFields]); @@ -115,8 +124,8 @@ function buildQuery(filters = [], sort = [], allowedFields = [], computedFields return { // Computed (subquery/literal) columns aren't real table columns — filtering // via Sequelize.col() would error, so only allow them in ORDER BY, not WHERE. - where: buildWhere(filters, fieldSet), - order: buildOrder(sort, orderFieldSet, computedSet), + where: buildWhere(filters, fieldSet, parentAlias), + order: buildOrder(sort, orderFieldSet, computedSet, parentAlias), }; } diff --git a/utils/paginate.util.js b/utils/paginate.util.js index 6c0a9d9..56b4332 100644 --- a/utils/paginate.util.js +++ b/utils/paginate.util.js @@ -1,5 +1,5 @@ // utils/paginate.util.js -const { Sequelize } = require('sequelize'); +const { Sequelize, Op } = require('sequelize'); const { modelToAttributes } = require('./modelToAttributes.util'); const { excludeJsonbPaths } = require('./excludeJSONBPaths.util'); const { buildQuery } = require('./buildQuery.util'); @@ -108,7 +108,10 @@ async function paginate(model, req, { const ALLOWED_FIELDS = attributes.map((a) => a.field); const computedFieldKeys = computedAttributes.map((c) => c.key); - const { where, order } = buildQuery(filters, sort, ALLOWED_FIELDS, computedFieldKeys); + // Sequelize aliases the main model's table with the model's name by default + // (e.g. `FROM "users" AS "User"`) — needed to qualify Sequelize.col() + // references so they don't collide with same-named columns on joined tables. + const { where, order } = buildQuery(filters, sort, ALLOWED_FIELDS, computedFieldKeys, model.name); // Build attribute includes: jsonb + audit subqueries + any extra from findOptions const baseIncludes = jsonbAttr ? [jsonbAttr] : []; @@ -131,9 +134,16 @@ async function paginate(model, req, { const { attributes: _attr, ...restFindOptions } = findOptions; + // Nest rather than shallow-spread — `where` and `restFindOptions.where` can + // both carry an [Op.and] key (the same global Symbol), and spreading two + // objects that share a Symbol key silently drops the first one's value. + const combinedWhere = restFindOptions.where + ? { [Op.and]: [where, restFindOptions.where] } + : where; + const { count, rows } = await model.findAndCountAll({ ...restFindOptions, - where: { ...where, ...(restFindOptions.where ?? {}) }, + where: combinedWhere, order, limit, offset,