import { Type } from "@nestjs/common"; import { DataMeta, DataModelInterface } from "./datamodel.interface"; /** * Neo4j/Cypher base data types for field definitions (scalar) */ export type CypherBaseType = "string" | "number" | "boolean" | "date" | "datetime" | "json"; /** * Neo4j/Cypher array data types for field definitions */ export type CypherArrayType = "string[]" | "number[]" | "boolean[]" | "date[]" | "datetime[]" | "json[]"; /** * All supported Neo4j/Cypher data types (scalar and array) */ export type CypherType = CypherBaseType | CypherArrayType; /** * Transformer function for field serialisation * Receives the entity data and any injected services */ export type FieldTransformFn = (data: any, services: Record) => Promise | any; /** * Semantic kind of a scalar field, used by the chatbot layer to render unit * markers in the graph catalogue and to format values for LLM narration. * The discriminator shape leaves room for non-money kinds (percent, duration, * etc.) without widening a plain string literal later. */ export type FieldKind = { type: "money"; minorUnits?: number; } | { type: "richtext"; }; /** * Field definition for entity schema * Defines a single property with its type and constraints */ export interface FieldDef { /** Neo4j/Cypher data type */ type: CypherType; /** Whether the field is required (default: false) */ required?: boolean; /** Default value to use when creating an entity */ default?: any; /** If true, field goes to JSON:API meta instead of attributes (default: false) */ meta?: boolean; /** Async transformer function for serialisation - receives data and injected services */ transform?: FieldTransformFn; /** If true, field is excluded from JSONAPI serialization entirely (default: false) */ excludeFromJsonApi?: boolean; /** * If true, a `type: "string"` field is kept out of the entity's auto-derived * FULLTEXT index (default: false — every string field is indexed). * * Set this on machine-readable strings: serialised JSON config, UUID * references, status enums, auth tokens. Their contents would otherwise be * searchable text, so a term matching inside a config blob returns an entity * whose name has nothing to do with the query. * * Storage, validation and serialisation are unaffected — this only narrows * the index. Excluding every string field leaves `fulltextIndexName` empty, * which the search guards already treat as "no index". * * NOTE: changing this on an existing entity does not rebuild the live index. * `AbstractRepository` issues `CREATE ... IF NOT EXISTS`, which no-ops when an * index of that name already exists, so the old properties persist until that * index is dropped manually. */ excludeFromSearch?: boolean; /** * If false, field is suppressed from JSON:API output (attributes and meta). * Defaults to true. Used for storage-only fields (e.g. cents storage backing * a float computed) that must remain readable from Neo4j and addressable in * Cypher writes, but should never appear on the wire. */ serialise?: boolean; /** Human-readable description. Required for the field to be visible to the chatbot. */ description?: string; /** * If true, the field is never written through the generic create/update (PUT) * path. On PUT the existing stored value is preserved instead of being nulled, * and any client-supplied value is ignored. Use for server-generated fields that * a client edit form does not carry (e.g. an AI-generated opening) so a partial * full-replacement does not wipe them. Mirrors relationship-level `immutable`. * (default: false) */ immutable?: boolean; /** * Semantic kind for chatbot rendering. When set to `{ type: "money" }`, * the field is stored as an integer in the currency's minor unit (cents by * default). The catalogue annotates it and tool returns emit a companion * `_formatted` string so the LLM can narrate the human value safely. */ kind?: FieldKind; } /** * Which parts of a `chat.writable` entity the operator's write tools may set. * Declared by the host app on the descriptor, surfaced to the model through * `describe_entity`, enforced by the write tools before any approval is asked. */ export interface ChatWritableConfig { /** Field names the assistant may write on create and update. Every other field is refused. */ fields: string[]; /** Relationship keys the assistant may set on create and through link/unlink. Omitted: none. */ relationships?: string[]; } /** * Function signature for computed field calculation * @param params.data - The raw Neo4j node data * @param params.record - The full Neo4j record (for accessing related data like totalScore) * @param params.entityFactory - EntityFactory instance for creating related entities * @param params.name - Optional name for the entity in the record */ export type ComputedFieldFn = (params: { data: any; record: any; entityFactory: any; name?: string; }) => T; /** * Computed field definition for runtime-calculated values * These fields don't exist in Neo4j but are calculated from record data */ export interface ComputedFieldDef { /** Function to compute the field value */ compute: ComputedFieldFn; /** If true, field goes to JSON:API meta instead of attributes (default: false) */ meta?: boolean; /** If true, field is excluded from JSONAPI serialization entirely (default: false) */ excludeFromJsonApi?: boolean; /** Human-readable description. Required for the field to be visible to the chatbot. */ description?: string; } /** * Virtual field definition for output-only computed values. * Unlike computed fields, virtual fields can have arbitrary names that * don't exist in the entity type. They appear in JSON:API attributes by default. * * Example: avatarUrl returns the raw S3 key while avatar returns the signed URL. */ export interface VirtualFieldDef { /** Function to compute the field value */ compute: ComputedFieldFn; /** If true, field goes to JSON:API meta instead of attributes (default: false) */ meta?: boolean; /** If true, field is excluded from JSONAPI serialization entirely (default: false) */ excludeFromJsonApi?: boolean; /** Human-readable description. Required for the field to be visible to the chatbot. */ description?: string; } /** * Field definition for relationship properties * These are stored on the Neo4j relationship (edge), not on nodes */ export interface RelationshipFieldDef { /** Field name */ name: string; /** Neo4j/Cypher data type */ type: CypherType; /** Whether the field is required (default: false) */ required?: boolean; /** Default value */ default?: any; } /** * Relationship definition for entity schema * Defines how this entity relates to another entity */ export interface RelationshipDef { /** Metadata of the related entity */ model: DataMeta; /** Direction: 'in' = (related)-[:REL]->(this), 'out' = (this)-[:REL]->(related) */ direction: "in" | "out"; /** Neo4j relationship type (e.g., "PUBLISHED", "RELEVANT_FOR") */ relationship: string; /** Cardinality: 'one' for single, 'many' for collection */ cardinality: "one" | "many"; /** Whether the relationship is required. If false, uses OPTIONAL MATCH in queries */ required?: boolean; /** Context key for relationships whose value comes from CLS context (e.g., 'userId' for author) */ contextKey?: string; /** DTO key override for the relationships object (e.g., 'topics' instead of relationship key 'topic') */ dtoKey?: string; /** Fields stored on the relationship (edge properties). Only supported for cardinality: 'one' */ fields?: RelationshipFieldDef[]; /** For polymorphic relationships - multiple possible types for same Neo4j label */ polymorphic?: PolymorphicConfig; /** If true, relationship is set only on creation and skipped during PUT (default: false) */ immutable?: boolean; /** If true, the relationship is never written by generic descriptor-driven paths (create/put/patch skip it; relationship handlers reject it). Serialisation-only. */ readOnly?: boolean; /** Human-readable description. Required for the relationship to be visible to the chatbot. */ description?: string; /** Opts in reverse traversal from the target entity. Omit to keep one-way. */ reverse?: { name: string; description: string; }; /** * Dot-paths, relative to THIS relationship's TARGET descriptor, to also fetch * and nest in the response. Each segment must be a relationship key on the * descriptor at that level. Finite by construction → no cycle risk. * include: ["npc"] // 3rd level: turn.npc * include: ["npc", "user"] // turn.npc + turn.user * include: ["npc.faction"] // 4th level: turn.npc.faction * Not allowed together with edge `fields` on the same relationship (v1). */ include?: string[]; } /** * Data available to polymorphic discriminator function */ export interface PolymorphicDiscriminatorData { /** Entity properties from Neo4j */ properties: Record; /** Neo4j labels on the node */ labels: string[]; /** Whether node has outgoing SPECIALISES relationship (for taxonomy case) */ hasParent?: boolean; } /** * Configuration for polymorphic relationships where multiple entity types * share the same Neo4j label but have different JSON:API types */ export interface PolymorphicConfig { /** List of candidate models that could match this relationship */ candidates: DataMeta[]; /** Function that determines the correct model based on node data */ discriminator: (data: PolymorphicDiscriminatorData) => DataMeta; /** Relationship type to check for discriminator (e.g., "SPECIALISES") */ discriminatorRelationship?: string; /** Direction of discriminator relationship check: "out" = node has outgoing */ discriminatorDirection?: "in" | "out"; /** When true, also fetch the target of discriminatorRelationship for inclusion */ includeDiscriminatorTarget?: boolean; /** Property name to assign the discriminator target to (e.g., "taxonomy" for LeafTaxonomy's parent) */ discriminatorTargetProperty?: string; } /** * Input schema for defineEntity function * This is the clean, declarative API for defining entities * * @template T - The entity type (e.g., Glossary) * @template R - The relationships record type for autocomplete support */ export interface EntitySchemaInput = Record> { /** JSON:API type (e.g., "glossaries") */ type: string; /** API endpoint path (e.g., "glossaries") */ endpoint: string; /** Neo4j node name/alias used in Cypher queries (e.g., "glossary") */ nodeName: string; /** Neo4j label name (e.g., "Glossary") */ labelName: string; /** Whether this entity belongs to a company (default: true). Set to false for generic/global entities. */ isCompanyScoped?: boolean; /** Field definitions - keys must be valid entity properties */ fields: { [K in keyof Partial]?: FieldDef; }; /** Relationship definitions with named keys for autocomplete */ relationships: R; /** Computed fields - calculated at runtime from Neo4j record data. Keys must be valid entity properties. */ computed?: { [K in keyof Partial]?: ComputedFieldDef; }; /** Virtual fields - output-only computed values with arbitrary names (e.g., avatarUrl). * Unlike computed fields, virtual field keys don't need to exist in the entity type. */ virtualFields?: Record; /** Custom serializer class (only needed for special transformations like S3 URL signing) */ serialiser?: new (...args: any[]) => any; /** Services to inject into auto-generated serialiser for field transformers */ injectServices?: Type[]; /** Entity purpose for the chatbot. Required for the entity to be visible to the LLM. */ description?: string; /** Optional LLM-facing presentation hints. */ chat?: { /** One-line summary renderer used in chatbot search/traverse results. */ summary?: (data: any) => string; /** Described string fields used for the `text` parameter in `search_entities`. */ textSearchFields?: string[]; /** Stage-1 (list) field names, emitted in declaration order. Others go to availableOnRead. */ list?: string[]; /** * Campaign/tenant-style scoping. Either "self" (this entity IS the scope * root) or the key of a relationship on THIS descriptor pointing one hop * closer to the root. The catalog walks the chain at boot. */ scope?: string; /** * Opts this type into the operator's generic write tools. * `true` keeps the legacy meaning (every described field, every forward * non-polymorphic relationship except the scope one). A `ChatWritableConfig` * names exactly which fields and relationships the assistant may write; * everything else is refused before the user is asked to approve. */ writable?: boolean | ChatWritableConfig; /** Compile a polymorphic chat-only "related" traversal (RELATES_TO, both directions). */ related?: boolean; }; /** * Marks this entity as a bridge (junction) node. When a tool returns a record * of this type, the listed relationships are auto-fetched one hop and inlined * into the response, with a `__materialised` array listing what was filled. * Each name MUST be a key of `relationships` on this descriptor. The bridge * also requires a top-level `description`, otherwise it would be invisible * to the catalog. */ bridge?: { materialiseTo: string[]; }; } /** * Computed entity descriptor - output of defineEntity() * Contains both the original schema and computed/derived values * * @template T - The entity type * @template R - The relationships record type for autocomplete support */ export interface EntityDescriptor = Record> { /** The data model interface (auto-generated with mapper, childrenTokens, etc.) */ model: DataModelInterface; /** Whether this entity belongs to a company. False for generic/global entities. */ isCompanyScoped: boolean; /** Named relationships with autocomplete support (e.g., descriptor.relationships.author) */ relationships: R; /** Relationship keys for autocomplete (e.g., descriptor.relationshipKeys.author returns "author") */ relationshipKeys: { [K in keyof R]: K; }; /** All field names (keys of fields) */ fieldNames: string[]; /** Fields with type: 'string' - used for FULLTEXT index */ stringFields: string[]; /** Fields with required: true */ requiredFields: string[]; /** Default values extracted from field definitions */ fieldDefaults: Record; /** Field definitions map for type lookup */ fields: { [K in keyof Partial]?: FieldDef; }; /** Computed field definitions */ computed: { [K in keyof Partial]?: ComputedFieldDef; }; /** Virtual field definitions - output-only computed values with arbitrary names */ virtualFields: Record; /** Services to inject into auto-generated serialiser for field transformers */ injectServices: Type[]; /** Always: [{ property: 'id', type: 'UNIQUE' }] */ constraints: Array<{ property: string; type: "UNIQUE" | "EXISTS" | "NODE_KEY"; }>; /** Auto-generated FULLTEXT index from string fields */ indexes: Array<{ name: string; properties: string[]; type: "FULLTEXT" | "BTREE" | "TEXT"; }>; /** Auto-generated index name: {nodeName}_search_index */ fulltextIndexName: string; /** Default ordering for queries */ defaultOrderBy: string; /** Entity purpose for the chatbot. Required for the entity to be visible to the LLM. */ description?: string; /** Optional LLM-facing presentation hints. */ chat?: { /** One-line summary renderer used in chatbot search/traverse results. */ summary?: (data: any) => string; /** Described string fields used for the `text` parameter in `search_entities`. */ textSearchFields?: string[]; /** Stage-1 (list) field names, emitted in declaration order. Others go to availableOnRead. */ list?: string[]; /** * Campaign/tenant-style scoping. Either "self" (this entity IS the scope * root) or the key of a relationship on THIS descriptor pointing one hop * closer to the root. The catalog walks the chain at boot. */ scope?: string; /** * Opts this type into the operator's generic write tools. * `true` keeps the legacy meaning (every described field, every forward * non-polymorphic relationship except the scope one). A `ChatWritableConfig` * names exactly which fields and relationships the assistant may write; * everything else is refused before the user is asked to approve. */ writable?: boolean | ChatWritableConfig; /** Compile a polymorphic chat-only "related" traversal (RELATES_TO, both directions). */ related?: boolean; }; /** See EntitySchemaInput.bridge. */ bridge?: { materialiseTo: string[]; }; } //# sourceMappingURL=entity.schema.interface.d.ts.map