[PUT-487] fix: set max-min on app-user-count queries 🐛 (#2384)
Docker Image CI / build-and-push-image (push) Has been cancelled
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
release-please / release-please (push) Has been cancelled
test / test-backend (24.x) (push) Has been cancelled
test / API tests (node env, api-test) (24.x) (push) Has been cancelled
test / puterjs (node env, vitest) (24.x) (push) Has been cancelled

This commit is contained in:
Daniel Salazar
2026-01-31 00:52:53 -08:00
committed by GitHub
parent af62835d48
commit 7a9302f479
2 changed files with 65 additions and 9 deletions
+10 -7
View File
@@ -1,21 +1,23 @@
import APIError from '@heyputer/backend/src/api/APIError.js';
import type APIError from '@heyputer/backend/src/api/APIError.js';
import type { WebServerService } from '@heyputer/backend/src/modules/web/WebServerService.js';
import query from '@heyputer/backend/src/om/query/query';
import type query from '@heyputer/backend/src/om/query/query';
import type { Actor } from '@heyputer/backend/src/services/auth/Actor.js';
import type { BaseDatabaseAccessService } from '@heyputer/backend/src/services/database/BaseDatabaseAccessService.d.ts';
import type { EmailService } from '@heyputer/backend/src/services/EmailService.js';
import type { EntityStoreService } from '@heyputer/backend/src/services/EntityStoreService.js';
import type { MeteringService } from '@heyputer/backend/src/services/MeteringService/MeteringService.ts';
import type { MeteringServiceWrapper } from '@heyputer/backend/src/services/MeteringService/MeteringServiceWrapper.mjs';
import { DynamoKVStore } from '@heyputer/backend/src/services/repositories/DynamoKVStore/DynamoKVStore.ts';
import type { DynamoKVStore } from '@heyputer/backend/src/services/repositories/DynamoKVStore/DynamoKVStore.ts';
import type { SUService } from '@heyputer/backend/src/services/SUService.js';
import type { IUser } from '@heyputer/backend/src/services/User.js';
import type { UserService } from '@heyputer/backend/src/services/UserService.d.ts';
import { Context } from '@heyputer/backend/src/util/context.js';
import kvjs from '@heyputer/kv.js';
import type { Context } from '@heyputer/backend/src/util/context.js';
import type kvjs from '@heyputer/kv.js';
import type { RequestHandler } from 'express';
import type FSNodeContext from '../src/backend/src/filesystem/FSNodeContext.js';
import type helpers from '../src/backend/src/helpers.js';
import type * as ExtensionControllerExports from './ExtensionController/src/ExtensionController.ts';
import { type EmailService } from '@heyputer/backend/src/services/EmailService.js';
declare global {
namespace Express {
interface Request {
@@ -98,7 +100,8 @@ interface ServiceNameMap {
database: BaseDatabaseAccessService;
user: UserService;
'web-server': WebServerService;
'email': EmailService
'email': EmailService;
'es:app': EntityStoreService;
}
export interface ExtensionEventTypeMap {
+55 -2
View File
@@ -3,6 +3,46 @@ const { db } = extension.import('data');
const { APIError } = extension.import('core');
const app_es = extension.import('service:es:app') as any;
const DEFAULT_LIMIT = 100;
const MAX_LIMIT = 1000;
const MAX_OFFSET = 100_000;
const parseIntegerParam = (
value: unknown,
{
key,
min,
max,
fallback,
}: { key: string, min: number, max: number, fallback: number },
) => {
if ( value === undefined || value === null ) return fallback;
const parsed = typeof value === 'number'
? value
: (typeof value === 'string' && value.trim() !== ''
? Number(value)
: Number.NaN);
if ( !Number.isFinite(parsed) || !Number.isInteger(parsed) ) {
throw APIError.create('field_invalid', undefined, {
key,
expected: `an integer between ${min} and ${max}`,
got: value,
});
}
if ( parsed < min || parsed > max ) {
throw APIError.create('field_invalid', undefined, {
key,
expected: `an integer between ${min} and ${max}`,
got: parsed,
});
}
return parsed;
};
extension.on('create.interfaces', (event) => {
event.createInterface('app-telemetry', {
description: 'Provides methods for getting app telemetry',
@@ -39,7 +79,20 @@ extension.on('create.interfaces', (event) => {
extension.on('create.drivers', event => {
event.createDriver('app-telemetry', 'app-telemetry', {
async get_users ({ app_uuid, limit = 100, offset = 0 }: { app_uuid: string, limit: number, offset: number }) {
async get_users ({ app_uuid, limit, offset }: { app_uuid: string, limit?: number, offset?: number }) {
const safeLimit = parseIntegerParam(limit, {
key: 'limit',
min: 1,
max: MAX_LIMIT,
fallback: DEFAULT_LIMIT,
});
const safeOffset = parseIntegerParam(offset, {
key: 'offset',
min: 0,
max: MAX_OFFSET,
fallback: 0,
});
// first lets make sure executor owns this app
const [result] = (await app_es.select({ predicate: new Eq({ key: 'uid', value: app_uuid }) }));
if ( ! result ) {
@@ -50,7 +103,7 @@ extension.on('create.drivers', event => {
const users: Array<{ username: string, uuid: string }> = await db.read(`SELECT user.username, user.uuid FROM user_to_app_permissions
INNER JOIN user ON user_to_app_permissions.user_id = user.id
WHERE permission = 'flag:app-is-authenticated' AND app_id=? ORDER BY (dt IS NOT NULL), dt, user_id LIMIT ? OFFSET ?`,
[result.private_meta.mysql_id, limit, offset]);
[result.private_meta.mysql_id, safeLimit, safeOffset]);
return users.map(e => {
return { user: e.username, user_uuid: e.uuid };
});