@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 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.18",
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.17",
35
- "@vertz/db": "^0.2.17",
36
- "@vertz/errors": "^0.2.17",
37
- "@vertz/schema": "^0.2.17",
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
  },