Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
fb96362
Merge pull request #478 from zenstackhq/dev
ymc9 Dec 12, 2025
d35b939
Merge pull request #486 from zenstackhq/dev
ymc9 Dec 13, 2025
39fb7d3
Merge pull request #489 from zenstackhq/dev
ymc9 Dec 14, 2025
69dcf6b
Merge pull request #500 from zenstackhq/dev
ymc9 Dec 14, 2025
3f3ffbe
Merge pull request #508 from zenstackhq/dev
ymc9 Dec 16, 2025
da6cf60
Merge pull request #517 from zenstackhq/dev
ymc9 Dec 18, 2025
e371ec9
Merge pull request #521 from zenstackhq/dev
ymc9 Dec 18, 2025
2966af1
Merge pull request #529 from zenstackhq/dev
ymc9 Dec 24, 2025
e71bee7
Merge pull request #532 from zenstackhq/dev
ymc9 Dec 24, 2025
de795f5
Merge pull request #545 from zenstackhq/dev
ymc9 Dec 30, 2025
9b95c29
feat(cli): implement watch mode for generate
DoctorFTB Dec 30, 2025
4895949
chore(root): update pnpm-lock.yaml
DoctorFTB Jan 5, 2026
469cb26
chore(cli): track all model declaration and removed paths, logs in pa…
DoctorFTB Jan 5, 2026
92814ca
fix(cli): typo, unused double array from
DoctorFTB Jan 5, 2026
c9f31ea
fix(orm): preserve zod validation errors when validating custom json …
ymc9 Jan 6, 2026
b625c50
update
ymc9 Jan 6, 2026
3d8f203
Merge branch 'fix/issue-558' into dev
ymc9 Jan 6, 2026
835a01b
chore(cli): move import, fix parallel generation on watch
DoctorFTB Jan 7, 2026
ce76178
Merge branch 'dev' of github.com:DoctorFTB/zenstack-v3 into dev
DoctorFTB Jan 7, 2026
f969ec4
feat(common-helpers): implement single-debounce
DoctorFTB Jan 7, 2026
87a95f0
chore(cli): use single-debounce for debouncing
DoctorFTB Jan 7, 2026
d966e2a
feat(common-helpers): implement single-debounce
DoctorFTB Jan 7, 2026
385e791
fix(common-helpers): re run single-debounce
DoctorFTB Jan 7, 2026
cf61959
Merge branch 'dev' of https://github.com/zenstackhq/zenstack-v3 into dev
ymc9 Jan 8, 2026
b3bb612
fix(qaas): add options validation
ymc9 Jan 8, 2026
5d1621a
Merge remote-tracking branch 'origin/dev' into fix/api-handler-option…
ymc9 Jan 8, 2026
8977043
fix pr comments
ymc9 Jan 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/server/src/api/common/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import z from 'zod';

export const loggerSchema = z.union([z.enum(['debug', 'info', 'warn', 'error']).array(), z.function()]);
56 changes: 40 additions & 16 deletions packages/server/src/api/rest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,12 @@ import tsjapi, { type Linker, type Paginator, type Relator, type Serializer, typ
import { match } from 'ts-pattern';
import UrlPattern from 'url-pattern';
import z from 'zod';
import { fromError } from 'zod-validation-error/v4';
import type { ApiHandler, LogConfig, RequestContext, Response } from '../../types';
import { getProcedureDef, mapProcedureArgs } from '../common/procedures';
import { loggerSchema } from '../common/schemas';
import { processSuperJsonRequestPayload } from '../common/utils';
import { getZodErrorMessage, log, registerCustomSerializers } from '../utils';
import {
getProcedureDef,
mapProcedureArgs,
} from '../common/procedures';
import {
processSuperJsonRequestPayload,
} from '../common/utils';

/**
* Options for {@link RestApiHandler}
Expand Down Expand Up @@ -58,8 +55,14 @@ export type RestApiHandlerOptions<Schema extends SchemaDef = SchemaDef> = {
*/
urlSegmentCharset?: string;

/**
* Mapping from model names to URL segment names.
*/
modelNameMapping?: Record<string, string>;

/**
* Mapping from model names to unique field name to be used as resource's ID.
*/
externalIdMapping?: Record<string, string>;
};

Expand Down Expand Up @@ -260,6 +263,8 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
private externalIdMapping: Record<string, string>;

constructor(private readonly options: RestApiHandlerOptions<Schema>) {
this.validateOptions(options);

this.idDivider = options.idDivider ?? DEFAULT_ID_DIVIDER;
const segmentCharset = options.urlSegmentCharset ?? 'a-zA-Z0-9-_~ %';

Expand All @@ -282,6 +287,23 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
this.buildSerializers();
}

private validateOptions(options: RestApiHandlerOptions<Schema>) {
const schema = z.strictObject({
schema: z.object(),
log: loggerSchema.optional(),
endpoint: z.string().min(1),
pageSize: z.number().positive().optional(),
idDivider: z.string().min(1).optional(),
urlSegmentCharset: z.string().min(1).optional(),
modelNameMapping: z.record(z.string(), z.string()).optional(),
externalIdMapping: z.record(z.string(), z.string()).optional(),
});
const parseResult = schema.safeParse(options);
if (!parseResult.success) {
throw new Error(`Invalid options: ${fromError(parseResult.error)}`);
}
}

get schema() {
return this.options.schema;
}
Expand Down Expand Up @@ -530,7 +552,9 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
try {
procInput = mapProcedureArgs(procDef, processedArgsPayload);
} catch (err) {
return this.makeProcBadInputErrorResponse(err instanceof Error ? err.message : 'invalid procedure arguments');
return this.makeProcBadInputErrorResponse(
err instanceof Error ? err.message : 'invalid procedure arguments',
);
}

try {
Expand Down Expand Up @@ -926,16 +950,16 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
prev:
offset - limit >= 0 && offset - limit <= total - 1
? this.replaceURLSearchParams(baseUrl, {
'page[offset]': offset - limit,
'page[limit]': limit,
})
'page[offset]': offset - limit,
'page[limit]': limit,
})
: null,
next:
offset + limit <= total - 1
? this.replaceURLSearchParams(baseUrl, {
'page[offset]': offset + limit,
'page[limit]': limit,
})
'page[offset]': offset + limit,
'page[limit]': limit,
})
: null,
}));
}
Expand Down Expand Up @@ -2001,8 +2025,8 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
} else {
currPayload[relation] = select
? {
select: { ...select },
}
select: { ...select },
}
: true;
}
}
Expand Down
47 changes: 28 additions & 19 deletions packages/server/src/api/rpc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@ import { ORMError, ORMErrorReason, type ClientContract } from '@zenstackhq/orm';
import type { SchemaDef } from '@zenstackhq/orm/schema';
import SuperJSON from 'superjson';
import { match } from 'ts-pattern';
import z from 'zod';
import { fromError } from 'zod-validation-error/v4';
import type { ApiHandler, LogConfig, RequestContext, Response } from '../../types';
import { getProcedureDef, mapProcedureArgs, PROCEDURE_ROUTE_PREFIXES } from '../common/procedures';
import { loggerSchema } from '../common/schemas';
import { processSuperJsonRequestPayload, unmarshalQ } from '../common/utils';
import { log, registerCustomSerializers } from '../utils';
import {
getProcedureDef,
mapProcedureArgs,
} from '../common/procedures';
import {
processSuperJsonRequestPayload,
unmarshalQ,
} from '../common/utils';

registerCustomSerializers();

Expand All @@ -35,7 +32,17 @@ export type RPCApiHandlerOptions<Schema extends SchemaDef = SchemaDef> = {
* RPC style API request handler that mirrors the ZenStackClient API
*/
export class RPCApiHandler<Schema extends SchemaDef = SchemaDef> implements ApiHandler<Schema> {
constructor(private readonly options: RPCApiHandlerOptions<Schema>) { }
constructor(private readonly options: RPCApiHandlerOptions<Schema>) {
this.validateOptions(options);
}

private validateOptions(options: RPCApiHandlerOptions<Schema>) {
const schema = z.strictObject({ schema: z.object(), log: loggerSchema.optional() });
const parseResult = schema.safeParse(options);
if (!parseResult.success) {
throw new Error(`Invalid options: ${fromError(parseResult.error)}`);
}
}

get schema(): Schema {
return this.options.schema;
Expand All @@ -54,7 +61,7 @@ export class RPCApiHandler<Schema extends SchemaDef = SchemaDef> implements ApiH
return this.makeBadInputErrorResponse('invalid request path');
}

if (model === '$procs') {
if (model === PROCEDURE_ROUTE_PREFIXES) {
return this.handleProcedureRequest({
client,
method: method.toUpperCase(),
Expand Down Expand Up @@ -96,9 +103,7 @@ export class RPCApiHandler<Schema extends SchemaDef = SchemaDef> implements ApiH
return this.makeBadInputErrorResponse('invalid request method, only GET is supported');
}
try {
args = query?.['q']
? unmarshalQ(query['q'] as string, query['meta'] as string | undefined)
: {};
args = query?.['q'] ? unmarshalQ(query['q'] as string, query['meta'] as string | undefined) : {};
} catch {
return this.makeBadInputErrorResponse('invalid "q" query parameter');
}
Expand All @@ -123,9 +128,7 @@ export class RPCApiHandler<Schema extends SchemaDef = SchemaDef> implements ApiH
return this.makeBadInputErrorResponse('invalid request method, only DELETE is supported');
}
try {
args = query?.['q']
? unmarshalQ(query['q'] as string, query['meta'] as string | undefined)
: {};
args = query?.['q'] ? unmarshalQ(query['q'] as string, query['meta'] as string | undefined) : {};
} catch (err) {
return this.makeBadInputErrorResponse(
err instanceof Error ? err.message : 'invalid "q" query parameter',
Expand Down Expand Up @@ -223,7 +226,9 @@ export class RPCApiHandler<Schema extends SchemaDef = SchemaDef> implements ApiH
? unmarshalQ(query['q'] as string, query['meta'] as string | undefined)
: undefined;
} catch (err) {
return this.makeBadInputErrorResponse(err instanceof Error ? err.message : 'invalid "q" query parameter');
return this.makeBadInputErrorResponse(
err instanceof Error ? err.message : 'invalid "q" query parameter',
);
}
}

Expand Down Expand Up @@ -251,7 +256,11 @@ export class RPCApiHandler<Schema extends SchemaDef = SchemaDef> implements ApiH
}

const response = { status: 200, body: responseBody };
log(this.options.log, 'debug', () => `sending response for "$procs.${proc}" request: ${safeJSONStringify(response)}`);
log(
this.options.log,
'debug',
() => `sending response for "$procs.${proc}" request: ${safeJSONStringify(response)}`,
);
return response;
} catch (err) {
log(this.options.log, 'error', `error occurred when handling "$procs.${proc}" request`, err);
Expand Down Expand Up @@ -312,7 +321,7 @@ export class RPCApiHandler<Schema extends SchemaDef = SchemaDef> implements ApiH
status = 400;
error.dbErrorCode = err.dbErrorCode;
})
.otherwise(() => { });
.otherwise(() => {});

const resp = { status, body: { error } };
log(this.options.log, 'debug', () => `sending error response: ${safeJSONStringify(resp)}`);
Expand Down
Loading
Loading