@vertz/server 0.2.18 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +118 -1
- package/dist/index.js +548 -0
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -2063,6 +2063,123 @@ declare function stripHiddenFields(table: TableDef2, data: Record<string, unknow
|
|
|
2063
2063
|
* Used before DB writes to prevent setting immutable fields.
|
|
2064
2064
|
*/
|
|
2065
2065
|
declare function stripReadOnlyFields(table: TableDef2, data: Record<string, unknown>): Record<string, unknown>;
|
|
2066
|
+
import { ColumnBuilder, ColumnMetadata } from "@vertz/db";
|
|
2067
|
+
interface JSONSchemaObject {
|
|
2068
|
+
type?: string | string[];
|
|
2069
|
+
format?: string;
|
|
2070
|
+
enum?: readonly (string | number | boolean | null)[];
|
|
2071
|
+
items?: JSONSchemaObject;
|
|
2072
|
+
maxLength?: number;
|
|
2073
|
+
description?: string;
|
|
2074
|
+
$ref?: string;
|
|
2075
|
+
[key: string]: unknown;
|
|
2076
|
+
}
|
|
2077
|
+
/**
|
|
2078
|
+
* Maps a single database column to its JSON Schema representation.
|
|
2079
|
+
*/
|
|
2080
|
+
declare function columnToJsonSchema(column: ColumnBuilder<unknown, ColumnMetadata>): JSONSchemaObject;
|
|
2081
|
+
interface EntitySchemaObject {
|
|
2082
|
+
type: "object";
|
|
2083
|
+
required?: string[];
|
|
2084
|
+
properties?: Record<string, JSONSchemaObject>;
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* Generates a JSON Schema for an entity's response shape.
|
|
2088
|
+
*
|
|
2089
|
+
* When `expose.select` is present, only listed fields appear.
|
|
2090
|
+
* Otherwise, all public (non-hidden) columns appear.
|
|
2091
|
+
* Descriptor-guarded fields (AccessRule values) become nullable with a description.
|
|
2092
|
+
*
|
|
2093
|
+
* If `relationSchemas` is provided, relation schemas are collected into it
|
|
2094
|
+
* with keys like `TasksAssigneeResponse`, and $ref properties are added
|
|
2095
|
+
* to the parent schema for each relation.
|
|
2096
|
+
*/
|
|
2097
|
+
declare function entityResponseSchema(def: EntityDefinition, relationSchemas?: Record<string, EntitySchemaObject>): EntitySchemaObject;
|
|
2098
|
+
/**
|
|
2099
|
+
* Generates a JSON Schema for an entity's create input.
|
|
2100
|
+
* Excludes PK, readOnly, autoUpdate, and hidden columns.
|
|
2101
|
+
* Fields with defaults or nullable are optional.
|
|
2102
|
+
*/
|
|
2103
|
+
declare function entityCreateInputSchema(def: EntityDefinition): EntitySchemaObject;
|
|
2104
|
+
/**
|
|
2105
|
+
* Generates a JSON Schema for an entity's update input (PATCH).
|
|
2106
|
+
* Same exclusions as create, but all fields are optional.
|
|
2107
|
+
*/
|
|
2108
|
+
declare function entityUpdateInputSchema(def: EntityDefinition): EntitySchemaObject;
|
|
2109
|
+
interface OpenAPISpecOptions {
|
|
2110
|
+
info: {
|
|
2111
|
+
title: string;
|
|
2112
|
+
version: string;
|
|
2113
|
+
description?: string;
|
|
2114
|
+
};
|
|
2115
|
+
servers?: {
|
|
2116
|
+
url: string;
|
|
2117
|
+
description?: string;
|
|
2118
|
+
}[];
|
|
2119
|
+
/** API path prefix. Defaults to '/api'. */
|
|
2120
|
+
apiPrefix?: string;
|
|
2121
|
+
}
|
|
2122
|
+
interface OpenAPIOperation {
|
|
2123
|
+
operationId: string;
|
|
2124
|
+
tags: string[];
|
|
2125
|
+
summary: string;
|
|
2126
|
+
parameters?: OpenAPIParameter[];
|
|
2127
|
+
requestBody?: {
|
|
2128
|
+
required: boolean;
|
|
2129
|
+
content: {
|
|
2130
|
+
"application/json": {
|
|
2131
|
+
schema: JSONSchemaObject;
|
|
2132
|
+
};
|
|
2133
|
+
};
|
|
2134
|
+
};
|
|
2135
|
+
responses: Record<string, OpenAPIResponse>;
|
|
2136
|
+
}
|
|
2137
|
+
interface OpenAPIParameter {
|
|
2138
|
+
name: string;
|
|
2139
|
+
in: "path" | "query";
|
|
2140
|
+
required: boolean;
|
|
2141
|
+
schema: JSONSchemaObject;
|
|
2142
|
+
description?: string;
|
|
2143
|
+
}
|
|
2144
|
+
interface OpenAPIResponse {
|
|
2145
|
+
description: string;
|
|
2146
|
+
content?: {
|
|
2147
|
+
"application/json": {
|
|
2148
|
+
schema: JSONSchemaObject | EntitySchemaObject;
|
|
2149
|
+
};
|
|
2150
|
+
};
|
|
2151
|
+
$ref?: string;
|
|
2152
|
+
}
|
|
2153
|
+
interface OpenAPIPathItem {
|
|
2154
|
+
get?: OpenAPIOperation;
|
|
2155
|
+
post?: OpenAPIOperation;
|
|
2156
|
+
patch?: OpenAPIOperation;
|
|
2157
|
+
delete?: OpenAPIOperation;
|
|
2158
|
+
}
|
|
2159
|
+
interface OpenAPISpec {
|
|
2160
|
+
openapi: "3.1.0";
|
|
2161
|
+
info: {
|
|
2162
|
+
title: string;
|
|
2163
|
+
version: string;
|
|
2164
|
+
description?: string;
|
|
2165
|
+
};
|
|
2166
|
+
servers?: {
|
|
2167
|
+
url: string;
|
|
2168
|
+
description?: string;
|
|
2169
|
+
}[];
|
|
2170
|
+
paths: Record<string, OpenAPIPathItem>;
|
|
2171
|
+
components?: {
|
|
2172
|
+
schemas?: Record<string, EntitySchemaObject>;
|
|
2173
|
+
responses?: Record<string, OpenAPIResponse>;
|
|
2174
|
+
};
|
|
2175
|
+
tags?: {
|
|
2176
|
+
name: string;
|
|
2177
|
+
}[];
|
|
2178
|
+
}
|
|
2179
|
+
/**
|
|
2180
|
+
* Generates a full OpenAPI 3.1 specification from entity definitions.
|
|
2181
|
+
*/
|
|
2182
|
+
declare function generateOpenAPISpec(entities: EntityDefinition[], options: OpenAPISpecOptions): OpenAPISpec;
|
|
2066
2183
|
import { EntityRouteEntry } from "@vertz/core";
|
|
2067
2184
|
interface EntityRouteOptions {
|
|
2068
2185
|
apiPrefix?: string;
|
|
@@ -2083,4 +2200,4 @@ declare function service<
|
|
|
2083
2200
|
TInject extends Record<string, EntityDefinition> = {},
|
|
2084
2201
|
TActions extends Record<string, ServiceActionDef<any, any, any>> = Record<string, ServiceActionDef<any, any, any>>
|
|
2085
2202
|
>(name: string, config: ServiceConfig<TActions, TInject>): ServiceDefinition;
|
|
2086
|
-
export { vertz, verifyPassword, validatePassword, validateOverrides, validateAuthModels, stripReadOnlyFields, stripHiddenFields, service, rules, resolveTenantChain, makeImmutable, isContentDescriptor, initializeAuthTables, hashPassword, google, github, getIncompatibleAddOns, generateEntityRoutes, entityErrorHandler, entity, enforceAccess, encodeAccessSet, discord, defineAccess, defaultAccess, deepFreeze, decodeAccessSet, createWebhookHandler, createStripeBillingAdapter, createServer, createPlanManager, createMiddleware, createImmutableProxy, createEnv, createEntityContext, createCrudHandlers, createBillingEventEmitter, createAuth, createAccessEventBroadcaster, createAccessContext, createAccess, content, computePlanHash, computeOverage, computeEntityAccess, computeAccessSet, checkFva, checkAddOnCompatibility, calculateBillingPeriod, authModels, WebhookHandlerConfig, WalletStore, WalletEntry, VertzException, ValidationException, UserTableEntry, UserStore, UnauthorizedException, TenantOverrides, TenantChainHop, TenantChain, SubscriptionStore, Subscription, StripeProduct, StripePrice, StripeClient, StripeBillingAdapterConfig, StoredSession, StoredPasswordReset, StoredEmailVerification, SignUpInput, SignInInput, SessionStrategy, SessionStore, SessionPayload, SessionInfo, SessionConfig, Session, ServiceUnavailableException, ServiceRequestInfo, ServiceDefinition, ServiceContext, ServiceConfig, ServiceActionDef, ServerInstance, ServerHandle, ServerConfig, ServerAdapter, RuleContext, RoleAssignmentTableEntry, RoleAssignmentStore, RoleAssignment, ResourceRef, Resource, RequestInfo, RelationExposeConfig, RawRequest, RateLimitStore, RateLimitResult, RateLimitConfig, PriceInterval, PlanVersionStore, PlanVersionInfo, PlanSnapshot, PlanPrice, PlanHashInput, PlanDef, Period, PasswordResetStore, PasswordResetConfig, PasswordRequirements, ParentRef, OverrideStore, OverageInput, OverageConfig, OnUserCreatedPayload, OAuthUserInfo, OAuthTokens, OAuthProviderConfig, OAuthProvider, OAuthAccountStore, NotFoundException, NamedMiddlewareDef, MiddlewareDef, MfaSetupData, MfaConfig, MfaChallengeData, MFAStore, ListenOptions, ListResult, ListOptions2 as ListOptions, LimitOverrideDef, LimitOverride, LimitDef, InternalServerErrorException, InferSchema, Infer2 as Infer, InMemoryWalletStore, InMemoryUserStore, InMemorySubscriptionStore, InMemorySessionStore, InMemoryRoleAssignmentStore, InMemoryRateLimitStore, InMemoryPlanVersionStore, InMemoryPasswordResetStore, InMemoryOverrideStore, InMemoryOAuthAccountStore, InMemoryMFAStore, InMemoryGrandfatheringStore, InMemoryFlagStore, InMemoryEmailVerificationStore, InMemoryClosureStore, HttpStatusCode, HttpMethod, HandlerCtx, GrandfatheringStore, GrandfatheringState, ForbiddenException, FlagStore, ExposeConfig, EnvConfig, EntityRouteOptions, EntityRelationsConfig, EntityRegistry, EntityOperations, EntityErrorResult, EntityDefinition, EntityDef, EntityDbAdapter2 as EntityDbAdapter, EntityContext, EntityConfig, EntityActionDef, EntitlementValue, EntitlementRegistry, EntitlementDefinition, EntitlementDef, Entitlement, EncodedAccessSet, EmailVerificationStore, EmailVerificationConfig, EmailPasswordConfig, Deps, DenialReason, DenialMeta, DefineAccessInput, DeepReadonly, DbUserStore, DbSubscriptionStore, DbSessionStore, DbRoleAssignmentStore, DbOAuthAccountStore, DbFlagStore, DbDialectName, DbClosureStore, Ctx, CrudResult, CrudHandlers, CorsConfig, CookieConfig, ContentDescriptor, ConsumeResult, ConflictException, ComputeAccessSetConfig, ClosureStore, ClosureRow, ClosureEntry, BillingPeriod, BillingEventType, BillingEventHandler, BillingEventEmitter, BillingEvent, BillingAdapter, BaseContext, BadRequestException, AuthorizationError, AuthUser, AuthTokens, AuthInstance, AuthEntityProxy, AuthDbClient, AuthContext, AuthConfig, AuthCallbackContext, AuthApi, AppConfig2 as AppConfig, AppBuilder2 as AppBuilder, AddOnRequires, AclClaim, AccumulateProvides, AccessWsData, AccessSet, AccessRule2 as AccessRule, AccessInstance, AccessEventBroadcasterConfig, AccessEventBroadcaster, AccessEvent, AccessDefinition, AccessContextConfig, AccessContext, AccessConfig, AccessCheckResult, AccessCheckData, AUTH_TABLE_NAMES };
|
|
2203
|
+
export { vertz, verifyPassword, validatePassword, validateOverrides, validateAuthModels, stripReadOnlyFields, stripHiddenFields, service, rules, resolveTenantChain, makeImmutable, isContentDescriptor, initializeAuthTables, hashPassword, google, github, getIncompatibleAddOns, generateOpenAPISpec, generateEntityRoutes, entityUpdateInputSchema, entityResponseSchema, entityErrorHandler, entityCreateInputSchema, entity, enforceAccess, encodeAccessSet, discord, defineAccess, defaultAccess, deepFreeze, decodeAccessSet, createWebhookHandler, createStripeBillingAdapter, createServer, createPlanManager, createMiddleware, createImmutableProxy, createEnv, createEntityContext, createCrudHandlers, createBillingEventEmitter, createAuth, createAccessEventBroadcaster, createAccessContext, createAccess, content, computePlanHash, computeOverage, computeEntityAccess, computeAccessSet, columnToJsonSchema, checkFva, checkAddOnCompatibility, calculateBillingPeriod, authModels, WebhookHandlerConfig, WalletStore, WalletEntry, VertzException, ValidationException, UserTableEntry, UserStore, UnauthorizedException, TenantOverrides, TenantChainHop, TenantChain, SubscriptionStore, Subscription, StripeProduct, StripePrice, StripeClient, StripeBillingAdapterConfig, StoredSession, StoredPasswordReset, StoredEmailVerification, SignUpInput, SignInInput, SessionStrategy, SessionStore, SessionPayload, SessionInfo, SessionConfig, Session, ServiceUnavailableException, ServiceRequestInfo, ServiceDefinition, ServiceContext, ServiceConfig, ServiceActionDef, ServerInstance, ServerHandle, ServerConfig, ServerAdapter, RuleContext, RoleAssignmentTableEntry, RoleAssignmentStore, RoleAssignment, ResourceRef, Resource, RequestInfo, RelationExposeConfig, RawRequest, RateLimitStore, RateLimitResult, RateLimitConfig, PriceInterval, PlanVersionStore, PlanVersionInfo, PlanSnapshot, PlanPrice, PlanHashInput, PlanDef, Period, PasswordResetStore, PasswordResetConfig, PasswordRequirements, ParentRef, OverrideStore, OverageInput, OverageConfig, OpenAPISpecOptions, OnUserCreatedPayload, OAuthUserInfo, OAuthTokens, OAuthProviderConfig, OAuthProvider, OAuthAccountStore, NotFoundException, NamedMiddlewareDef, MiddlewareDef, MfaSetupData, MfaConfig, MfaChallengeData, MFAStore, ListenOptions, ListResult, ListOptions2 as ListOptions, LimitOverrideDef, LimitOverride, LimitDef, JSONSchemaObject, InternalServerErrorException, InferSchema, Infer2 as Infer, InMemoryWalletStore, InMemoryUserStore, InMemorySubscriptionStore, InMemorySessionStore, InMemoryRoleAssignmentStore, InMemoryRateLimitStore, InMemoryPlanVersionStore, InMemoryPasswordResetStore, InMemoryOverrideStore, InMemoryOAuthAccountStore, InMemoryMFAStore, InMemoryGrandfatheringStore, InMemoryFlagStore, InMemoryEmailVerificationStore, InMemoryClosureStore, HttpStatusCode, HttpMethod, HandlerCtx, GrandfatheringStore, GrandfatheringState, ForbiddenException, FlagStore, ExposeConfig, EnvConfig, EntitySchemaObject, EntityRouteOptions, EntityRelationsConfig, EntityRegistry, EntityOperations, EntityErrorResult, EntityDefinition, EntityDef, EntityDbAdapter2 as EntityDbAdapter, EntityContext, EntityConfig, EntityActionDef, EntitlementValue, EntitlementRegistry, EntitlementDefinition, EntitlementDef, Entitlement, EncodedAccessSet, EmailVerificationStore, EmailVerificationConfig, EmailPasswordConfig, Deps, DenialReason, DenialMeta, DefineAccessInput, DeepReadonly, DbUserStore, DbSubscriptionStore, DbSessionStore, DbRoleAssignmentStore, DbOAuthAccountStore, DbFlagStore, DbDialectName, DbClosureStore, Ctx, CrudResult, CrudHandlers, CorsConfig, CookieConfig, ContentDescriptor, ConsumeResult, ConflictException, ComputeAccessSetConfig, ClosureStore, ClosureRow, ClosureEntry, BillingPeriod, BillingEventType, BillingEventHandler, BillingEventEmitter, BillingEvent, BillingAdapter, BaseContext, BadRequestException, AuthorizationError, AuthUser, AuthTokens, AuthInstance, AuthEntityProxy, AuthDbClient, AuthContext, AuthConfig, AuthCallbackContext, AuthApi, AppConfig2 as AppConfig, AppBuilder2 as AppBuilder, AddOnRequires, AclClaim, AccumulateProvides, AccessWsData, AccessSet, AccessRule2 as AccessRule, AccessInstance, AccessEventBroadcasterConfig, AccessEventBroadcaster, AccessEvent, AccessDefinition, AccessContextConfig, AccessContext, AccessConfig, AccessCheckResult, AccessCheckData, AUTH_TABLE_NAMES };
|
package/dist/index.js
CHANGED
|
@@ -7660,6 +7660,549 @@ function entity(name, config) {
|
|
|
7660
7660
|
};
|
|
7661
7661
|
return deepFreeze(def);
|
|
7662
7662
|
}
|
|
7663
|
+
// src/entity/openapi-generator.ts
|
|
7664
|
+
function columnToJsonSchema(column) {
|
|
7665
|
+
const meta = column._meta;
|
|
7666
|
+
const schema = sqlTypeToJsonSchema(meta);
|
|
7667
|
+
if (meta.nullable) {
|
|
7668
|
+
const baseType = schema.type;
|
|
7669
|
+
if (baseType) {
|
|
7670
|
+
schema.type = [baseType, "null"];
|
|
7671
|
+
} else {
|
|
7672
|
+
return { oneOf: [schema, { type: "null" }] };
|
|
7673
|
+
}
|
|
7674
|
+
}
|
|
7675
|
+
return schema;
|
|
7676
|
+
}
|
|
7677
|
+
function sqlTypeToJsonSchema(meta) {
|
|
7678
|
+
if (meta.format === "email") {
|
|
7679
|
+
return { type: "string", format: "email" };
|
|
7680
|
+
}
|
|
7681
|
+
switch (meta.sqlType) {
|
|
7682
|
+
case "uuid":
|
|
7683
|
+
return { type: "string", format: "uuid" };
|
|
7684
|
+
case "text":
|
|
7685
|
+
return { type: "string" };
|
|
7686
|
+
case "varchar": {
|
|
7687
|
+
const schema = { type: "string" };
|
|
7688
|
+
if (meta.length !== undefined)
|
|
7689
|
+
schema.maxLength = meta.length;
|
|
7690
|
+
return schema;
|
|
7691
|
+
}
|
|
7692
|
+
case "boolean":
|
|
7693
|
+
return { type: "boolean" };
|
|
7694
|
+
case "integer":
|
|
7695
|
+
case "serial":
|
|
7696
|
+
return { type: "integer" };
|
|
7697
|
+
case "bigint":
|
|
7698
|
+
case "decimal":
|
|
7699
|
+
return { type: "string" };
|
|
7700
|
+
case "real":
|
|
7701
|
+
return { type: "number" };
|
|
7702
|
+
case "double precision":
|
|
7703
|
+
return { type: "number", format: "double" };
|
|
7704
|
+
case "timestamp with time zone":
|
|
7705
|
+
return { type: "string", format: "date-time" };
|
|
7706
|
+
case "date":
|
|
7707
|
+
return { type: "string", format: "date" };
|
|
7708
|
+
case "time":
|
|
7709
|
+
return { type: "string", format: "time" };
|
|
7710
|
+
case "jsonb":
|
|
7711
|
+
return {};
|
|
7712
|
+
case "text[]":
|
|
7713
|
+
return { type: "array", items: { type: "string" } };
|
|
7714
|
+
case "integer[]":
|
|
7715
|
+
return { type: "array", items: { type: "integer" } };
|
|
7716
|
+
case "enum":
|
|
7717
|
+
return { type: "string", enum: meta.enumValues };
|
|
7718
|
+
default:
|
|
7719
|
+
return {};
|
|
7720
|
+
}
|
|
7721
|
+
}
|
|
7722
|
+
function toPascalCase(name) {
|
|
7723
|
+
return name.split("-").map((s2) => s2.charAt(0).toUpperCase() + s2.slice(1)).join("");
|
|
7724
|
+
}
|
|
7725
|
+
function buildColumnsSchema(columns, selectFilter) {
|
|
7726
|
+
const properties = {};
|
|
7727
|
+
const required = [];
|
|
7728
|
+
for (const [name, col] of Object.entries(columns)) {
|
|
7729
|
+
const meta = col._meta;
|
|
7730
|
+
if (meta._annotations.hidden)
|
|
7731
|
+
continue;
|
|
7732
|
+
if (selectFilter && !(name in selectFilter))
|
|
7733
|
+
continue;
|
|
7734
|
+
const isDescriptorGuarded = selectFilter && selectFilter[name] !== true;
|
|
7735
|
+
let schema = columnToJsonSchema(col);
|
|
7736
|
+
if (isDescriptorGuarded) {
|
|
7737
|
+
const baseType = schema.type;
|
|
7738
|
+
if (baseType) {
|
|
7739
|
+
const typeArray = Array.isArray(baseType) ? baseType : [baseType];
|
|
7740
|
+
if (!typeArray.includes("null")) {
|
|
7741
|
+
schema = { ...schema, type: [...typeArray, "null"] };
|
|
7742
|
+
}
|
|
7743
|
+
}
|
|
7744
|
+
const descriptor = selectFilter?.[name];
|
|
7745
|
+
const entitlementName = descriptor.entitlement ?? descriptor.type ?? "access rule";
|
|
7746
|
+
schema.description = `Requires entitlement '${entitlementName}'. Returns null when the caller lacks the entitlement.`;
|
|
7747
|
+
}
|
|
7748
|
+
properties[name] = schema;
|
|
7749
|
+
if (!meta.nullable && !isDescriptorGuarded) {
|
|
7750
|
+
required.push(name);
|
|
7751
|
+
}
|
|
7752
|
+
}
|
|
7753
|
+
const result = { type: "object", properties };
|
|
7754
|
+
if (required.length > 0)
|
|
7755
|
+
result.required = required;
|
|
7756
|
+
return result;
|
|
7757
|
+
}
|
|
7758
|
+
function entityResponseSchema(def, relationSchemas) {
|
|
7759
|
+
const table = def.model.table;
|
|
7760
|
+
const columns = table._columns;
|
|
7761
|
+
const exposeSelect = def.expose?.select;
|
|
7762
|
+
const schema = buildColumnsSchema(columns, exposeSelect);
|
|
7763
|
+
if (def.expose?.include && relationSchemas) {
|
|
7764
|
+
const relations = def.model.relations;
|
|
7765
|
+
const includeConfig = def.expose.include;
|
|
7766
|
+
const entityPrefix = toPascalCase(def.name);
|
|
7767
|
+
for (const [relationName, config] of Object.entries(includeConfig)) {
|
|
7768
|
+
if (config === false)
|
|
7769
|
+
continue;
|
|
7770
|
+
const relation = relations[relationName];
|
|
7771
|
+
if (!relation)
|
|
7772
|
+
continue;
|
|
7773
|
+
const targetTable = relation._target();
|
|
7774
|
+
const targetColumns = targetTable._columns;
|
|
7775
|
+
const relationSchemaName = `${entityPrefix}${toPascalCase(relationName)}Response`;
|
|
7776
|
+
if (config === true) {
|
|
7777
|
+
relationSchemas[relationSchemaName] = buildColumnsSchema(targetColumns);
|
|
7778
|
+
} else {
|
|
7779
|
+
const relSelect = config.select;
|
|
7780
|
+
relationSchemas[relationSchemaName] = buildColumnsSchema(targetColumns, relSelect);
|
|
7781
|
+
}
|
|
7782
|
+
if (schema.properties) {
|
|
7783
|
+
const isMany = relation._type === "many";
|
|
7784
|
+
if (isMany) {
|
|
7785
|
+
schema.properties[relationName] = {
|
|
7786
|
+
type: "array",
|
|
7787
|
+
items: { $ref: `#/components/schemas/${relationSchemaName}` }
|
|
7788
|
+
};
|
|
7789
|
+
} else {
|
|
7790
|
+
schema.properties[relationName] = {
|
|
7791
|
+
$ref: `#/components/schemas/${relationSchemaName}`
|
|
7792
|
+
};
|
|
7793
|
+
}
|
|
7794
|
+
}
|
|
7795
|
+
}
|
|
7796
|
+
}
|
|
7797
|
+
return schema;
|
|
7798
|
+
}
|
|
7799
|
+
function buildInputColumnsSchema(columns, allOptional) {
|
|
7800
|
+
const properties = {};
|
|
7801
|
+
const required = [];
|
|
7802
|
+
for (const [name, col] of Object.entries(columns)) {
|
|
7803
|
+
const meta = col._meta;
|
|
7804
|
+
if (meta.primary)
|
|
7805
|
+
continue;
|
|
7806
|
+
if (meta.isReadOnly)
|
|
7807
|
+
continue;
|
|
7808
|
+
if (meta.isAutoUpdate)
|
|
7809
|
+
continue;
|
|
7810
|
+
if (meta._annotations.hidden)
|
|
7811
|
+
continue;
|
|
7812
|
+
properties[name] = columnToJsonSchema(col);
|
|
7813
|
+
if (!allOptional && !meta.nullable && !meta.hasDefault) {
|
|
7814
|
+
required.push(name);
|
|
7815
|
+
}
|
|
7816
|
+
}
|
|
7817
|
+
const result = { type: "object", properties };
|
|
7818
|
+
if (required.length > 0)
|
|
7819
|
+
result.required = required;
|
|
7820
|
+
return result;
|
|
7821
|
+
}
|
|
7822
|
+
function entityCreateInputSchema(def) {
|
|
7823
|
+
const table = def.model.table;
|
|
7824
|
+
const columns = table._columns;
|
|
7825
|
+
return buildInputColumnsSchema(columns, false);
|
|
7826
|
+
}
|
|
7827
|
+
function entityUpdateInputSchema(def) {
|
|
7828
|
+
const table = def.model.table;
|
|
7829
|
+
const columns = table._columns;
|
|
7830
|
+
return buildInputColumnsSchema(columns, true);
|
|
7831
|
+
}
|
|
7832
|
+
var ERROR_RESPONSE_SCHEMA = {
|
|
7833
|
+
type: "object",
|
|
7834
|
+
required: ["error"],
|
|
7835
|
+
properties: {
|
|
7836
|
+
error: {
|
|
7837
|
+
type: "object",
|
|
7838
|
+
required: ["code", "message"],
|
|
7839
|
+
properties: {
|
|
7840
|
+
code: { type: "string" },
|
|
7841
|
+
message: { type: "string" }
|
|
7842
|
+
}
|
|
7843
|
+
}
|
|
7844
|
+
}
|
|
7845
|
+
};
|
|
7846
|
+
var STANDARD_RESPONSES = {
|
|
7847
|
+
BadRequest: {
|
|
7848
|
+
description: "Bad Request",
|
|
7849
|
+
content: {
|
|
7850
|
+
"application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } }
|
|
7851
|
+
}
|
|
7852
|
+
},
|
|
7853
|
+
Unauthorized: {
|
|
7854
|
+
description: "Unauthorized",
|
|
7855
|
+
content: {
|
|
7856
|
+
"application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } }
|
|
7857
|
+
}
|
|
7858
|
+
},
|
|
7859
|
+
NotFound: {
|
|
7860
|
+
description: "Not Found",
|
|
7861
|
+
content: {
|
|
7862
|
+
"application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } }
|
|
7863
|
+
}
|
|
7864
|
+
}
|
|
7865
|
+
};
|
|
7866
|
+
function errorRefs(...codes) {
|
|
7867
|
+
const result = {};
|
|
7868
|
+
for (const code of codes) {
|
|
7869
|
+
const refName = code === "400" ? "BadRequest" : code === "401" ? "Unauthorized" : "NotFound";
|
|
7870
|
+
result[code] = { $ref: `#/components/responses/${refName}` };
|
|
7871
|
+
}
|
|
7872
|
+
return result;
|
|
7873
|
+
}
|
|
7874
|
+
function extractJsonSchema(schema, entityName, actionName, field) {
|
|
7875
|
+
if (schema && typeof schema === "object" && "toJSONSchema" in schema && typeof schema.toJSONSchema === "function") {
|
|
7876
|
+
return schema.toJSONSchema();
|
|
7877
|
+
}
|
|
7878
|
+
console.warn(`[vertz] Warning: Action "${entityName}.${actionName}" ${field} schema does not expose JSON schema — using "any" in OpenAPI spec.`);
|
|
7879
|
+
return { description: "Schema not available for automated extraction." };
|
|
7880
|
+
}
|
|
7881
|
+
function generateOpenAPISpec(entities, options) {
|
|
7882
|
+
const apiPrefix = options.apiPrefix ?? "/api";
|
|
7883
|
+
const paths = {};
|
|
7884
|
+
const schemas = {
|
|
7885
|
+
ErrorResponse: ERROR_RESPONSE_SCHEMA
|
|
7886
|
+
};
|
|
7887
|
+
const tags = [];
|
|
7888
|
+
for (const def of entities) {
|
|
7889
|
+
const prefix = toPascalCase(def.name);
|
|
7890
|
+
const basePath = `${apiPrefix}/${def.name}`;
|
|
7891
|
+
const tag = def.name;
|
|
7892
|
+
tags.push({ name: tag });
|
|
7893
|
+
const relationSchemas = {};
|
|
7894
|
+
schemas[`${prefix}Response`] = entityResponseSchema(def, relationSchemas);
|
|
7895
|
+
Object.assign(schemas, relationSchemas);
|
|
7896
|
+
if (def.access.create !== undefined) {
|
|
7897
|
+
schemas[`${prefix}CreateInput`] = entityCreateInputSchema(def);
|
|
7898
|
+
}
|
|
7899
|
+
if (def.access.update !== undefined) {
|
|
7900
|
+
schemas[`${prefix}UpdateInput`] = entityUpdateInputSchema(def);
|
|
7901
|
+
}
|
|
7902
|
+
const collectionPath = {};
|
|
7903
|
+
const itemPath = {};
|
|
7904
|
+
if (def.access.list !== undefined && def.access.list !== false) {
|
|
7905
|
+
collectionPath.get = buildListOperation(def, prefix, tag);
|
|
7906
|
+
}
|
|
7907
|
+
if (def.access.create !== undefined && def.access.create !== false) {
|
|
7908
|
+
collectionPath.post = buildCreateOperation(prefix, tag);
|
|
7909
|
+
}
|
|
7910
|
+
if (def.access.get !== undefined && def.access.get !== false) {
|
|
7911
|
+
itemPath.get = buildGetOperation(prefix, tag);
|
|
7912
|
+
}
|
|
7913
|
+
if (def.access.update !== undefined && def.access.update !== false) {
|
|
7914
|
+
itemPath.patch = buildUpdateOperation(prefix, tag);
|
|
7915
|
+
}
|
|
7916
|
+
if (def.access.delete === false) {
|
|
7917
|
+
itemPath.delete = buildDisabledOperation(def.name, "delete", tag);
|
|
7918
|
+
} else if (def.access.delete !== undefined) {
|
|
7919
|
+
itemPath.delete = buildDeleteOperation(def.name, tag);
|
|
7920
|
+
}
|
|
7921
|
+
if (Object.keys(collectionPath).length > 0) {
|
|
7922
|
+
paths[basePath] = collectionPath;
|
|
7923
|
+
}
|
|
7924
|
+
if (Object.keys(itemPath).length > 0) {
|
|
7925
|
+
paths[`${basePath}/{id}`] = itemPath;
|
|
7926
|
+
}
|
|
7927
|
+
if (def.access.list !== undefined && def.access.list !== false) {
|
|
7928
|
+
paths[`${basePath}/query`] = {
|
|
7929
|
+
post: buildQueryOperation(def.name, prefix, tag)
|
|
7930
|
+
};
|
|
7931
|
+
}
|
|
7932
|
+
if (def.actions) {
|
|
7933
|
+
for (const [actionName, actionDef] of Object.entries(def.actions)) {
|
|
7934
|
+
const method = (actionDef.method ?? "POST").toUpperCase();
|
|
7935
|
+
const actionPath = actionDef.path ?? actionName;
|
|
7936
|
+
const fullPath = `${basePath}/{id}/${actionPath}`;
|
|
7937
|
+
const operation = buildActionOperation(def.name, actionName, actionDef, tag);
|
|
7938
|
+
const pathItem = {};
|
|
7939
|
+
if (method === "POST") {
|
|
7940
|
+
pathItem.post = operation;
|
|
7941
|
+
} else if (method === "PATCH") {
|
|
7942
|
+
pathItem.patch = operation;
|
|
7943
|
+
} else if (method === "GET") {
|
|
7944
|
+
pathItem.get = operation;
|
|
7945
|
+
} else if (method === "DELETE") {
|
|
7946
|
+
pathItem.delete = operation;
|
|
7947
|
+
}
|
|
7948
|
+
paths[fullPath] = pathItem;
|
|
7949
|
+
}
|
|
7950
|
+
}
|
|
7951
|
+
}
|
|
7952
|
+
const spec = {
|
|
7953
|
+
openapi: "3.1.0",
|
|
7954
|
+
info: options.info,
|
|
7955
|
+
paths,
|
|
7956
|
+
components: {
|
|
7957
|
+
schemas,
|
|
7958
|
+
responses: STANDARD_RESPONSES
|
|
7959
|
+
},
|
|
7960
|
+
tags
|
|
7961
|
+
};
|
|
7962
|
+
if (options.servers) {
|
|
7963
|
+
spec.servers = options.servers;
|
|
7964
|
+
}
|
|
7965
|
+
return spec;
|
|
7966
|
+
}
|
|
7967
|
+
function buildListOperation(def, prefix, tag) {
|
|
7968
|
+
const parameters = [];
|
|
7969
|
+
const table = def.model.table;
|
|
7970
|
+
const columns = table._columns;
|
|
7971
|
+
const expose = def.expose;
|
|
7972
|
+
if (expose?.allowWhere) {
|
|
7973
|
+
const allowWhere = expose.allowWhere;
|
|
7974
|
+
for (const field of Object.keys(allowWhere)) {
|
|
7975
|
+
const col = columns[field];
|
|
7976
|
+
if (!col)
|
|
7977
|
+
continue;
|
|
7978
|
+
const schema = columnToJsonSchema(col);
|
|
7979
|
+
parameters.push({
|
|
7980
|
+
name: `where[${field}]`,
|
|
7981
|
+
in: "query",
|
|
7982
|
+
required: false,
|
|
7983
|
+
schema
|
|
7984
|
+
});
|
|
7985
|
+
}
|
|
7986
|
+
}
|
|
7987
|
+
if (expose?.allowOrderBy) {
|
|
7988
|
+
const allowedFields = Object.keys(expose.allowOrderBy);
|
|
7989
|
+
parameters.push({
|
|
7990
|
+
name: "orderBy",
|
|
7991
|
+
in: "query",
|
|
7992
|
+
required: false,
|
|
7993
|
+
schema: {
|
|
7994
|
+
type: "string",
|
|
7995
|
+
enum: allowedFields.flatMap((f) => [`${f}:asc`, `${f}:desc`])
|
|
7996
|
+
},
|
|
7997
|
+
description: "Sort order. Format: field:direction"
|
|
7998
|
+
});
|
|
7999
|
+
}
|
|
8000
|
+
parameters.push({ name: "limit", in: "query", required: false, schema: { type: "integer" } }, { name: "after", in: "query", required: false, schema: { type: "string" } }, {
|
|
8001
|
+
name: "q",
|
|
8002
|
+
in: "query",
|
|
8003
|
+
required: false,
|
|
8004
|
+
schema: { type: "string" },
|
|
8005
|
+
description: "Base64-encoded VertzQL query"
|
|
8006
|
+
});
|
|
8007
|
+
return {
|
|
8008
|
+
operationId: `${def.name}_list`,
|
|
8009
|
+
tags: [tag],
|
|
8010
|
+
summary: `List ${def.name}`,
|
|
8011
|
+
parameters,
|
|
8012
|
+
responses: {
|
|
8013
|
+
"200": {
|
|
8014
|
+
description: "OK",
|
|
8015
|
+
content: {
|
|
8016
|
+
"application/json": {
|
|
8017
|
+
schema: {
|
|
8018
|
+
type: "object",
|
|
8019
|
+
properties: {
|
|
8020
|
+
items: {
|
|
8021
|
+
type: "array",
|
|
8022
|
+
items: { $ref: `#/components/schemas/${prefix}Response` }
|
|
8023
|
+
},
|
|
8024
|
+
cursor: { type: "string" }
|
|
8025
|
+
}
|
|
8026
|
+
}
|
|
8027
|
+
}
|
|
8028
|
+
}
|
|
8029
|
+
},
|
|
8030
|
+
...errorRefs("400", "401")
|
|
8031
|
+
}
|
|
8032
|
+
};
|
|
8033
|
+
}
|
|
8034
|
+
function buildCreateOperation(prefix, tag) {
|
|
8035
|
+
return {
|
|
8036
|
+
operationId: `${tag}_create`,
|
|
8037
|
+
tags: [tag],
|
|
8038
|
+
summary: `Create a ${tag}`,
|
|
8039
|
+
requestBody: {
|
|
8040
|
+
required: true,
|
|
8041
|
+
content: {
|
|
8042
|
+
"application/json": {
|
|
8043
|
+
schema: { $ref: `#/components/schemas/${prefix}CreateInput` }
|
|
8044
|
+
}
|
|
8045
|
+
}
|
|
8046
|
+
},
|
|
8047
|
+
responses: {
|
|
8048
|
+
"201": {
|
|
8049
|
+
description: "Created",
|
|
8050
|
+
content: {
|
|
8051
|
+
"application/json": {
|
|
8052
|
+
schema: { $ref: `#/components/schemas/${prefix}Response` }
|
|
8053
|
+
}
|
|
8054
|
+
}
|
|
8055
|
+
},
|
|
8056
|
+
...errorRefs("400", "401")
|
|
8057
|
+
}
|
|
8058
|
+
};
|
|
8059
|
+
}
|
|
8060
|
+
function buildGetOperation(prefix, tag) {
|
|
8061
|
+
return {
|
|
8062
|
+
operationId: `${tag}_get`,
|
|
8063
|
+
tags: [tag],
|
|
8064
|
+
summary: `Get a ${tag} by ID`,
|
|
8065
|
+
parameters: [
|
|
8066
|
+
{ name: "id", in: "path", required: true, schema: { type: "string", format: "uuid" } }
|
|
8067
|
+
],
|
|
8068
|
+
responses: {
|
|
8069
|
+
"200": {
|
|
8070
|
+
description: "OK",
|
|
8071
|
+
content: {
|
|
8072
|
+
"application/json": {
|
|
8073
|
+
schema: { $ref: `#/components/schemas/${prefix}Response` }
|
|
8074
|
+
}
|
|
8075
|
+
}
|
|
8076
|
+
},
|
|
8077
|
+
...errorRefs("401", "404")
|
|
8078
|
+
}
|
|
8079
|
+
};
|
|
8080
|
+
}
|
|
8081
|
+
function buildUpdateOperation(prefix, tag) {
|
|
8082
|
+
return {
|
|
8083
|
+
operationId: `${tag}_update`,
|
|
8084
|
+
tags: [tag],
|
|
8085
|
+
summary: `Update a ${tag}`,
|
|
8086
|
+
parameters: [
|
|
8087
|
+
{ name: "id", in: "path", required: true, schema: { type: "string", format: "uuid" } }
|
|
8088
|
+
],
|
|
8089
|
+
requestBody: {
|
|
8090
|
+
required: true,
|
|
8091
|
+
content: {
|
|
8092
|
+
"application/json": {
|
|
8093
|
+
schema: { $ref: `#/components/schemas/${prefix}UpdateInput` }
|
|
8094
|
+
}
|
|
8095
|
+
}
|
|
8096
|
+
},
|
|
8097
|
+
responses: {
|
|
8098
|
+
"200": {
|
|
8099
|
+
description: "OK",
|
|
8100
|
+
content: {
|
|
8101
|
+
"application/json": {
|
|
8102
|
+
schema: { $ref: `#/components/schemas/${prefix}Response` }
|
|
8103
|
+
}
|
|
8104
|
+
}
|
|
8105
|
+
},
|
|
8106
|
+
...errorRefs("400", "401", "404")
|
|
8107
|
+
}
|
|
8108
|
+
};
|
|
8109
|
+
}
|
|
8110
|
+
function buildDeleteOperation(entityName, tag) {
|
|
8111
|
+
return {
|
|
8112
|
+
operationId: `${entityName}_delete`,
|
|
8113
|
+
tags: [tag],
|
|
8114
|
+
summary: `Delete a ${entityName}`,
|
|
8115
|
+
parameters: [
|
|
8116
|
+
{ name: "id", in: "path", required: true, schema: { type: "string", format: "uuid" } }
|
|
8117
|
+
],
|
|
8118
|
+
responses: {
|
|
8119
|
+
"204": { description: "No Content" },
|
|
8120
|
+
...errorRefs("401", "404")
|
|
8121
|
+
}
|
|
8122
|
+
};
|
|
8123
|
+
}
|
|
8124
|
+
function buildDisabledOperation(entityName, operation, tag) {
|
|
8125
|
+
return {
|
|
8126
|
+
operationId: `${entityName}_${operation}`,
|
|
8127
|
+
tags: [tag],
|
|
8128
|
+
summary: `${operation} is disabled for ${entityName}`,
|
|
8129
|
+
parameters: [
|
|
8130
|
+
{ name: "id", in: "path", required: true, schema: { type: "string", format: "uuid" } }
|
|
8131
|
+
],
|
|
8132
|
+
responses: {
|
|
8133
|
+
"405": {
|
|
8134
|
+
description: `Method Not Allowed — operation "${operation}" is disabled for ${entityName}`
|
|
8135
|
+
}
|
|
8136
|
+
}
|
|
8137
|
+
};
|
|
8138
|
+
}
|
|
8139
|
+
function buildQueryOperation(entityName, prefix, tag) {
|
|
8140
|
+
return {
|
|
8141
|
+
operationId: `${entityName}_query`,
|
|
8142
|
+
tags: [tag],
|
|
8143
|
+
summary: `Query ${entityName} (structured query via POST body)`,
|
|
8144
|
+
requestBody: {
|
|
8145
|
+
required: true,
|
|
8146
|
+
content: {
|
|
8147
|
+
"application/json": {
|
|
8148
|
+
schema: { $ref: `#/components/schemas/${prefix}Query` }
|
|
8149
|
+
}
|
|
8150
|
+
}
|
|
8151
|
+
},
|
|
8152
|
+
responses: {
|
|
8153
|
+
"200": {
|
|
8154
|
+
description: "OK",
|
|
8155
|
+
content: {
|
|
8156
|
+
"application/json": {
|
|
8157
|
+
schema: {
|
|
8158
|
+
type: "object",
|
|
8159
|
+
properties: {
|
|
8160
|
+
items: {
|
|
8161
|
+
type: "array",
|
|
8162
|
+
items: { $ref: `#/components/schemas/${prefix}Response` }
|
|
8163
|
+
},
|
|
8164
|
+
cursor: { type: "string" }
|
|
8165
|
+
}
|
|
8166
|
+
}
|
|
8167
|
+
}
|
|
8168
|
+
}
|
|
8169
|
+
},
|
|
8170
|
+
...errorRefs("400", "401")
|
|
8171
|
+
}
|
|
8172
|
+
};
|
|
8173
|
+
}
|
|
8174
|
+
function buildActionOperation(entityName, actionName, actionDef, tag) {
|
|
8175
|
+
const operation = {
|
|
8176
|
+
operationId: `${entityName}_${actionName}`,
|
|
8177
|
+
tags: [tag],
|
|
8178
|
+
summary: `${actionName} action on ${entityName}`,
|
|
8179
|
+
parameters: [
|
|
8180
|
+
{ name: "id", in: "path", required: true, schema: { type: "string", format: "uuid" } }
|
|
8181
|
+
],
|
|
8182
|
+
responses: {
|
|
8183
|
+
"200": {
|
|
8184
|
+
description: "OK",
|
|
8185
|
+
content: {
|
|
8186
|
+
"application/json": {
|
|
8187
|
+
schema: extractJsonSchema(actionDef.response, entityName, actionName, "response")
|
|
8188
|
+
}
|
|
8189
|
+
}
|
|
8190
|
+
},
|
|
8191
|
+
...errorRefs("400", "401", "404")
|
|
8192
|
+
}
|
|
8193
|
+
};
|
|
8194
|
+
if (actionDef.body) {
|
|
8195
|
+
operation.requestBody = {
|
|
8196
|
+
required: true,
|
|
8197
|
+
content: {
|
|
8198
|
+
"application/json": {
|
|
8199
|
+
schema: extractJsonSchema(actionDef.body, entityName, actionName, "body")
|
|
8200
|
+
}
|
|
8201
|
+
}
|
|
8202
|
+
};
|
|
8203
|
+
}
|
|
8204
|
+
return operation;
|
|
8205
|
+
}
|
|
7663
8206
|
// src/service/service.ts
|
|
7664
8207
|
import { deepFreeze as deepFreeze2 } from "@vertz/core";
|
|
7665
8208
|
var SERVICE_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
@@ -7697,8 +8240,12 @@ export {
|
|
|
7697
8240
|
google,
|
|
7698
8241
|
github,
|
|
7699
8242
|
getIncompatibleAddOns,
|
|
8243
|
+
generateOpenAPISpec,
|
|
7700
8244
|
generateEntityRoutes,
|
|
8245
|
+
entityUpdateInputSchema,
|
|
8246
|
+
entityResponseSchema,
|
|
7701
8247
|
entityErrorHandler,
|
|
8248
|
+
entityCreateInputSchema,
|
|
7702
8249
|
entity,
|
|
7703
8250
|
enforceAccess,
|
|
7704
8251
|
encodeAccessSet,
|
|
@@ -7726,6 +8273,7 @@ export {
|
|
|
7726
8273
|
computeOverage,
|
|
7727
8274
|
computeEntityAccess,
|
|
7728
8275
|
computeAccessSet,
|
|
8276
|
+
columnToJsonSchema,
|
|
7729
8277
|
checkFva,
|
|
7730
8278
|
checkAddOnCompatibility,
|
|
7731
8279
|
calculateBillingPeriod,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertz/server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Vertz server runtime — modules, routing, and auth",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"typecheck": "tsc --noEmit"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@vertz/core": "^0.2.
|
|
35
|
-
"@vertz/db": "^0.2.
|
|
36
|
-
"@vertz/errors": "^0.2.
|
|
37
|
-
"@vertz/schema": "^0.2.
|
|
34
|
+
"@vertz/core": "^0.2.18",
|
|
35
|
+
"@vertz/db": "^0.2.18",
|
|
36
|
+
"@vertz/errors": "^0.2.18",
|
|
37
|
+
"@vertz/schema": "^0.2.18",
|
|
38
38
|
"bcryptjs": "^3.0.3",
|
|
39
39
|
"jose": "^6.0.11"
|
|
40
40
|
},
|