feat: solicitudes de compra como documento de marketing (solicitante, válido hasta, fecha necesaria, proveedor y fecha por línea) y API de usuarios con campos limitados
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a3bd976bbd
commit
c1f2cea1e1
19 files changed
+342
-62
No files matched your search
@@ -15,6 +15,7 @@ import {
|
||||
type GetQuery,
|
||||
type ListQuery,
|
||||
assertExpandAllowed,
|
||||
assertSelectAllowed,
|
||||
entityPath,
|
||||
} from './sl-request.js';
|
||||
import { SL_RESOURCE_MAP, type SlMethod, type SlResource } from './sl-resources.js';
|
||||
@@ -41,6 +42,7 @@ export class SlProxyService {
|
||||
async list(user: AuthUser, name: string, query: ListQuery, headers: Headers) {
|
||||
const resource = await this.access(user, name, 'GET');
|
||||
assertExpandAllowed(resource, query.$expand);
|
||||
assertSelectAllowed(resource, query.$select);
|
||||
const top = query.$top ?? DEFAULT_TOP;
|
||||
const res = await this.sap.request({
|
||||
method: 'GET',
|
||||
@@ -58,6 +60,7 @@ export class SlProxyService {
|
||||
async get(user: AuthUser, name: string, key: string, query: GetQuery) {
|
||||
const resource = await this.access(user, name, 'GET');
|
||||
assertExpandAllowed(resource, query.$expand);
|
||||
assertSelectAllowed(resource, query.$select);
|
||||
const res = await this.sap.request({ method: 'GET', path: entityPath(resource, key), query });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { menuPaths } from '@basic-erp/sap-types';
|
||||
import { SL_RESOURCES, SL_RESOURCE_MAP } from './sl-resources.js';
|
||||
import { entityPath, getQuerySchema, listQuerySchema, pickSlHeaders } from './sl-request.js';
|
||||
import { assertSelectAllowed, entityPath, getQuerySchema, listQuerySchema, pickSlHeaders } from './sl-request.js';
|
||||
|
||||
describe('registro de recursos', () => {
|
||||
it('no tiene nombres repetidos ni expone EmployeesInfo', () => {
|
||||
@@ -91,3 +91,26 @@ describe('pickSlHeaders', () => {
|
||||
expect(() => pickSlHeaders({ 'b1s-pagesize': '99999' }, 'GET')).toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('recursos con campos limitados ($select obligatorio)', () => {
|
||||
const users = SL_RESOURCE_MAP.get('Users')!;
|
||||
|
||||
it('acepta solo los campos de la lista', () => {
|
||||
expect(() => assertSelectAllowed(users, 'UserCode,UserName,eMail')).not.toThrow();
|
||||
});
|
||||
|
||||
it('rechaza un campo fuera de la lista, como la contraseña', () => {
|
||||
expect(() => assertSelectAllowed(users, 'UserCode,UserPassword')).toThrow(BadRequestException);
|
||||
expect(() => assertSelectAllowed(users, 'UserPassword')).toThrow(/no permitido: UserPassword/);
|
||||
});
|
||||
|
||||
it('rechaza pedir el recurso sin $select (traería todos los campos)', () => {
|
||||
expect(() => assertSelectAllowed(users, undefined)).toThrow(BadRequestException);
|
||||
expect(() => assertSelectAllowed(users, '')).toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('los demás recursos no se limitan', () => {
|
||||
expect(() => assertSelectAllowed(SL_RESOURCE_MAP.get('Items')!, undefined)).not.toThrow();
|
||||
expect(() => assertSelectAllowed(SL_RESOURCE_MAP.get('Items')!, 'ItemCode,Cualquiera')).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -102,6 +102,17 @@ export function entityPath(resource: SlResource, key: string): string {
|
||||
return `${resource.name}('${encodeURIComponent(key.replace(/'/g, "''"))}')`;
|
||||
}
|
||||
|
||||
/** Un recurso con campos limitados exige `$select` y solo acepta esos campos. */
|
||||
export function assertSelectAllowed(resource: SlResource, selectParam: string | undefined): void {
|
||||
if (!resource.selectOnly) return;
|
||||
const allowed = new Set(resource.selectOnly);
|
||||
const fields = (selectParam ?? '').split(',').map((f) => f.trim()).filter(Boolean);
|
||||
const rejected = fields.filter((f) => !allowed.has(f));
|
||||
if (!fields.length || rejected.length) {
|
||||
throw new BadRequestException(`${resource.name} solo permite $select de: ${resource.selectOnly.join(', ')}${rejected.length ? ` (no permitido: ${rejected.join(', ')})` : ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function assertExpandAllowed(resource: SlResource, expandParam: string | undefined): void {
|
||||
if (!expandParam) return;
|
||||
const allowed = new Set(resource.expand ?? []);
|
||||
|
||||
@@ -84,4 +84,11 @@ describe('recursos del Service Layer', () => {
|
||||
expect(rate.write).toBeNull();
|
||||
for (const name of ['ShippingTypes', 'WizardPaymentMethods']) expect(SL_RESOURCE_MAP.get(name)!.methods, name).toEqual(['GET']);
|
||||
});
|
||||
|
||||
it('los usuarios de SAP solo entregan los campos que necesita el solicitante', () => {
|
||||
const r = SL_RESOURCE_MAP.get('Users')!;
|
||||
expect(r.methods).toEqual(['GET']);
|
||||
expect(r.selectOnly).toEqual(['UserCode', 'UserName', 'eMail', 'Department', 'Branch']);
|
||||
expect(r.selectOnly).not.toContain('UserPassword');
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,11 @@ export interface SlResource {
|
||||
write: RequiredPath | null;
|
||||
/** Colecciones permitidas en `$expand` (por defecto ninguna). */
|
||||
expand?: string[];
|
||||
/**
|
||||
* Solo estos campos se pueden pedir (`$select` obligatorio y dentro de la lista): para recursos cuya entidad trae
|
||||
* más de lo que debe salir (los usuarios de SAP traen contraseña, permisos y actividad).
|
||||
*/
|
||||
selectOnly?: readonly string[];
|
||||
/** Acciones sobre un registro (`POST Entidad(clave)/Accion`) que se pueden invocar, p. ej. `Cancel`. Requieren el permiso de escritura. */
|
||||
actions?: readonly string[];
|
||||
}
|
||||
@@ -83,6 +88,12 @@ export const SL_RESOURCES: readonly SlResource[] = [
|
||||
// Finanzas: los asientos manuales se crean y se cancelan (`Cancel` genera el asiento de reverso), no se modifican.
|
||||
{ ...document('JournalEntries', '/finanzas/asientos'), methods: ['GET', 'POST'], actions: ['Cancel'] },
|
||||
|
||||
// Solicitante de una solicitud de compra: usuarios, departamentos y sucursales de SAP. Los usuarios solo entregan
|
||||
// código, nombre, correo, departamento y sucursal (la entidad trae también contraseña y permisos).
|
||||
{ ...catalog('Users', 'string'), selectOnly: ['UserCode', 'UserName', 'eMail', 'Department', 'Branch'] },
|
||||
catalog('Departments', 'number'),
|
||||
catalog('Branches', 'number'),
|
||||
|
||||
// Maestros (consulta para cualquier sesión; escritura según el módulo)
|
||||
catalog('BusinessPartners', 'string', ['/socios/clientes', '/socios/proveedores']),
|
||||
catalog('Items', 'string', '/inventario/articulos'),
|
||||
|
||||
Reference in new issue
Block a user