feat: add v1 management APIs, audit logs, and import/export
This commit is contained in:
@@ -39,9 +39,9 @@
|
|||||||
|
|
||||||
## Phase 2 — v1 增强(P1)
|
## Phase 2 — v1 增强(P1)
|
||||||
- [ ] 模糊/正则性能优化(索引/缓存)
|
- [ ] 模糊/正则性能优化(索引/缓存)
|
||||||
- [ ] 管理命令与校验(重命名/删除/迁移)
|
- [x] 管理命令与校验(重命名/删除/迁移)
|
||||||
- [ ] 完善错误码与审计日志
|
- [x] 完善错误码与审计日志
|
||||||
- [ ] 增加导入/导出工具
|
- [x] 增加导入/导出工具
|
||||||
|
|
||||||
## Phase 3 — 体验与文档(P1)
|
## Phase 3 — 体验与文档(P1)
|
||||||
- [ ] README(安装/配置/示例)
|
- [ ] README(安装/配置/示例)
|
||||||
|
|||||||
@@ -31,6 +31,14 @@ Implemented in this branch:
|
|||||||
- `queryAgents`
|
- `queryAgents`
|
||||||
- Query parser/executor with pagination
|
- Query parser/executor with pagination
|
||||||
- Scope memory adapter (`put/search`)
|
- Scope memory adapter (`put/search`)
|
||||||
|
- Management APIs:
|
||||||
|
- `renameDepartment`
|
||||||
|
- `renameTeam`
|
||||||
|
- `migrateTeam`
|
||||||
|
- `deleteDepartment`
|
||||||
|
- `deleteTeam`
|
||||||
|
- Error code model (`YonexusError`) and audit logs
|
||||||
|
- Import/export APIs (`importData` / `exportData`)
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
|||||||
126
src/index.ts
126
src/index.ts
@@ -1,5 +1,7 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { DEFAULT_SCHEMA } from "./config/defaults";
|
import { DEFAULT_SCHEMA } from "./config/defaults";
|
||||||
|
import type { AuditLogEntry } from "./models/audit";
|
||||||
|
import { YonexusError } from "./models/errors";
|
||||||
import type {
|
import type {
|
||||||
Actor,
|
Actor,
|
||||||
Agent,
|
Agent,
|
||||||
@@ -10,6 +12,7 @@ import type {
|
|||||||
YonexusSchema
|
YonexusSchema
|
||||||
} from "./models/types";
|
} from "./models/types";
|
||||||
import { authorize } from "./permissions/authorize";
|
import { authorize } from "./permissions/authorize";
|
||||||
|
import { AuditStore } from "./store/auditStore";
|
||||||
import { JsonStore } from "./store/jsonStore";
|
import { JsonStore } from "./store/jsonStore";
|
||||||
import { queryIdentities } from "./tools/query";
|
import { queryIdentities } from "./tools/query";
|
||||||
import { makeId } from "./utils/id";
|
import { makeId } from "./utils/id";
|
||||||
@@ -24,6 +27,7 @@ export class Yonexus {
|
|||||||
private readonly schema: YonexusSchema;
|
private readonly schema: YonexusSchema;
|
||||||
private readonly registrars: Set<string>;
|
private readonly registrars: Set<string>;
|
||||||
private readonly store: JsonStore;
|
private readonly store: JsonStore;
|
||||||
|
private readonly audit = new AuditStore();
|
||||||
|
|
||||||
constructor(options: YonexusOptions = {}) {
|
constructor(options: YonexusOptions = {}) {
|
||||||
const dataFile = options.dataFile ?? path.resolve(process.cwd(), "data/org.json");
|
const dataFile = options.dataFile ?? path.resolve(process.cwd(), "data/org.json");
|
||||||
@@ -32,32 +36,62 @@ export class Yonexus {
|
|||||||
this.registrars = new Set(options.registrars ?? []);
|
this.registrars = new Set(options.registrars ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private log(entry: Omit<AuditLogEntry, "id" | "ts">): void {
|
||||||
|
this.audit.append({
|
||||||
|
id: makeId("audit", `${entry.actorId}-${entry.action}-${Date.now()}`),
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
...entry
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
createDepartment(actor: Actor, name: string, orgId: string) {
|
createDepartment(actor: Actor, name: string, orgId: string) {
|
||||||
|
try {
|
||||||
authorize("create_department", actor, { orgId }, this.store);
|
authorize("create_department", actor, { orgId }, this.store);
|
||||||
const dept = { id: makeId("dept", name), name, orgId };
|
const dept = { id: makeId("dept", name), name, orgId };
|
||||||
return this.store.addDepartment(dept);
|
const result = this.store.addDepartment(dept);
|
||||||
|
this.log({ actorId: actor.agentId, action: "create_department", target: result.id, status: "ok" });
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
this.log({
|
||||||
|
actorId: actor.agentId,
|
||||||
|
action: "create_department",
|
||||||
|
target: name,
|
||||||
|
status: "error",
|
||||||
|
message: error instanceof Error ? error.message : String(error)
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createTeam(actor: Actor, name: string, deptId: string) {
|
createTeam(actor: Actor, name: string, deptId: string) {
|
||||||
authorize("create_team", actor, { deptId }, this.store);
|
authorize("create_team", actor, { deptId }, this.store);
|
||||||
if (!this.store.findDepartment(deptId)) throw new Error(`department_not_found: ${deptId}`);
|
if (!this.store.findDepartment(deptId)) {
|
||||||
|
throw new YonexusError("NOT_FOUND", `department_not_found: ${deptId}`, { deptId });
|
||||||
|
}
|
||||||
const team = { id: makeId("team", `${deptId}-${name}`), name, deptId };
|
const team = { id: makeId("team", `${deptId}-${name}`), name, deptId };
|
||||||
return this.store.addTeam(team);
|
const result = this.store.addTeam(team);
|
||||||
|
this.log({ actorId: actor.agentId, action: "create_team", target: result.id, status: "ok" });
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerAgent(actor: Actor, agentId: string, name: string, roles: Agent["roles"] = ["agent"]) {
|
registerAgent(actor: Actor, agentId: string, name: string, roles: Agent["roles"] = ["agent"]) {
|
||||||
if (this.registrars.size > 0 && !this.registrars.has(actor.agentId)) {
|
if (this.registrars.size > 0 && !this.registrars.has(actor.agentId)) {
|
||||||
authorize("register_agent", actor, {}, this.store);
|
throw new YonexusError("REGISTRAR_DENIED", `registrar_denied: ${actor.agentId}`);
|
||||||
}
|
}
|
||||||
if (this.store.findAgent(agentId)) throw new Error(`agent_exists: ${agentId}`);
|
authorize("register_agent", actor, {}, this.store);
|
||||||
return this.store.addAgent({ id: agentId, name, roles });
|
if (this.store.findAgent(agentId)) {
|
||||||
|
throw new YonexusError("ALREADY_EXISTS", `agent_exists: ${agentId}`, { agentId });
|
||||||
|
}
|
||||||
|
const result = this.store.addAgent({ id: agentId, name, roles });
|
||||||
|
this.log({ actorId: actor.agentId, action: "register_agent", target: result.id, status: "ok" });
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
assignIdentity(actor: Actor, agentId: string, deptId: string, teamId: string, meta: Record<string, string>): Identity {
|
assignIdentity(actor: Actor, agentId: string, deptId: string, teamId: string, meta: Record<string, string>): Identity {
|
||||||
authorize("assign_identity", actor, { deptId, teamId }, this.store);
|
authorize("assign_identity", actor, { deptId, teamId }, this.store);
|
||||||
if (!this.store.findAgent(agentId)) throw new Error(`agent_not_found: ${agentId}`);
|
if (!this.store.findAgent(agentId)) throw new YonexusError("NOT_FOUND", `agent_not_found: ${agentId}`);
|
||||||
if (!this.store.findDepartment(deptId)) throw new Error(`department_not_found: ${deptId}`);
|
if (!this.store.findDepartment(deptId)) throw new YonexusError("NOT_FOUND", `department_not_found: ${deptId}`);
|
||||||
if (!this.store.findTeam(teamId)) throw new Error(`team_not_found: ${teamId}`);
|
if (!this.store.findTeam(teamId)) throw new YonexusError("NOT_FOUND", `team_not_found: ${teamId}`);
|
||||||
|
|
||||||
const validatedMeta: Record<string, string> = {};
|
const validatedMeta: Record<string, string> = {};
|
||||||
for (const [field, value] of Object.entries(meta)) {
|
for (const [field, value] of Object.entries(meta)) {
|
||||||
@@ -65,26 +99,30 @@ export class Yonexus {
|
|||||||
validatedMeta[field] = String(value);
|
validatedMeta[field] = String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.store.addIdentity({
|
const result = this.store.addIdentity({
|
||||||
id: makeId("identity", `${agentId}-${deptId}-${teamId}-${Date.now()}`),
|
id: makeId("identity", `${agentId}-${deptId}-${teamId}-${Date.now()}`),
|
||||||
agentId,
|
agentId,
|
||||||
deptId,
|
deptId,
|
||||||
teamId,
|
teamId,
|
||||||
meta: validatedMeta
|
meta: validatedMeta
|
||||||
});
|
});
|
||||||
|
this.log({ actorId: actor.agentId, action: "assign_identity", target: result.id, status: "ok" });
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSupervisor(actor: Actor, agentId: string, supervisorId: string, deptId?: string) {
|
setSupervisor(actor: Actor, agentId: string, supervisorId: string, deptId?: string) {
|
||||||
authorize("set_supervisor", actor, { deptId }, this.store);
|
authorize("set_supervisor", actor, { deptId }, this.store);
|
||||||
if (!this.store.findAgent(agentId)) throw new Error(`agent_not_found: ${agentId}`);
|
if (!this.store.findAgent(agentId)) throw new YonexusError("NOT_FOUND", `agent_not_found: ${agentId}`);
|
||||||
if (!this.store.findAgent(supervisorId)) throw new Error(`supervisor_not_found: ${supervisorId}`);
|
if (!this.store.findAgent(supervisorId)) throw new YonexusError("NOT_FOUND", `supervisor_not_found: ${supervisorId}`);
|
||||||
if (agentId === supervisorId) throw new Error("invalid_supervisor: self_reference");
|
if (agentId === supervisorId) throw new YonexusError("INVALID_SUPERVISOR", "invalid_supervisor: self_reference");
|
||||||
return this.store.upsertSupervisor({ agentId, supervisorId });
|
const result = this.store.upsertSupervisor({ agentId, supervisorId });
|
||||||
|
this.log({ actorId: actor.agentId, action: "set_supervisor", target: `${agentId}->${supervisorId}`, status: "ok" });
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
whoami(agentId: string) {
|
whoami(agentId: string) {
|
||||||
const agent = this.store.findAgent(agentId);
|
const agent = this.store.findAgent(agentId);
|
||||||
if (!agent) throw new Error(`agent_not_found: ${agentId}`);
|
if (!agent) throw new YonexusError("NOT_FOUND", `agent_not_found: ${agentId}`);
|
||||||
|
|
||||||
const identities = this.store.listIdentities().filter((x) => x.agentId === agentId);
|
const identities = this.store.listIdentities().filter((x) => x.agentId === agentId);
|
||||||
const supervisor = this.store.findSupervisor(agentId);
|
const supervisor = this.store.findSupervisor(agentId);
|
||||||
@@ -97,6 +135,64 @@ export class Yonexus {
|
|||||||
return queryIdentities(identities, query, this.schema);
|
return queryIdentities(identities, query, this.schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renameDepartment(actor: Actor, deptId: string, newName: string) {
|
||||||
|
authorize("create_department", actor, {}, this.store);
|
||||||
|
const updated = this.store.renameDepartment(deptId, newName);
|
||||||
|
if (!updated) throw new YonexusError("NOT_FOUND", `department_not_found: ${deptId}`);
|
||||||
|
this.log({ actorId: actor.agentId, action: "rename_department", target: deptId, status: "ok" });
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
renameTeam(actor: Actor, teamId: string, newName: string, deptId?: string) {
|
||||||
|
authorize("create_team", actor, { deptId }, this.store);
|
||||||
|
const updated = this.store.renameTeam(teamId, newName);
|
||||||
|
if (!updated) throw new YonexusError("NOT_FOUND", `team_not_found: ${teamId}`);
|
||||||
|
this.log({ actorId: actor.agentId, action: "rename_team", target: teamId, status: "ok" });
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
migrateTeam(actor: Actor, teamId: string, newDeptId: string) {
|
||||||
|
authorize("create_team", actor, { deptId: newDeptId }, this.store);
|
||||||
|
if (!this.store.findDepartment(newDeptId)) throw new YonexusError("NOT_FOUND", `department_not_found: ${newDeptId}`);
|
||||||
|
const updated = this.store.migrateTeam(teamId, newDeptId);
|
||||||
|
if (!updated) throw new YonexusError("NOT_FOUND", `team_not_found: ${teamId}`);
|
||||||
|
this.log({ actorId: actor.agentId, action: "migrate_team", target: `${teamId}->${newDeptId}`, status: "ok" });
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteDepartment(actor: Actor, deptId: string) {
|
||||||
|
authorize("create_department", actor, {}, this.store);
|
||||||
|
const ok = this.store.deleteDepartment(deptId);
|
||||||
|
if (!ok) throw new YonexusError("NOT_FOUND", `department_not_found: ${deptId}`);
|
||||||
|
this.log({ actorId: actor.agentId, action: "delete_department", target: deptId, status: "ok" });
|
||||||
|
return { ok };
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTeam(actor: Actor, teamId: string, deptId?: string) {
|
||||||
|
authorize("create_team", actor, { deptId }, this.store);
|
||||||
|
const ok = this.store.deleteTeam(teamId);
|
||||||
|
if (!ok) throw new YonexusError("NOT_FOUND", `team_not_found: ${teamId}`);
|
||||||
|
this.log({ actorId: actor.agentId, action: "delete_team", target: teamId, status: "ok" });
|
||||||
|
return { ok };
|
||||||
|
}
|
||||||
|
|
||||||
|
exportData(actor: Actor): StoreState {
|
||||||
|
authorize("query_agents", actor, {}, this.store);
|
||||||
|
this.log({ actorId: actor.agentId, action: "export_data", status: "ok" });
|
||||||
|
return this.store.snapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
importData(actor: Actor, state: StoreState): { ok: true } {
|
||||||
|
authorize("create_department", actor, {}, this.store);
|
||||||
|
this.store.replace(state);
|
||||||
|
this.log({ actorId: actor.agentId, action: "import_data", status: "ok" });
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
listAuditLogs(limit = 100, offset = 0): AuditLogEntry[] {
|
||||||
|
return this.audit.list(limit, offset);
|
||||||
|
}
|
||||||
|
|
||||||
debugSnapshot(): StoreState {
|
debugSnapshot(): StoreState {
|
||||||
return this.store.snapshot();
|
return this.store.snapshot();
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/models/audit.ts
Normal file
10
src/models/audit.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export interface AuditLogEntry {
|
||||||
|
id: string;
|
||||||
|
ts: string;
|
||||||
|
actorId: string;
|
||||||
|
action: string;
|
||||||
|
target?: string;
|
||||||
|
status: 'ok' | 'error';
|
||||||
|
message?: string;
|
||||||
|
meta?: Record<string, unknown>;
|
||||||
|
}
|
||||||
19
src/models/errors.ts
Normal file
19
src/models/errors.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export type ErrorCode =
|
||||||
|
| 'PERMISSION_DENIED'
|
||||||
|
| 'NOT_FOUND'
|
||||||
|
| 'ALREADY_EXISTS'
|
||||||
|
| 'VALIDATION_ERROR'
|
||||||
|
| 'FIELD_NOT_QUERYABLE'
|
||||||
|
| 'INVALID_SUPERVISOR'
|
||||||
|
| 'REGISTRAR_DENIED';
|
||||||
|
|
||||||
|
export class YonexusError extends Error {
|
||||||
|
constructor(
|
||||||
|
public readonly code: ErrorCode,
|
||||||
|
message: string,
|
||||||
|
public readonly details?: Record<string, unknown>
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'YonexusError';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { YonexusError } from '../models/errors';
|
||||||
import type { Action, Actor, Scope } from "../models/types";
|
import type { Action, Actor, Scope } from "../models/types";
|
||||||
import { JsonStore } from "../store/jsonStore";
|
import { JsonStore } from "../store/jsonStore";
|
||||||
|
|
||||||
@@ -29,6 +30,10 @@ export function authorize(action: Action, actor: Actor, scope: Scope, store: Jso
|
|||||||
(action === "query_agents" && (orgAdmin || deptAdmin || teamLead || agent));
|
(action === "query_agents" && (orgAdmin || deptAdmin || teamLead || agent));
|
||||||
|
|
||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
throw new Error(`permission_denied: ${action}`);
|
throw new YonexusError('PERMISSION_DENIED', `permission_denied: ${action}`, {
|
||||||
|
action,
|
||||||
|
actorId: actor.agentId,
|
||||||
|
scope
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/store/auditStore.ts
Normal file
19
src/store/auditStore.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { AuditLogEntry } from '../models/audit';
|
||||||
|
|
||||||
|
const MAX_AUDIT = 1000;
|
||||||
|
|
||||||
|
export class AuditStore {
|
||||||
|
private logs: AuditLogEntry[] = [];
|
||||||
|
|
||||||
|
append(entry: AuditLogEntry): AuditLogEntry {
|
||||||
|
this.logs.push(entry);
|
||||||
|
if (this.logs.length > MAX_AUDIT) this.logs.shift();
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
list(limit = 100, offset = 0): AuditLogEntry[] {
|
||||||
|
const safeLimit = Math.min(Math.max(1, limit), 500);
|
||||||
|
const safeOffset = Math.max(0, offset);
|
||||||
|
return this.logs.slice(safeOffset, safeOffset + safeLimit);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,11 @@ export class JsonStore {
|
|||||||
return JSON.parse(JSON.stringify(this.state)) as StoreState;
|
return JSON.parse(JSON.stringify(this.state)) as StoreState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
replace(state: StoreState): void {
|
||||||
|
this.state = JSON.parse(JSON.stringify(state)) as StoreState;
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
|
||||||
addOrganization(org: Organization): Organization {
|
addOrganization(org: Organization): Organization {
|
||||||
this.state.organizations.push(org);
|
this.state.organizations.push(org);
|
||||||
this.save();
|
this.save();
|
||||||
@@ -37,12 +42,58 @@ export class JsonStore {
|
|||||||
return dept;
|
return dept;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renameDepartment(deptId: string, name: string): Department | undefined {
|
||||||
|
const dept = this.findDepartment(deptId);
|
||||||
|
if (!dept) return undefined;
|
||||||
|
dept.name = name;
|
||||||
|
this.save();
|
||||||
|
return dept;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteDepartment(deptId: string): boolean {
|
||||||
|
const before = this.state.departments.length;
|
||||||
|
this.state.departments = this.state.departments.filter((d) => d.id !== deptId);
|
||||||
|
this.state.teams = this.state.teams.filter((t) => t.deptId !== deptId);
|
||||||
|
this.state.identities = this.state.identities.filter((i) => i.deptId !== deptId);
|
||||||
|
const changed = this.state.departments.length !== before;
|
||||||
|
if (changed) this.save();
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
addTeam(team: Team): Team {
|
addTeam(team: Team): Team {
|
||||||
this.state.teams.push(team);
|
this.state.teams.push(team);
|
||||||
this.save();
|
this.save();
|
||||||
return team;
|
return team;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renameTeam(teamId: string, name: string): Team | undefined {
|
||||||
|
const team = this.findTeam(teamId);
|
||||||
|
if (!team) return undefined;
|
||||||
|
team.name = name;
|
||||||
|
this.save();
|
||||||
|
return team;
|
||||||
|
}
|
||||||
|
|
||||||
|
migrateTeam(teamId: string, newDeptId: string): Team | undefined {
|
||||||
|
const team = this.findTeam(teamId);
|
||||||
|
if (!team) return undefined;
|
||||||
|
team.deptId = newDeptId;
|
||||||
|
for (const identity of this.state.identities) {
|
||||||
|
if (identity.teamId === teamId) identity.deptId = newDeptId;
|
||||||
|
}
|
||||||
|
this.save();
|
||||||
|
return team;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTeam(teamId: string): boolean {
|
||||||
|
const before = this.state.teams.length;
|
||||||
|
this.state.teams = this.state.teams.filter((t) => t.id !== teamId);
|
||||||
|
this.state.identities = this.state.identities.filter((i) => i.teamId !== teamId);
|
||||||
|
const changed = before !== this.state.teams.length;
|
||||||
|
if (changed) this.save();
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
addAgent(agent: Agent): Agent {
|
addAgent(agent: Agent): Agent {
|
||||||
this.state.agents.push(agent);
|
this.state.agents.push(agent);
|
||||||
this.save();
|
this.save();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { YonexusError } from '../models/errors';
|
||||||
import type { Identity, QueryFilter, QueryInput, QueryOptions, YonexusSchema } from "../models/types";
|
import type { Identity, QueryFilter, QueryInput, QueryOptions, YonexusSchema } from "../models/types";
|
||||||
|
|
||||||
const DEFAULT_LIMIT = 20;
|
const DEFAULT_LIMIT = 20;
|
||||||
@@ -33,7 +34,9 @@ function normalizeOptions(options?: QueryOptions): Required<QueryOptions> {
|
|||||||
export function queryIdentities(identities: Identity[], input: QueryInput, schema: YonexusSchema): Identity[] {
|
export function queryIdentities(identities: Identity[], input: QueryInput, schema: YonexusSchema): Identity[] {
|
||||||
for (const filter of input.filters) {
|
for (const filter of input.filters) {
|
||||||
if (!isQueryable(filter.field, schema)) {
|
if (!isQueryable(filter.field, schema)) {
|
||||||
throw new Error(`field_not_queryable: ${filter.field}`);
|
throw new YonexusError('FIELD_NOT_QUERYABLE', `field_not_queryable: ${filter.field}`, {
|
||||||
|
field: filter.field
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user