fraiseql
v2.8.0
Published
FraiseQL v2 - Compiled GraphQL execution engine (schema authoring + HTTP client)
Maintainers
Readme
FraiseQL v2 - TypeScript Schema Authoring
Compiled GraphQL execution engine - Schema authoring in TypeScript
FraiseQL v2 is a high-performance GraphQL engine that compiles schemas at build-time for zero-cost query execution. This package provides schema authoring in TypeScript that generates JSON schemas consumed by the Rust compiler.
Key Principle: TypeScript is for authoring only - no runtime FFI, no language bindings. Just pure JSON generation.
Architecture
TypeScript Code (decorators)
↓
schema.json
↓
fraiseql-cli compile
↓
schema.compiled.json
↓
Rust Runtime (fraiseql-server)Installation
npm install fraiseql
# or
yarn add fraiseql
# or
pnpm add fraiseqlRequirements: Node.js 18+
Quick Start
1. Define Types
import * as fraiseql from "fraiseql";
@fraiseql.type()
class User {
id!: number;
name!: string;
email!: string;
}
// Register fields (TypeScript doesn't preserve type info at runtime)
fraiseql.registerTypeFields("User", [
{ name: "id", type: "Int", nullable: false },
{ name: "name", type: "String", nullable: false },
{ name: "email", type: "String", nullable: false },
]);2. Define Queries
@fraiseql.query({ sqlSource: "v_user" })
function users(limit: number = 10, offset: number = 0): User[] {
throw new Error("Not executed");
}
fraiseql.registerQuery(
"users",
"User",
true, // returns list
false, // not nullable
[
{ name: "limit", type: "Int", nullable: false, default: 10 },
{ name: "offset", type: "Int", nullable: false, default: 0 },
],
"Get all users",
{ sql_source: "v_user" }
);3. Define Mutations
@fraiseql.mutation({ sqlSource: "fn_create_user", operation: "CREATE" })
function createUser(name: string, email: string): User {
throw new Error("Not executed");
}
fraiseql.registerMutation(
"createUser",
"User",
false, // single item
false, // not nullable
[
{ name: "name", type: "String", nullable: false },
{ name: "email", type: "String", nullable: false },
],
"Create a new user",
{ sql_source: "fn_create_user", operation: "CREATE" }
);4. Export Schema
// At end of file
if (require.main === module) {
fraiseql.exportSchema("schema.json");
}5. Compile
# Generate compiled schema
fraiseql-cli compile schema.json
# Start server
fraiseql-server --schema schema.compiled.json --port 3000API Reference
Decorators
@Type(config?)
Mark a class as a GraphQL type.
@fraiseql.type()
class User {
id!: number;
name!: string;
}Note: Decorators alone don't capture field types. Use registerTypeFields() to provide field metadata.
@Query(config)
Mark a function as a GraphQL query.
@fraiseql.query({ sqlSource: "v_user" })
function users(limit: number = 10): User[] {
throw new Error("Not executed");
}Config Options:
sqlSource: SQL view/table name (required for data operations)autoParams: Auto-parameter configuration- Other custom configuration
@Mutation(config)
Mark a function as a GraphQL mutation.
@fraiseql.mutation({ sqlSource: "fn_create_user", operation: "CREATE" })
function createUser(name: string): User {
throw new Error("Not executed");
}Config Options:
sqlSource: SQL function name (required)operation: "CREATE" | "UPDATE" | "DELETE" | "CUSTOM"- Other custom configuration
@FactTable(config)
Mark a class as a fact table for analytics.
@fraiseql.FactTable({
tableName: "tf_sales",
measures: ["revenue", "quantity"],
dimensionPaths: [
{
name: "category",
json_path: "data->>'category'",
data_type: "text",
},
],
})
@fraiseql.type()
class Sale {
id!: number;
revenue!: number;
quantity!: number;
}@AggregateQuery(config)
Mark a function as an aggregate query on a fact table.
@fraiseql.AggregateQuery({
factTable: "tf_sales",
autoGroupBy: true,
autoAggregates: true,
})
@fraiseql.query()
function salesAggregate(): Record<string, unknown>[] {
throw new Error("Not executed");
}@Subscription(config?)
Mark a function as a GraphQL subscription for real-time events.
Subscriptions in FraiseQL are compiled database event projections sourced from LISTEN/NOTIFY or CDC, not resolver-based.
@fraiseql.Subscription({ topic: "order_events" })
function orderCreated(userId?: string): Order {
pass;
}Subscription Configuration
SubscriptionConfig Options:
entityType: Entity type being subscribed to (defaults to return type)topic: Optional topic/channel name for filtering eventsoperation: Single event type filter - "CREATE" | "UPDATE" | "DELETE"operations: Multiple event type filters - ["CREATE", "UPDATE", "DELETE"]
Manual Registration:
fraiseql.registerSubscription(
"orderCreated", // name
"Order", // entityType
false, // nullable
[
{ name: "userId", type: "String", nullable: true }
], // filter arguments
"Subscribe to new orders",
{ topic: "order_events", operation: "CREATE" }
);Subscription Patterns:
- Event Type Filtering - Subscribe to specific operations
fraiseql.registerSubscription(
"userCreated",
"User",
false,
[],
"New user registrations",
{ operation: "CREATE" } // Only CREATE events
);- Topic-Based Subscriptions - Route to different channels
fraiseql.registerSubscription(
"criticalOrders",
"Order",
false,
[],
"High-priority orders",
{ topic: "orders.critical", operation: "CREATE" }
);- Filtered Subscriptions - Target specific records
fraiseql.registerSubscription(
"customerOrders",
"Order",
false,
[{ name: "customerId", type: "ID", nullable: false }], // Filter by customer
"Orders for specific customer"
);- Change Data Capture (CDC) - Capture all changes
fraiseql.registerSubscription(
"userCDC",
"User",
false,
[],
"All user changes",
{ operations: ["CREATE", "UPDATE", "DELETE"] }
);- Alerts and Notifications - Complex filtering
fraiseql.registerSubscription(
"unusualOrders",
"Order",
false,
[
{ name: "minAmount", type: "Decimal", nullable: false },
{ name: "timeWindowMinutes", type: "Int", nullable: true }
],
"Alert on high-value orders",
{ operation: "CREATE" }
);Type System Decorators
enum_(name, values, config?)
Define a GraphQL enum type.
const OrderStatus = fraiseql.enum_("OrderStatus", {
PENDING: "pending",
SHIPPED: "shipped",
DELIVERED: "delivered",
}, {
description: "Status of an order"
});Then use in types:
fraiseql.registerTypeFields("Order", [
{ name: "id", type: "ID", nullable: false },
{ name: "status", type: "OrderStatus", nullable: false },
]);interface_(name, fields, config?)
Define a GraphQL interface - shared fields for multiple types.
const Node = fraiseql.interface_("Node", [
{ name: "id", type: "ID", nullable: false },
{ name: "createdAt", type: "DateTime", nullable: false },
], {
description: "An object with a globally unique ID"
});Types can implement interfaces:
fraiseql.registerTypeFields("User", [
{ name: "id", type: "ID", nullable: false },
{ name: "createdAt", type: "DateTime", nullable: false },
{ name: "name", type: "String", nullable: false },
]);union(name, memberTypes, config?)
Define a GraphQL union - polymorphic return type.
const SearchResult = fraiseql.union("SearchResult",
["User", "Post", "Comment"],
{ description: "Result of a search query" }
);Then use in queries:
fraiseql.registerQuery(
"search",
"SearchResult", // Returns union
true, // returns list
false, // not nullable
[{ name: "query", type: "String", nullable: false }],
"Search across content"
);input(name, fields, config?)
Define a GraphQL input type - structured parameters.
const CreateUserInput = fraiseql.input("CreateUserInput", [
{ name: "email", type: "Email", nullable: false },
{ name: "name", type: "String", nullable: false },
{ name: "role", type: "String", nullable: false, default: "user" },
], {
description: "Input for creating a new user"
});Use in mutations:
fraiseql.registerMutation(
"createUser",
"User",
false,
false,
[{ name: "input", type: "CreateUserInput", nullable: false }],
"Create a new user"
);Field-Level Metadata
Add access control, deprecation markers, and documentation to individual fields:
field(options)
Create field metadata for use with registerTypeFields():
fraiseql.registerTypeFields("User", [
{ name: "id", type: "ID", nullable: false },
{
name: "salary",
type: "Decimal",
nullable: false,
requiresScope: "read:User.salary",
description: "Annual salary (requires HR scope)"
},
{
name: "oldEmail",
type: "String",
nullable: true,
deprecated: "Use email instead",
description: "Legacy email field (deprecated)"
}
]);Field Metadata Options:
requiresScope: string | string[]- JWT scope(s) required to access this field (field-level access control)deprecated: boolean | string- Mark field as deprecated. Pass a string with migration guidance.description: string- Field documentation (appears in GraphQL schema)
Use Cases:
- PII Protection: Require specific scopes for sensitive fields
{
name: "ssn",
type: "String",
nullable: false,
requiresScope: "pii:read" // Only users with pii:read scope can query this
}- API Versioning: Deprecate fields with migration guidance
{
name: "oldPrice",
type: "Decimal",
nullable: true,
deprecated: "Use pricing.current instead - structure moved to pricing object"
}- Schema Documentation: Add rich field descriptions
{
name: "discount",
type: "Decimal",
nullable: false,
description: "Discount percentage. Access requires orders:view_discounts scope.",
requiresScope: "orders:view_discounts"
}Manual Registration Functions
When decorators alone don't provide enough type information:
registerTypeFields(typeName, fields, description?)
Register type field definitions.
fraiseql.registerTypeFields("User", [
{ name: "id", type: "Int", nullable: false },
{ name: "name", type: "String", nullable: false },
{ name: "email", type: "String", nullable: true },
]);registerQuery(name, returnType, returnsList, nullable, args, description?, config?)
Register a query with full metadata.
fraiseql.registerQuery(
"users",
"User",
true, // returns list
false, // not nullable
[
{ name: "limit", type: "Int", nullable: false, default: 10 },
],
"Get all users",
{ sql_source: "v_user" }
);registerMutation(name, returnType, returnsList, nullable, args, description?, config?)
Register a mutation with full metadata.
fraiseql.registerMutation(
"createUser",
"User",
false, // single item
false, // not nullable
[
{ name: "name", type: "String", nullable: false },
],
"Create a new user",
{ sql_source: "fn_create_user", operation: "CREATE" }
);Schema Export
exportSchema(outputPath, options?)
Export the schema to a JSON file.
fraiseql.exportSchema("schema.json", { pretty: true });getSchemaDict()
Get the schema as a JavaScript object.
const schema = fraiseql.getSchemaDict();
console.log(schema.types);
console.log(schema.queries);exportSchemaToString(options?)
Export schema to a JSON string.
const json = fraiseql.exportSchemaToString({ pretty: true });
console.log(json);Supported GraphQL Types
Scalars
Int- 32-bit integerFloat- Floating point numberString- Text stringBoolean- True/FalseID- Unique identifier
Modifiers
T[]- List type (maps to[T!]in GraphQL)T | null- Nullable typeT | undefined- Optional parameter
Type Mapping
TypeScript types are converted to GraphQL types:
// TypeScript → GraphQL
number → Float
string → String
boolean → Boolean
SomeClass → SomeClass (custom type)
T[] → [T!] (list)
T | null → T (nullable)
T | undefined → T (optional param)Analytics Features
Fact Tables
Fact tables are special analytics tables with:
- Measures: Numeric columns for aggregation (SUM, AVG, COUNT)
- Dimensions: JSONB column for flexible GROUP BY
- Denormalized Filters: Indexed columns for fast WHERE clauses
@fraiseql.FactTable({
tableName: "tf_sales", // Must start with "tf_"
measures: ["revenue", "cost"], // Numeric columns
dimensionPaths: [
{
name: "category",
json_path: "data->>'category'",
data_type: "text",
},
],
})
@fraiseql.type()
class Sale {
id!: number;
revenue!: number;
cost!: number;
customerId!: string;
}Aggregate Queries
Queries that perform GROUP BY aggregations on fact tables:
@fraiseql.AggregateQuery({
factTable: "tf_sales",
autoGroupBy: true, // Auto-generate groupBy fields
autoAggregates: true, // Auto-generate aggregate functions
})
@fraiseql.query()
function salesSummary(): Record<string, unknown>[] {
throw new Error("Not executed");
}These queries support:
groupBy: Dimensions and temporal bucketsaggregates: COUNT, SUM, AVG, MIN, MAXwhere: Pre-aggregation filtershaving: Post-aggregation filtersorderBy: Sort results- Pagination:
limit,offset
Examples
See the examples/ directory:
- basic_schema.ts - Simple CRUD queries and mutations
- analytics_schema.ts - Fact tables and aggregate queries
- enums-example.ts - Enum definitions and usage
- types-advanced.ts - Comprehensive type system example (enums, interfaces, unions, input types)
- unions-interfaces-example.ts - Interfaces, unions, and polymorphic queries
- field-metadata.ts - Field-level access control, deprecation, and documentation
- subscriptions.ts - Real-time subscriptions: event filtering, topics, CDC, alerts
- comprehensive-example.ts - Full-featured schema with all FraiseQL capabilities
Run examples:
npm run example:basic # Generate basic schema
npm run example:analytics # Generate analytics schema
npm run example:enums # Generate enum example
npm run example:advanced # Generate advanced types example
npm run example:metadata # Generate field metadata example
npm run example:subscriptions # Generate subscriptions exampleDevelopment
# Install dependencies
npm install
# Build
npm run build
# Run tests
npm test
# Watch mode
npm run test:watch
# Lint
npm run lint
# Format code
npm run formatTesting
Tests verify:
- Type introspection and conversion
- Schema registration and retrieval
- Decorator functionality
- Schema JSON generation
- Analytics fact tables and aggregate queries
npm testTroubleshooting
Issue: "Field type information not available"
Cause: TypeScript doesn't preserve type information at runtime by default.
Solution: Use registerTypeFields() or registerQuery()/registerMutation() with explicit type metadata.
// Instead of relying on decorators alone:
fraiseql.registerTypeFields("User", [
{ name: "id", type: "Int", nullable: false },
// ... other fields
]);Issue: "Factory not started: fraiseql-cli not found"
Solution: Install the CLI tool:
# Global installation
npm install -g fraiseql-cli
# Or use local version
npx fraiseql-cli compile schema.jsonPerformance
- Compile-time: Negligible (< 100ms for typical schemas)
- Runtime: Zero overhead - SQL is compiled, not interpreted
- Schema generation: Fast JSON serialization
Architecture Notes
No Runtime FFI
This package generates JSON only. There's no FFI, no native bindings, no runtime dependencies on the Rust engine.
The workflow is:
- Write TypeScript with decorators
- Run
exportSchema()to generateschema.json - Compile with
fraiseql-clito getschema.compiled.json - Deploy compiled schema to Rust runtime
Why Manual Field Registration?
TypeScript's decorator system doesn't preserve generic type parameters at runtime. To provide full type information, we require explicit field registration. This is a limitation of the language, not the framework.
Future versions may use TypeScript 5.2+ metadata if decorators mature in the standard.
License
MIT
Support
- Documentation: https://docs.fraiseql.io
- Issues: https://github.com/fraiseql/fraiseql/issues
- Examples: See
examples/directory
Contributing
Contributions welcome! Please follow the contribution guidelines in the main repository.
Remember: FraiseQL TypeScript is for authoring only. Runtime execution happens in the Rust engine.
