diff --git a/index.d.ts b/index.d.ts index facb0ca..85ea054 100644 --- a/index.d.ts +++ b/index.d.ts @@ -6,6 +6,286 @@ declare global { [key: string]: any } + /** + * Represents a `boolean` value or an object with an `eval` JS expression. + * Used for field-level `readonly` and `hidden` configuration. + */ + export type BoolOrEval = boolean | { eval: string } + + /** Structured field validator as defined in Go CollectionField.Validator */ + export interface FieldValidator { + /** Array of allowed values */ + in?: any[] + /** Whether the field is required */ + required?: boolean + /** Whether zero-values are allowed when required is true */ + allowZero?: boolean + /** JS eval expression for custom validation */ + eval?: string + } + + /** Search configuration for a collection */ + export interface SearchConfig { + /** Search config name (used in query parameter qName) */ + name: string + /** Search mode: text (MongoDB text index), regex, eval (JS), filter, ngram */ + mode: "text" | "regex" | "eval" | "filter" | "ngram" + /** Fields to search in (for text/regex mode) */ + fields?: string[] + /** Meta information */ + meta?: { [key: string]: any } + /** JS eval expression (for eval mode) */ + eval?: string + /** MongoDB filter template (for filter mode) */ + filter?: { [key: string]: any } + } + + /** Query limits configuration for a collection */ + export interface QueryLimitsConfig { + /** Default limit for GET queries (if client doesn't specify) */ + defaultLimit?: number + /** Maximum allowed limit for GET queries */ + maxLimit?: number + } + + /** Bulk operation optimization configuration */ + export interface BulkOptimizeConfig { + /** Optimize bulk create operations */ + create?: boolean + /** Optimize bulk update operations */ + update?: boolean + /** Optimize bulk delete operations */ + delete?: boolean + } + + /** Bulk configuration for a collection */ + export interface BulkConfig { + /** Optimization settings for bulk operations */ + optimize?: BulkOptimizeConfig + } + + /** Collection index definition */ + export interface CollectionIndex { + /** Index name */ + name?: string + /** Index key fields (prefix with - for descending) */ + key: string[] + /** Whether the index enforces uniqueness */ + unique?: boolean + /** Whether to drop duplicate entries */ + dropDups?: boolean + /** Build index in background */ + background?: boolean + /** Only index documents that contain the indexed field */ + sparse?: boolean + /** Default language for text indexes */ + defaultLanguage?: string + /** Field that contains the language override */ + languageOverride?: string + } + + /** Audit configuration for a collection */ + export interface AuditCollectionConfig { + /** Whether audit logging is enabled for this collection */ + enabled?: boolean + /** List of actions to audit: create, update, delete, bulkCreate, bulkUpdate, bulkDelete, get */ + actions?: string[] + } + + /** Image filter parameters — used in both CollectionConfig.imageFilter and ImagePackage.filter() */ + export interface ImageFilterParams { + width?: number + height?: number + fit?: boolean + fill?: boolean + resampling?: + | "nearestNeighbor" + | "hermite" + | "linear" + | "catmullRom" + | "lanczos" + | "box" + | "mitchellNetravili" + | "bSpline" + | "gaussian" + | "bartlett" + | "hann" + | "hamming" + | "blackman" + | "welch" + | "cosine" + anchor?: + | "center" + | "topLeft" + | "top" + | "topRight" + | "left" + | "right" + | "bottomLeft" + | "bottom" + | "bottomRight" + brightness?: number + saturation?: number + contrast?: number + gamma?: number + blur?: number + sharpen?: number + invert?: boolean + grayscale?: boolean + quality?: number + lossless?: boolean + skipLargerDimension?: boolean + skipLargerFilesize?: boolean + outputType?: "jpg" | "jpeg" | "png" | "webp" + } + + /** Collection config as returned by context.collection() – JSON-serialized from Go CollectionConfig */ + export interface CollectionInfo { + name: string + defaultLanguage?: string + permissions?: { [role: string]: PermissionSet } + projections?: { [name: string]: ProjectionConfig } + meta?: { [key: string]: any } + fields?: CollectionFieldInfo[] + /** Named image filter presets for file fields */ + imageFilter?: { [name: string]: ImageFilterParams[] } + /** Collection index definitions */ + indexes?: CollectionIndex[] + /** Audit configuration */ + audit?: AuditCollectionConfig + /** Bulk operation configuration */ + bulk?: BulkConfig + /** Query limits for GET requests */ + queryLimits?: QueryLimitsConfig + /** Fields that are read-only at collection level */ + readonlyFields?: string[] + /** Fields that are hidden at collection level */ + hiddenFields?: string[] + /** Search configurations */ + search?: SearchConfig[] + /** Reject unknown fields not defined in the fields array */ + strictFields?: boolean + [key: string]: any + } + + export type MethodPermission = boolean | { allow: boolean; bulk?: boolean } + export type SimpleMethodPermission = boolean | { allow: boolean } + + export interface PermissionSet { + methods?: { + get?: SimpleMethodPermission + /** `true` = allow single POST, `{allow: true, bulk: true}` = allow single + bulk POST */ + post?: MethodPermission + /** `true` = allow single PUT, `{allow: true, bulk: true}` = allow single + bulk PUT */ + put?: MethodPermission + /** `true` = allow single DELETE, `{allow: true, bulk: true}` = allow single + bulk DELETE */ + delete?: MethodPermission + } + /** MongoDB filter applied to all queries for this permission set */ + filter?: { [key: string]: any } + /** List of projection names this permission set is allowed to use */ + validProjections?: string[] + /** Fields that are read-only for this permission set */ + readonlyFields?: string[] + /** Fields that are hidden for this permission set */ + hiddenFields?: string[] + [key: string]: any + } + + export interface ProjectionConfig { + select?: { [field: string]: 0 | 1 } + [key: string]: any + } + + export interface CollectionFieldInfo { + name: string + type: string + subFields?: CollectionFieldInfo[] + index?: string[] + validator?: FieldValidator + meta?: { [key: string]: any } + /** Field-level readonly — boolean or JS eval expression */ + readonly?: BoolOrEval + /** Field-level hidden — boolean or JS eval expression */ + hidden?: BoolOrEval + /** Reject unknown sub-fields not defined in subFields */ + strictFields?: boolean + } + + /** API info as returned by context.api() – JSON-serialized from Go API struct */ + export interface ApiInfo { + isOnline: boolean + lastReload: string + namespace: string + meta?: { [key: string]: any } + assets?: { name: string; path: string }[] + collections?: CollectionInfo[] + jobs?: JobConfig[] + errors?: { collection?: string; context?: string; error: any }[] + [key: string]: any + } + + /** Project info as returned by context.project() – JSON-serialized from Go Project struct */ + export interface ProjectInfo { + name: string + description?: string + namespace?: string + users?: string[] + configFile?: string + api?: ApiInfo + yourPermissions?: { [key: string]: any } + [key: string]: any + } + + /** Represents a single audit log entry as received in audit.return hooks */ + export interface AuditLogEntry { + id?: string + insertTime?: string + updateTime?: string + /** ID of the user who performed the action */ + userId?: string + /** Username of the user who performed the action */ + username?: string + /** Role of the user (0=admin, 1=user) */ + userRole?: number + /** Project namespace */ + projectNamespace?: string + /** Project display name */ + projectName?: string + /** Collection name */ + collection?: string + /** Action performed: create, bulkCreate, update, delete, bulkUpdate, bulkDelete, get, login, reload, shutdown */ + action?: string + /** Source information (type, collection, method, step, file, line) */ + source?: { + type?: string + collection?: string + method?: string + step?: string + file?: string + line?: number + } + /** Document ID affected */ + documentId?: string + /** Full document snapshot at time of action */ + snapshot?: { [key: string]: any } + /** Changed fields (for updates) */ + changes?: { [key: string]: any } + /** Filter used (for queries/deletes) */ + filter?: { [key: string]: any } + /** Number of affected documents (e.g. deleteMany) */ + count?: number + /** Client IP address */ + ip?: string + /** Authentication method used (e.g. "jwt", "adminToken") */ + authMethod?: string + /** Label of the admin token used (if applicable) */ + tokenLabel?: string + /** Prefix of the admin token used (if applicable) */ + tokenPrefix?: string + [key: string]: any + } + export interface DbReadOptions { filter?: { [key: string]: any @@ -24,13 +304,16 @@ declare global { interface GetHookGetOnlyData { /** - * true if only one document was requested via /COLLECTION/ID + * true if only one document was requested via /COLLECTION/ID. + * In audit.return hooks this is always true (entries are processed individually). */ one?: boolean /** - * get list of documents (only valid after stage "read" in "get" hook) + * Get list of documents. + * - In get.return hooks: array of collection documents + * - In audit.return hooks: array with a single AuditLogEntry */ - results(): CollectionDocument[] + results(): CollectionDocument[] | AuditLogEntry[] } interface GetHookData { @@ -68,9 +351,30 @@ declare global { interface PostHookData { /** - * post data only valid in "post" and "put" hooks + * post data only valid in "post" and "put" hooks. + * For bulk hooks (bulkCreate), this is an array of documents. */ - data?: CollectionDocument + data?: CollectionDocument | CollectionDocument[] + + /** + * Number of created documents — only set in post.bulkReturn hook + */ + created?: number + + /** + * Array of created document IDs — only set in post.bulkReturn hook + */ + ids?: string[] + + /** + * Number of modified documents — only set in put.bulkReturn hook + */ + modified?: number + + /** + * Number of deleted documents — only set in delete.bulkReturn hook + */ + deleted?: number } interface JobConfig { @@ -93,6 +397,11 @@ declare global { * jobs program file */ file?: string + + /** + * execution timeout in seconds + */ + timeout?: number } interface JobData { @@ -113,22 +422,58 @@ declare global { * get current project object * */ - project(): { - [key: string]: any - } + project(): ProjectInfo /** - * get server config object + * get server config object (sensitive fields like jwtSecret, adminTokens, apiKeys are excluded) * */ server(): { api: { port: number + secureCookies?: boolean + accessTokenLifetime?: string + refreshTokenLifetime?: string } security: { allowAbsolutePaths: boolean allowUpperPaths: boolean } + llm?: { + providers?: { + name: string + type: string + baseURL: string + defaultModel: string + models?: string[] + maxTokensPerRequest?: number + }[] + } + audit?: { + enabled: boolean + defaultTTL: string + defaultLimit?: number + maxLimit?: number + } + ratelimit?: { + enabled?: boolean + loginInitialDelay?: string + loginMaxDelay?: string + loginResetAfter?: string + } + hook?: { + cache: { + maxEntries?: number + defaultTTL?: string + } + ratelimit: { + backoffInitialDelay?: string + backoffMaxDelay?: string + backoffResetAfter?: string + windowSize?: string + windowMaxHits?: number + } + } } } @@ -160,14 +505,25 @@ declare global { /** * update a document in a collection * + * Supports field-level operators in data: + * - `{ "$inc": value }` — increment by value + * - `{ "$mul": value }` — multiply by value + * - `{ "$unset": true }` — remove the field + * - `{ "$set": value }` — explicit set (same as plain value) + * + * Operators are converted to native MongoDB operators and executed + * atomically in a single DB call. + * * @param colName collection name * @param id id of entry - * @param data new/changed data + * @param data new/changed data (may contain operators) + * @param options optional: `{ raw: true }` to pass data as raw MongoDB update document */ update( colName: string, id: string, - data: CollectionDocument + data: CollectionDocument, + options?: { raw?: boolean } ): CollectionDocument /** @@ -188,6 +544,26 @@ declare global { colName: string, options?: DbReadOptions ): { message: "ok"; removed: number } + + /** + * update multiple documents matching a filter + * + * Use `changes` for simple field updates (supports operators like $inc, $mul). + * Use `update` for raw MongoDB update documents. + * + * @param colName collection name + * @param options options with filter and changes/update + */ + updateMany( + colName: string, + options: { + filter?: { [key: string]: any } + /** Field updates — may contain operators like { "$inc": 1 } */ + changes?: { [key: string]: any } + /** Raw MongoDB update document (e.g. { "$inc": { stock: -1 } }) */ + update?: { [key: string]: any } + } + ): { message: "ok"; modified: number } } interface SmtpPackage { @@ -305,7 +681,7 @@ declare global { interface HttpPackage { /** - * http request + * http request — reads the entire response body into memory * * @param url url for request * @param options request options @@ -316,7 +692,7 @@ declare global { method?: string headers?: { [key: string]: string } body?: string - // timeout in seconds + /** timeout in seconds (default 10) */ timeout?: number } ): { @@ -330,6 +706,35 @@ declare global { json(): any } } + + /** + * streaming http request — response body is NOT read into memory. + * Use body.read() to read line by line (ideal for SSE / newline-delimited + * protocols like OpenAI streaming API). + * + * @param url url for request + * @param options request options + */ + fetchStream( + url: string, + options?: { + method?: string + headers?: { [key: string]: string } + body?: string + /** timeout in seconds (default 0 = no timeout, hook timeout controls duration) */ + timeout?: number + } + ): { + status: number + statusText: string + headers: { [key: string]: string } + body: { + /** returns the next line as string, or null at EOF */ + read(): string | null + /** explicitly closes the response body (auto-closed at EOF) */ + close(): void + } + } } interface DebugPackage { @@ -355,12 +760,46 @@ declare global { interface ResponsePackage { /** - * set response header + * set response header (must be called before writeHeader/write) * * @param name header name * @param value value */ header(name: string, value: any): void + + /** + * set the HTTP status code — must be called before write(). + * If not called, the first write() will implicitly send status 200. + * + * @param status HTTP status code + */ + writeHeader(status: number): void + + /** + * write data directly to the HTTP response body. + * Use for streaming responses (e.g. SSE to a chat widget). + * + * @param data string or byte data to write + */ + write(data: string | any): void + + /** + * flush buffered data to the client immediately. + * Essential for streaming / SSE to push chunks without waiting + * for the full response to complete. + */ + flush(): void + + /** + * write a Server-Sent Event and flush immediately. + * Formats the output as SSE: "event: {event}\ndata: {data}\n\n" + * If called with one argument, only "data: {data}\n\n" is written. + * + * @param event SSE event name (optional if using single-argument form) + * @param data SSE data payload + */ + writeSSE(event: string, data: string): void + writeSSE(data: string): void } interface UserPackage { @@ -464,41 +903,7 @@ declare global { filter( sourceFile: string, targetFile: string, - filters: { - width?: number - height?: number - fit?: boolean - fill?: boolean - resampling?: "nearestNeighbor" - | "hermite" - | "linear" - | "catmullRom" - | "lanczos" - | "box" - | "mitchellNetravili" - | "bSpline" - | "gaussian" - | "bartlett" - | "hann" - | "hamming" - | "blackman" - | "welch" - | "cosine" - anchor?: "center" | "topLeft" | "top" | "topRight" | "left" | "right" | "bottomLeft" | "bottom" | "bottomRight" - brightness?: number - saturation?: number - contrast?: number - gamma?: number - blur?: number - sharpen?: number - invert?: boolean - grayscale?: boolean - quality?: number - lossless?: boolean - skipLargerDimensions?: boolean - skipLargerFilesize?: boolean - outputType?: "jpg" | "jpeg" | "png" | "webp" - }[] + filters: ImageFilterParams[] ): void } @@ -509,10 +914,13 @@ declare global { * @param data object or array (map with string keys as nodes (-KEY will be attribute in parent node)) * @param options options */ - create(data: {[key: string]: any}, options?: { - rootElement?: string - includeHeader?: boolean - }): string + create( + data: { [key: string]: any }, + options?: { + rootElement?: string + includeHeader?: boolean + } + ): string /** * parse xml string to json @@ -689,45 +1097,224 @@ declare global { } interface ExecPackage { - command(cmd: string, options?: { - args?: string[] - stdin?: string - dir?: string - env?: string[] - timeout?: number - returnObject?: boolean - combineOutput?: boolean - }): string + command( + cmd: string, + options?: { + args?: string[] + stdin?: string + dir?: string + env?: string[] + timeout?: number + returnObject?: boolean + combineOutput?: boolean + } + ): string - command(cmd: string, options: T): - T['returnObject'] extends true - ? T['combineOutput'] extends true - ? { output: string; exitCode: number } - : { stdout: string; stderr: string; exitCode: number } - : string + command< + T extends { + args?: string[] + stdin?: string + dir?: string + env?: string[] + timeout?: number + returnObject?: boolean + combineOutput?: boolean + }, + >( + cmd: string, + options: T + ): T["returnObject"] extends true + ? T["combineOutput"] extends true + ? { output: string; exitCode: number } + : { stdout: string; stderr: string; exitCode: number } + : string } interface Base64Package { - encode(data: string|Int8Array): string + encode(data: string | Int8Array): string decode(data: string): string - decode(data: string, options: {returnBytes: true}): Int8Array - decode(data: string, options: {returnBytes: false}): string - decode(data: string, options: {returnBytes?: boolean}): string | Int8Array + decode(data: string, options: { returnBytes: true }): Int8Array + decode(data: string, options: { returnBytes: false }): string + decode( + data: string, + options: { returnBytes?: boolean } + ): string | Int8Array + } + + /** + * Project-scoped in-memory cache. + * + * Entries survive across requests but are cleared on project reload. + * Configured via project config `hook.cache` or server config `hook.cache`. + */ + interface CachePackage { + /** + * Store a value in the cache. + * + * @param key Cache key + * @param value Any JSON-serializable value + * @param ttlMs Optional time-to-live in milliseconds (overrides defaultTTL) + */ + set(key: string, value: any, ttlMs?: number): void + + /** + * Retrieve a value from the cache. + * + * @param key Cache key + * @returns The cached value, or `null` if not found / expired. + */ + get(key: string): any | null + + /** + * Check whether a key exists (and is not expired) in the cache. + * + * @param key Cache key + */ + has(key: string): boolean + + /** + * Delete a single key from the cache. + * + * @param key Cache key + * @returns `true` if the key existed, `false` otherwise. + */ + delete(key: string): boolean + + /** + * Remove all entries from the cache. + */ + clear(): void + + /** + * Return the number of entries currently in the cache. + */ + count(): number + } + + /** + * Project-scoped rate limiter with two strategies: + * - **Backoff**: Exponential back-off per key (e.g. login brute-force protection). + * - **Window**: Sliding-window counter per key (e.g. API call quotas). + * + * Configured via project config `hook.ratelimit` or server config `hook.ratelimit`. + */ + interface RatelimitPackage { + /** + * Check whether a key is currently blocked by the exponential back-off strategy. + * + * @param key Rate-limit key (e.g. IP address or user ID) + * @returns `{ blocked, retryAfterMs }` — if blocked, retryAfterMs indicates how long to wait. + */ + backoffCheck(key: string): { blocked: boolean; retryAfterMs: number } + + /** + * Record a failure for the given key, increasing its back-off delay. + * + * @param key Rate-limit key + */ + backoffRecord(key: string): void + + /** + * Reset the back-off state for a key (e.g. after a successful login). + * + * @param key Rate-limit key + */ + backoffReset(key: string): void + + /** + * Check the sliding-window strategy for a key. + * + * @param key Rate-limit key + * @returns `{ allowed, retryAfterMs }` — if not allowed, retryAfterMs indicates how long to wait. + */ + windowCheck(key: string): { allowed: boolean; retryAfterMs: number } + + /** + * Reset the sliding-window counter for a key. + * + * @param key Rate-limit key + */ + windowReset(key: string): void + + /** + * Return the current hit count in the sliding window for a key. + * + * @param key Rate-limit key + */ + windowCount(key: string): number + } + + /** + * Options for channel.subscribe(). + */ + interface ChannelSubscribeOptions { + /** Maximum number of buffered messages per subscriber (default: 64). */ + bufferSize?: number + /** + * Behaviour when the buffer is full. + * - "drop-oldest" (default): discard the oldest message + * - "drop-newest": discard the incoming message + */ + onFull?: "drop-oldest" | "drop-newest" + /** Time-to-live per message in milliseconds. Expired messages are skipped on receive. */ + messageTTL?: number + /** Replay the last N messages from the channel's ring-buffer to the new subscriber. */ + lastN?: number + /** Replay only messages younger than maxAge milliseconds. */ + maxAge?: number + } + + /** + * A subscription to a real-time channel. + * Returned by channel.subscribe(). + */ + interface ChannelSubscription { + /** + * Blocks until a message arrives, the client disconnects, or the + * subscription/channel is closed. + * + * @returns The message data, or `null` when the client disconnects. + * @throws `{error: string, code: "channel_closed"}` when the + * subscription or channel is closed (e.g. project reload). + */ + receive(): any + /** Manually close the subscription (idempotent). */ + close(): void + } + + /** + * Real-time pub/sub channel package. + * + * Channels are in-memory, project-scoped, and named freely (not bound + * to collections). Every subscriber receives an independent deep-copy + * of each message. + */ + interface ChannelPackage { + /** + * Subscribe to a named channel. The calling hook will block until + * the client disconnects or the subscription is closed. + * The execution timeout is automatically disabled for hooks that subscribe. + * + * @param name Channel name (arbitrary string). + * @param options Optional subscribe options. + */ + subscribe( + name: string, + options?: ChannelSubscribeOptions + ): ChannelSubscription + /** + * Publish data to all current subscribers of a named channel. + * This is a fire-and-forget operation; if no one is subscribed, the + * message is buffered in the channel's ring-buffer for future replay. + * + * @param name Channel name. + * @param data Arbitrary data (will be deep-copied per subscriber). + */ + send(name: string, data: any): void } export interface HookContext - extends GetHookData, - GetHookGetOnlyData, - PostHookData, - JobData { + extends GetHookData, GetHookGetOnlyData, PostHookData, JobData { request(): { method: string remoteAddr: string @@ -745,17 +1332,11 @@ declare global { bodyBytes(): string } - api(): { - [key: string]: any - } + api(): ApiInfo - project(): { - [key: string]: any - } + project(): ProjectInfo - collection(): { - [key: string]: any - } + collection(): CollectionInfo config: ConfigPackage db: DbPackage @@ -777,6 +1358,9 @@ declare global { json: JsonPackage exec: ExecPackage base64: Base64Package + channel: ChannelPackage + cache: CachePackage + ratelimit: RatelimitPackage } export interface HookException { diff --git a/schemas/api-config/collection.json b/schemas/api-config/collection.json index 95b6049..3198525 100644 --- a/schemas/api-config/collection.json +++ b/schemas/api-config/collection.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "JSON Schema tibi-server collection configuration", - "description": "tibi-server collection linter", + "description": "tibi-server collection configuration", "type": "object", "additionalProperties": false, "patternProperties": { @@ -22,14 +22,39 @@ "type": "string", "description": "relative to config.yml, path for file uploads" }, + "upload": { + "type": "object", + "description": "collection-specific upload defaults and restrictions", + "additionalProperties": false, + "properties": { + "maxBodySize": { + "type": "string", + "description": "request body size limit override for this collection (e.g. '10MB')" + }, + "allowedMimeTypes": { + "type": "array", + "description": "allowed MIME types for file uploads in this collection; empty means all types are allowed", + "items": { + "type": "string" + } + } + } + }, "meta": { "oneOf": [ - { "$comment": "for include tag", "type": "string" }, + { + "$comment": "for include tag", + "type": "string" + }, { "type": "object", "description": "meta object used for admin ui configuration", "additionalProperties": true, - "allOf": [{ "$ref": "collectionNavigation.json" }], + "allOf": [ + { + "$ref": "collectionNavigation.json" + } + ], "properties": { "rowIdentTpl": { "description": "template which evaluates to short string to identify entry in pe. select boxes", @@ -57,6 +82,28 @@ } } ] + }, + "pagebuilder": { + "type": "object", + "description": "Collection-level pagebuilder defaults. These serve as fallbacks when field-level pagebuilder config omits them.", + "additionalProperties": false, + "properties": { + "blockTypeField": { + "type": "string", + "description": "Name of the sub-field that holds the block type identifier. Default: 'blockType'. Used as fallback for all pagebuilder fields in this collection." + }, + "blockRegistry": { + "type": "object", + "description": "Block registry configuration.", + "additionalProperties": false, + "properties": { + "file": { + "type": "string", + "description": "URL to an ES module that default-exports a BlockRegistry. Used as fallback for all pagebuilder fields in this collection." + } + } + } + } } } } @@ -64,31 +111,54 @@ }, "projections": { "oneOf": [ - { "$comment": "for include tag", "type": "string" }, - { "$ref": "projections.json" } + { + "$comment": "for include tag", + "type": "string" + }, + { + "$ref": "projections.json" + } ] }, "permissions": { "oneOf": [ - { "$comment": "for include tag", "type": "string" }, - { "$ref": "permissions.json" } + { + "$comment": "for include tag", + "type": "string" + }, + { + "$ref": "permissions.json" + } ] }, "hooks": { "oneOf": [ - { "$comment": "for include tag", "type": "string" }, - { "$ref": "hooks.json" } + { + "$comment": "for include tag", + "type": "string" + }, + { + "$ref": "hooks.json" + } ] }, "imageFilter": { "oneOf": [ - { "$comment": "for include tag", "type": "string" }, - { "$ref": "imageFilter.json" } + { + "$comment": "for include tag", + "type": "string" + }, + { + "$ref": "imageFilter.json" + } ] }, "fields": { "oneOf": [ - { "$comment": "for include tag", "type": "string" }, + { + "$comment": "for include tag", + "type": "string" + }, { "type": "array", "description": "fields of collection", @@ -108,7 +178,10 @@ }, "indexes": { "oneOf": [ - { "$comment": "for include tag", "type": "string" }, + { + "$comment": "for include tag", + "type": "string" + }, { "type": "array", "description": "indexes of collection", @@ -126,6 +199,32 @@ } ] }, + "bulk": { + "type": "object", + "description": "bulk operation configuration", + "additionalProperties": false, + "properties": { + "optimize": { + "type": "object", + "description": "enable optimised single-DB-call bulk path without requiring a hook file", + "additionalProperties": false, + "properties": { + "create": { + "type": "boolean", + "description": "POST with JSON array uses optimised InsertMany path" + }, + "update": { + "type": "boolean", + "description": "PUT without ID uses optimised path" + }, + "delete": { + "type": "boolean", + "description": "DELETE without ID uses optimised path" + } + } + } + } + }, "cors": { "type": "object", "description": "cors configuration", @@ -172,7 +271,119 @@ "description": "max age in seconds" } } + }, + "audit": { + "type": "object", + "description": "audit logging configuration for this collection", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "description": "enable audit logging for this collection" + }, + "actions": { + "type": "array", + "description": "list of actions to audit (create, update, delete, bulkCreate, bulkUpdate, bulkDelete, get)", + "items": { + "type": "string", + "enum": [ + "create", + "update", + "delete", + "bulkCreate", + "bulkUpdate", + "bulkDelete", + "get" + ] + } + } + } + }, + "queryLimits": { + "type": "object", + "description": "query limit configuration for GET requests", + "additionalProperties": false, + "properties": { + "defaultLimit": { + "type": "integer", + "description": "default limit for GET queries if client doesn't specify one" + }, + "maxLimit": { + "type": "integer", + "description": "maximum allowed limit for GET queries" + } + } + }, + "readonlyFields": { + "type": "array", + "description": "fields that are read-only at collection level", + "items": { + "type": "string" + } + }, + "hiddenFields": { + "type": "array", + "description": "fields that are hidden at collection level", + "items": { + "type": "string" + } + }, + "search": { + "type": "array", + "description": "search configurations for the collection", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "search config name (used in query parameter qName)" + }, + "mode": { + "type": "string", + "description": "search mode", + "enum": [ + "text", + "regex", + "eval", + "filter" + ] + }, + "fields": { + "type": "array", + "description": "fields to search in (for text/regex mode)", + "items": { + "type": "string" + } + }, + "meta": { + "type": "object", + "description": "meta information", + "additionalProperties": true + }, + "eval": { + "type": "string", + "description": "JS eval expression (for eval mode)" + }, + "filter": { + "type": "object", + "description": "MongoDB filter template (for filter mode)", + "additionalProperties": true + } + }, + "required": [ + "name", + "mode" + ] + } + }, + "strictFields": { + "type": "boolean", + "description": "reject unknown fields not defined in the fields array" } }, - "required": ["name", "permissions"] -} + "required": [ + "name", + "permissions" + ] +} \ No newline at end of file diff --git a/schemas/api-config/collectionNavigation.json b/schemas/api-config/collectionNavigation.json index c9b3d68..ee64d51 100644 --- a/schemas/api-config/collectionNavigation.json +++ b/schemas/api-config/collectionNavigation.json @@ -22,6 +22,61 @@ "type": "string", "description": "image filter name used for image previews" }, + "viewHint": { + "description": "default collection view hint or structured special view config", + "oneOf": [ + { + "type": "string", + "enum": [ + "table", + "cards", + "media" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "navigation": { + "type": "object", + "additionalProperties": false, + "properties": { + "nodesField": { + "type": "string", + "description": "root field containing the recursive node array" + }, + "declaredTrees": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "label": { + "$ref": "definitions.json#/definitions/i18nString" + }, + "singleton": { + "type": "object", + "description": "root-level field values that identify one declared tree slot", + "additionalProperties": true + }, + "maxLevel": { + "type": "number" + } + }, + "required": [ + "singleton" + ] + } + } + } + } + }, + "required": [ + "navigation" + ] + } + ] + }, "muiIcon": { "type": "string", "description": "material ui icon name" diff --git a/schemas/api-config/config.json b/schemas/api-config/config.json index b391d36..4fbb86a 100644 --- a/schemas/api-config/config.json +++ b/schemas/api-config/config.json @@ -35,6 +35,24 @@ } } }, + "upload": { + "type": "object", + "description": "project-wide upload defaults and restrictions", + "additionalProperties": false, + "properties": { + "maxBodySize": { + "type": "string", + "description": "default request body size limit for collections in this project (e.g. '50MB')" + }, + "allowedMimeTypes": { + "type": "array", + "description": "project-wide allowed MIME types for file uploads; empty means all types are allowed", + "items": { + "type": "string" + } + } + } + }, "collections": { "type": "array", "description": "list of collections in this project", @@ -150,6 +168,59 @@ } } } + }, + "hook": { + "type": "object", + "description": "project-level hook configuration (cache and ratelimit)", + "additionalProperties": false, + "properties": { + "cache": { + "type": "object", + "description": "hook cache configuration", + "additionalProperties": false, + "properties": { + "maxEntries": { + "type": "integer", + "description": "maximum number of cache entries" + }, + "defaultTTL": { + "type": "string", + "description": "default time-to-live (e.g. '5m', '1h')" + } + } + }, + "ratelimit": { + "type": "object", + "description": "hook ratelimit configuration", + "additionalProperties": false, + "properties": { + "backoffInitialDelay": { + "type": "string", + "description": "initial backoff delay (e.g. '1s')" + }, + "backoffMaxDelay": { + "type": "string", + "description": "maximum backoff delay (e.g. '5m')" + }, + "backoffResetAfter": { + "type": "string", + "description": "reset backoff after this duration of no failures (e.g. '15m')" + }, + "windowSize": { + "type": "string", + "description": "sliding window size (e.g. '1m')" + }, + "windowMaxHits": { + "type": "integer", + "description": "maximum hits allowed per window" + } + } + } + } + }, + "strictFields": { + "type": "boolean", + "description": "reject unknown fields not defined in the fields array (project-wide default)" } }, "required": [ diff --git a/schemas/api-config/field.json b/schemas/api-config/field.json index 5328fd2..0f62cd5 100644 --- a/schemas/api-config/field.json +++ b/schemas/api-config/field.json @@ -25,6 +25,7 @@ "object", "object[]", "file", + "file[]", "date", "any" ] @@ -32,7 +33,13 @@ "index": { "type": "array", "items": { - "enum": ["single", "unique", "text"] + "enum": [ + "single", + "unique", + "text", + "sparse", + "background" + ] } }, "subFields": { @@ -74,12 +81,82 @@ "eval": { "type": "string", "description": "javascript validator which failes if evaluates to false or string as error message, with following variables: $this, $parent, $stack, $auth, context" + }, + "minLength": { + "type": "number", + "description": "minimum string length" + }, + "maxLength": { + "type": "number", + "description": "maximum string length" + }, + "pattern": { + "type": "string", + "description": "regex pattern the value must match" + }, + "min": { + "type": "number", + "description": "minimum value for number fields" + }, + "max": { + "type": "number", + "description": "maximum value for number fields" + }, + "in": { + "type": "array", + "description": "array of allowed values" + }, + "maxFileSize": { + "type": "string", + "description": "maximum decoded file size for file and file[] fields (e.g. '500KB')" + }, + "accept": { + "type": "array", + "description": "allowed MIME types for file and file[] fields; supports wildcards like 'image/*'", + "items": { + "type": "string" + } } } }, + "readonly": { + "description": "field-level readonly — boolean or JS eval expression", + "oneOf": [ + { "type": "boolean" }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "eval": { "type": "string", "description": "JS expression that returns boolean" } + }, + "required": ["eval"] + } + ] + }, + "hidden": { + "description": "field-level hidden — boolean or JS eval expression", + "oneOf": [ + { "type": "boolean" }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "eval": { "type": "string", "description": "JS expression that returns boolean" } + }, + "required": ["eval"] + } + ] + }, + "strictFields": { + "type": "boolean", + "description": "reject unknown sub-fields not defined in subFields" + }, "meta": { "$ref": "fieldMeta.json" } }, - "required": ["name", "type"] -} + "required": [ + "name", + "type" + ] +} \ No newline at end of file diff --git a/schemas/api-config/fieldMeta.json b/schemas/api-config/fieldMeta.json index c5a16f5..40dae2e 100644 --- a/schemas/api-config/fieldMeta.json +++ b/schemas/api-config/fieldMeta.json @@ -413,6 +413,171 @@ } } ] + }, + { + "$ref": "#/definitions/minimum", + "patternProperties": { + "^x-.*|widget|pagebuilder$": { + "$comment": "stub for allOf" + } + }, + "allOf": [ + { + "properties": { + "widget": { + "enum": [ + "pagebuilder", + "pageBuilder" + ] + }, + "pagebuilder": { + "type": "object", + "description": "Pagebuilder widget configuration. Field-level settings override collection-level fallbacks.", + "additionalProperties": false, + "properties": { + "mode": { + "type": "string", + "enum": [ + "overlay", + "inline", + "both" + ], + "description": "[Planned] Editor mode. Currently only 'overlay' is implemented. 'inline' and 'both' are planned." + }, + "defaultViewport": { + "type": "number", + "description": "Default viewport width in pixels for the preview. Common presets: 1280 (Desktop), 768 (Tablet), 375 (Phone). Default: 1280. Overrides collection-level fallback." + }, + "blockTypeField": { + "type": "string", + "description": "Name of the sub-field that holds the block type identifier. Default: 'blockType'. Overrides collection-level fallback." + }, + "blockRegistry": { + "type": "object", + "description": "Block registry configuration for loading available block definitions.", + "additionalProperties": false, + "properties": { + "file": { + "type": "string", + "description": "URL to an ES module that default-exports a BlockRegistry (Record). Overrides collection-level fallback." + } + } + } + } + } + } + } + ] + }, + { + "$ref": "#/definitions/minimum", + "patternProperties": { + "^x-.*|widget|pagebuilder$": { + "$comment": "stub for allOf" + } + }, + "allOf": [ + { + "properties": { + "widget": { + "enum": [ + "pagebuilder", + "pageBuilder" + ] + }, + "pagebuilder": { + "type": "object", + "description": "Pagebuilder widget configuration. Field-level settings override collection-level fallbacks.", + "additionalProperties": false, + "properties": { + "mode": { + "type": "string", + "enum": [ + "overlay", + "inline", + "both" + ], + "description": "[Planned] Editor mode. Currently only 'overlay' is implemented. 'inline' and 'both' are planned." + }, + "defaultViewport": { + "type": "number", + "description": "Default viewport width in pixels for the preview. Common presets: 1280 (Desktop), 768 (Tablet), 375 (Phone). Default: 1280. Overrides collection-level fallback." + }, + "blockTypeField": { + "type": "string", + "description": "Name of the sub-field that holds the block type identifier. Default: 'blockType'. Overrides collection-level fallback." + }, + "blockRegistry": { + "type": "object", + "description": "Block registry configuration for loading available block definitions.", + "additionalProperties": false, + "properties": { + "file": { + "type": "string", + "description": "URL to an ES module that default-exports a BlockRegistry (Record). Overrides collection-level fallback." + } + } + } + } + } + } + } + ] + }, + { + "$ref": "#/definitions/minimum", + "patternProperties": { + "^x-.*|widget|pagebuilder$": { + "$comment": "stub for allOf" + } + }, + "allOf": [ + { + "properties": { + "widget": { + "enum": [ + "pagebuilder", + "pageBuilder" + ] + }, + "pagebuilder": { + "type": "object", + "description": "Pagebuilder widget configuration. Field-level settings override collection-level fallbacks.", + "additionalProperties": false, + "properties": { + "mode": { + "type": "string", + "enum": [ + "overlay", + "inline", + "both" + ], + "description": "[Planned] Editor mode. Currently only 'overlay' is implemented. 'inline' and 'both' are planned." + }, + "defaultViewport": { + "type": "number", + "description": "Default viewport width in pixels for the preview. Common presets: 1280 (Desktop), 768 (Tablet), 375 (Phone). Default: 1280. Overrides collection-level fallback." + }, + "blockTypeField": { + "type": "string", + "description": "Name of the sub-field that holds the block type identifier. Default: 'blockType'. Overrides collection-level fallback." + }, + "blockRegistry": { + "type": "object", + "description": "Block registry configuration for loading available block definitions.", + "additionalProperties": false, + "properties": { + "file": { + "type": "string", + "description": "URL to an ES module that default-exports a BlockRegistry (Record). Overrides collection-level fallback." + } + } + } + } + } + } + } + ] } ], "definitions": { @@ -721,18 +886,27 @@ "type": "boolean" }, "size": { - "type": "object", - "properties": { - "default": { - "type": "string" + "oneOf": [ + { + "type": "string", + "description": "shorthand: single size for all breakpoints (e.g. col-3)" }, - "small": { - "type": "string" - }, - "large": { - "type": "string" + { + "type": "object", + "description": "responsive sizes per breakpoint", + "properties": { + "default": { + "type": "string" + }, + "small": { + "type": "string" + }, + "large": { + "type": "string" + } + } } - } + ] } } } @@ -794,6 +968,62 @@ ] } } + }, + "widget": { + "type": "string", + "description": "widget type for rendering the field in admin UI" + }, + "position": { + "type": "string", + "description": "field position in the editor layout: 'main' (default), 'sidebar' (default settings card), or 'sidebar:' (named sidebar card, e.g. 'sidebar:SEO')" + }, + "section": { + "description": "section grouping for the field in the editor", + "$ref": "definitions.json#/definitions/i18nString" + }, + "choices": { + "description": "choices for select/chipArray/checkboxArray widgets", + "oneOf": [ + { + "type": "array", + "items": { + "type": "object" + } + }, + { + "type": "object" + } + ] + }, + "foreign": { + "type": "object", + "description": "foreign key configuration for foreignKey/foreignFile widgets" + }, + "downscale": { + "type": "object", + "description": "image downscale configuration for file/image widgets", + "properties": { + "maxWidth": { + "type": "number", + "description": "maximum width in pixels" + }, + "maxHeight": { + "type": "number", + "description": "maximum height in pixels" + }, + "quality": { + "type": "number", + "description": "JPEG quality (0-100)" + } + } + }, + "drillDown": { + "type": "boolean", + "description": "whether object subFields should be rendered inline (false) or as drill-down navigation (true, default)" + }, + "readonly": { + "type": "boolean", + "description": "if true, the field is displayed as a read-only view (table/card style) instead of an editable widget. Use inputProps for a disabled input instead." } } }, @@ -841,4 +1071,4 @@ ] } } -} \ No newline at end of file +} diff --git a/schemas/api-config/hooks.json b/schemas/api-config/hooks.json index 30b3206..d6c869c 100644 --- a/schemas/api-config/hooks.json +++ b/schemas/api-config/hooks.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "JSON Schema tibi-server hooks configuration", - "description": "tibi-server hooks linter", + "description": "tibi-server hooks configuration", "type": "object", "additionalProperties": false, "patternProperties": { @@ -22,6 +22,10 @@ "return": { "description": "hook before returning entries read from database", "$ref": "#/definitions/hookDef" + }, + "file": { + "description": "hook for file download requests (GET /collection/:id/:field/*path)", + "$ref": "#/definitions/hookDef" } } }, @@ -45,6 +49,14 @@ "return": { "description": "hook before returning result (after database write)", "$ref": "#/definitions/hookDef" + }, + "bulkCreate": { + "description": "hook for bulk create (POST with JSON array) — optimised single-DB-call path", + "$ref": "#/definitions/hookDef" + }, + "bulkReturn": { + "description": "hook before returning bulk create result", + "$ref": "#/definitions/hookDef" } } }, @@ -68,6 +80,14 @@ "return": { "description": "hook before returning result (after database write)", "$ref": "#/definitions/hookDef" + }, + "bulkUpdate": { + "description": "hook for bulk update (PUT without ID) — optimised single-DB-call path", + "$ref": "#/definitions/hookDef" + }, + "bulkReturn": { + "description": "hook before returning bulk update result", + "$ref": "#/definitions/hookDef" } } }, @@ -83,6 +103,25 @@ "return": { "description": "hook before returning result (after database write)", "$ref": "#/definitions/hookDef" + }, + "bulkDelete": { + "description": "hook for bulk delete (DELETE without ID) — optimised single-DB-call path", + "$ref": "#/definitions/hookDef" + }, + "bulkReturn": { + "description": "hook before returning bulk delete result", + "$ref": "#/definitions/hookDef" + } + } + }, + "audit": { + "type": "object", + "description": "hooks for audit log entries", + "additionalProperties": false, + "properties": { + "return": { + "description": "hook before returning audit log entries for this collection, can be used to remove sensitive fields from snapshots", + "$ref": "#/definitions/hookDef" } } } @@ -97,6 +136,10 @@ "file": { "type": "string", "description": "location of javascript hook file relative to config.yml base" + }, + "timeout": { + "type": "integer", + "description": "execution timeout in seconds" } }, "required": ["type", "file"] diff --git a/schemas/api-config/imageFilter.json b/schemas/api-config/imageFilter.json index a6666d1..0741c4c 100644 --- a/schemas/api-config/imageFilter.json +++ b/schemas/api-config/imageFilter.json @@ -52,18 +52,48 @@ "enum": [ "lanczos", "nearestNeighbor", + "hermite", "linear", - "catmullRom" + "catmullRom", + "box", + "mitchellNetravili", + "bSpline", + "gaussian", + "bartlett", + "hann", + "hamming", + "blackman", + "welch", + "cosine" + ] + }, + "anchor": { + "enum": [ + "center", + "topLeft", + "top", + "topRight", + "left", + "right", + "bottomLeft", + "bottom", + "bottomRight" ] }, "quality": { "type": "number" }, + "lossless": { + "type": "boolean" + }, "skipLargerDimension": { "type": "boolean" }, "skipLargerFilesize": { "type": "boolean" + }, + "outputType": { + "enum": ["jpg", "jpeg", "png", "webp"] } } } diff --git a/schemas/api-config/job.json b/schemas/api-config/job.json index 34aee81..5309cbb 100644 --- a/schemas/api-config/job.json +++ b/schemas/api-config/job.json @@ -26,6 +26,10 @@ "file": { "type": "string", "description": "javascript file relative to config.yml" + }, + "timeout": { + "type": "integer", + "description": "execution timeout in seconds" } }, "required": ["type", "file"] diff --git a/schemas/api-config/permissions.json b/schemas/api-config/permissions.json index a3792d6..e7a75dc 100644 --- a/schemas/api-config/permissions.json +++ b/schemas/api-config/permissions.json @@ -38,14 +38,85 @@ "description": "permissions for http methods", "additionalProperties": false, "properties": { - "get": { "type": "boolean" }, - "post": { "type": "boolean" }, - "put": { "type": "boolean" }, - "delete": { "type": "boolean" } + "get": { + "oneOf": [ + { "type": "boolean" }, + { + "type": "object", + "properties": { + "allow": { "type": "boolean", "description": "Allow GET." } + }, + "required": ["allow"], + "additionalProperties": false + } + ] + }, + "post": { + "oneOf": [ + { "type": "boolean" }, + { + "type": "object", + "properties": { + "allow": { "type": "boolean", "description": "Allow single-document POST." }, + "bulk": { "type": "boolean", "description": "Allow bulk POST (JSON array body)." } + }, + "required": ["allow"], + "additionalProperties": false + } + ] + }, + "put": { + "oneOf": [ + { "type": "boolean" }, + { + "type": "object", + "properties": { + "allow": { "type": "boolean", "description": "Allow single-document PUT." }, + "bulk": { "type": "boolean", "description": "Allow bulk PUT (without ID)." } + }, + "required": ["allow"], + "additionalProperties": false + } + ] + }, + "delete": { + "oneOf": [ + { "type": "boolean" }, + { + "type": "object", + "properties": { + "allow": { "type": "boolean", "description": "Allow single-document DELETE." }, + "bulk": { "type": "boolean", "description": "Allow bulk DELETE (without ID)." } + }, + "required": ["allow"], + "additionalProperties": false + } + ] + } } }, "validProjections": { "type": "array", + "description": "list of projection names this permission set is allowed to use", + "items": { + "type": "string" + } + }, + "filter": { + "type": "object", + "description": "MongoDB filter applied to all queries for this permission set", + "additionalProperties": true + }, + "readonlyFields": { + "type": "array", + "description": "fields that are read-only for this permission set", + "items": { + "type": "string" + } + }, + "hiddenFields": { + "type": "array", + "description": "fields that are hidden for this permission set", "items": { "type": "string" }