Add QueryBuilder documentation and video tutorials

- Created a new documentation file for the QueryBuilder class, detailing its methods and usage examples.
- Added a comprehensive video tutorials index, covering various aspects of the Debros Network framework.
- Updated the sidebar configuration to include the new video tutorials section and reorganized the API documentation categories.
This commit is contained in:
anonpenguin 2025-07-05 06:58:55 +03:00
parent 8cd23b433c
commit c7babf9aea
5 changed files with 2353 additions and 2 deletions

View File

@ -2,8 +2,14 @@
"src/**/*.{js,ts}": [ "src/**/*.{js,ts}": [
"prettier --write", "prettier --write",
"eslint --fix", "eslint --fix",
"pnpm run test:unit",
"npm run build" "npm run build"
], ],
"tests/**/*.{js,ts}": [
"prettier --write",
"eslint --fix",
"pnpm run test:unit"
],
"*.{json,md}": [ "*.{json,md}": [
"prettier --write" "prettier --write"
] ]

810
docs/docs/api/base-model.md Normal file
View File

@ -0,0 +1,810 @@
---
sidebar_position: 3
---
# BaseModel Class
The `BaseModel` class is the abstract base class for all data models in Debros Network. It provides ORM-like functionality with automatic database management, validation, relationships, and lifecycle hooks.
## Class Definition
```typescript
abstract class BaseModel {
// Instance properties
id: string;
createdAt?: number;
updatedAt?: number;
// Static methods
static async create<T extends BaseModel>(
this: ModelConstructor<T>,
data: Partial<T>,
options?: CreateOptions,
): Promise<T>;
static async findById<T extends BaseModel>(
this: ModelConstructor<T>,
id: string,
options?: FindOptions,
): Promise<T | null>;
static async findOne<T extends BaseModel>(
this: ModelConstructor<T>,
criteria: Partial<T>,
options?: FindOptions,
): Promise<T | null>;
static query<T extends BaseModel>(this: ModelConstructor<T>): QueryBuilder<T>;
// Instance methods
save(options?: SaveOptions): Promise<this>;
delete(options?: DeleteOptions): Promise<boolean>;
reload(options?: ReloadOptions): Promise<this>;
validate(): Promise<ValidationResult>;
toJSON(): Record<string, any>;
clone(): this;
}
```
## Static Methods
### create(data, options?)
Creates a new model instance and saves it to the database.
**Parameters:**
- `data`: Partial model data
- `options` (optional): Creation options
**Returns:** `Promise<T>` - The created model instance
**Throws:**
- `ValidationError` - If validation fails
- `DatabaseError` - If database operation fails
**Example:**
```typescript
import { BaseModel, Model, Field } from '@debros/network';
@Model({
scope: 'global',
type: 'docstore',
})
class User extends BaseModel {
@Field({ type: 'string', required: true, unique: true })
username: string;
@Field({ type: 'string', required: true, unique: true })
email: string;
@Field({ type: 'number', required: false, default: 0 })
score: number;
}
// Create a new user
const user = await User.create({
username: 'alice',
email: 'alice@example.com',
score: 100,
});
console.log(user.id); // Generated ID
console.log(user.username); // 'alice'
console.log(user.createdAt); // Timestamp
```
**With validation:**
```typescript
try {
const user = await User.create({
username: 'ab', // Too short
email: 'invalid-email', // Invalid format
});
} catch (error) {
if (error instanceof ValidationError) {
console.log('Validation failed:', error.field, error.constraint);
}
}
```
**With options:**
```typescript
const user = await User.create(
{
username: 'bob',
email: 'bob@example.com',
},
{
validate: true,
skipHooks: false,
userId: 'user123', // For user-scoped models
},
);
```
### findById(id, options?)
Finds a model instance by its ID.
**Parameters:**
- `id`: The model ID to search for
- `options` (optional): Find options
**Returns:** `Promise<T | null>` - The found model or null
**Example:**
```typescript
// Basic find
const user = await User.findById('user123');
if (user) {
console.log('Found user:', user.username);
} else {
console.log('User not found');
}
// With relationships
const user = await User.findById('user123', {
with: ['posts', 'profile'],
});
// With specific user context
const user = await User.findById('user123', {
userId: 'current-user-id',
});
```
### findOne(criteria, options?)
Finds the first model instance matching the criteria.
**Parameters:**
- `criteria`: Partial model data to match
- `options` (optional): Find options
**Returns:** `Promise<T | null>` - The found model or null
**Example:**
```typescript
// Find by field
const user = await User.findOne({
username: 'alice',
});
// Find with multiple criteria
const user = await User.findOne({
email: 'alice@example.com',
isActive: true,
});
// With options
const user = await User.findOne(
{
username: 'alice',
},
{
with: ['posts'],
cache: 300, // Cache for 5 minutes
},
);
```
### query()
Returns a query builder for complex queries.
**Returns:** `QueryBuilder<T>` - Query builder instance
**Example:**
```typescript
// Basic query
const users = await User.query()
.where('isActive', true)
.orderBy('createdAt', 'desc')
.limit(10)
.find();
// Complex query with relationships
const activeUsers = await User.query()
.where('isActive', true)
.where('score', '>', 100)
.where('registeredAt', '>', Date.now() - 30 * 24 * 60 * 60 * 1000)
.with(['posts.comments', 'profile'])
.orderBy('score', 'desc')
.paginate(1, 20);
// Query with caching
const cachedUsers = await User.query()
.where('isActive', true)
.cache(600) // Cache for 10 minutes
.find();
```
## Instance Methods
### save(options?)
Saves the current model instance to the database.
**Parameters:**
- `options` (optional): Save options
**Returns:** `Promise<this>` - The saved model instance
**Example:**
```typescript
const user = await User.findById('user123');
user.email = 'newemail@example.com';
user.score += 10;
await user.save();
console.log('User updated');
// With options
await user.save({
validate: true,
skipHooks: false,
});
```
### delete(options?)
Deletes the model instance from the database.
**Parameters:**
- `options` (optional): Delete options
**Returns:** `Promise<boolean>` - True if deletion was successful
**Example:**
```typescript
const user = await User.findById('user123');
if (user) {
const deleted = await user.delete();
console.log('User deleted:', deleted);
}
// With options
await user.delete({
skipHooks: false,
cascade: true, // Delete related records
});
```
### reload(options?)
Reloads the model instance from the database.
**Parameters:**
- `options` (optional): Reload options
**Returns:** `Promise<this>` - The reloaded model instance
**Example:**
```typescript
const user = await User.findById('user123');
// Model might be updated elsewhere
await user.reload();
// With relationships
await user.reload({
with: ['posts', 'profile'],
});
```
### validate()
Validates the current model instance.
**Returns:** `Promise<ValidationResult>` - Validation result
**Example:**
```typescript
const user = new User();
user.username = 'alice';
user.email = 'invalid-email';
const result = await user.validate();
if (!result.valid) {
console.log('Validation errors:', result.errors);
// [{ field: 'email', constraint: 'must be valid email', value: 'invalid-email' }]
}
```
### toJSON()
Converts the model instance to a plain JavaScript object.
**Returns:** `Record<string, any>` - Plain object representation
**Example:**
```typescript
const user = await User.findById('user123');
const userObj = user.toJSON();
console.log(userObj);
// {
// id: 'user123',
// username: 'alice',
// email: 'alice@example.com',
// score: 100,
// createdAt: 1234567890,
// updatedAt: 1234567890
// }
// Useful for JSON serialization
const jsonString = JSON.stringify(user); // Calls toJSON() automatically
```
### clone()
Creates a deep copy of the model instance (without ID).
**Returns:** `this` - Cloned model instance
**Example:**
```typescript
const user = await User.findById('user123');
const userCopy = user.clone();
userCopy.username = 'alice_copy';
await userCopy.save(); // Creates new record
console.log(user.id !== userCopy.id); // true
```
## Lifecycle Hooks
### Hook Decorators
Models can define lifecycle hooks using decorators:
```typescript
@Model({ scope: 'global', type: 'docstore' })
class User extends BaseModel {
@Field({ type: 'string', required: true })
username: string;
@Field({ type: 'string', required: true })
email: string;
@Field({ type: 'number', required: false })
loginCount: number = 0;
@BeforeCreate()
async beforeCreateHook() {
this.createdAt = Date.now();
this.updatedAt = Date.now();
// Validate unique username
const existing = await User.findOne({ username: this.username });
if (existing) {
throw new ValidationError('username', this.username, 'must be unique');
}
}
@AfterCreate()
async afterCreateHook() {
console.log(`New user created: ${this.username}`);
// Send welcome email
await this.sendWelcomeEmail();
// Create default settings
await this.createDefaultSettings();
}
@BeforeUpdate()
async beforeUpdateHook() {
this.updatedAt = Date.now();
}
@AfterUpdate()
async afterUpdateHook() {
console.log(`User updated: ${this.username}`);
}
@BeforeDelete()
async beforeDeleteHook() {
// Clean up related data
await this.deleteRelatedPosts();
}
@AfterDelete()
async afterDeleteHook() {
console.log(`User deleted: ${this.username}`);
}
// Custom methods
private async sendWelcomeEmail() {
// Implementation
}
private async createDefaultSettings() {
// Implementation
}
private async deleteRelatedPosts() {
// Implementation
}
}
```
### Available Hook Types
| Hook | Trigger | Usage |
| ----------------- | ------------------------------- | --------------------------------------- |
| `@BeforeCreate()` | Before creating new record | Validation, default values, preparation |
| `@AfterCreate()` | After creating new record | Notifications, related record creation |
| `@BeforeUpdate()` | Before updating existing record | Validation, timestamp updates, logging |
| `@AfterUpdate()` | After updating existing record | Cache invalidation, notifications |
| `@BeforeDelete()` | Before deleting record | Cleanup, validation, logging |
| `@AfterDelete()` | After deleting record | Cleanup, notifications, cascade deletes |
| `@BeforeSave()` | Before save (create or update) | Common validation, timestamps |
| `@AfterSave()` | After save (create or update) | Common post-processing |
## Validation
### Field Validation
```typescript
@Model({ scope: 'global', type: 'docstore' })
class User extends BaseModel {
@Field({
type: 'string',
required: true,
unique: true,
minLength: 3,
maxLength: 20,
validate: (username: string) => /^[a-zA-Z0-9_]+$/.test(username),
transform: (username: string) => username.toLowerCase(),
})
username: string;
@Field({
type: 'string',
required: true,
unique: true,
validate: (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email),
transform: (email: string) => email.toLowerCase(),
})
email: string;
@Field({
type: 'number',
required: false,
default: 0,
validate: (score: number) => score >= 0 && score <= 1000,
})
score: number;
@Field({
type: 'array',
required: false,
default: [],
validate: (tags: string[]) => tags.length <= 10,
})
tags: string[];
}
```
### Custom Validation Methods
```typescript
@Model({ scope: 'global', type: 'docstore' })
class User extends BaseModel {
@Field({ type: 'string', required: true })
username: string;
@Field({ type: 'string', required: true })
email: string;
@Field({ type: 'string', required: true })
password: string;
// Custom validation method
async customValidation(): Promise<ValidationResult> {
const errors: ValidationError[] = [];
// Check username availability
if (this.username) {
const existing = await User.findOne({ username: this.username });
if (existing && existing.id !== this.id) {
errors.push(new ValidationError('username', this.username, 'already taken'));
}
}
// Check email format and availability
if (this.email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(this.email)) {
errors.push(new ValidationError('email', this.email, 'invalid format'));
}
const existing = await User.findOne({ email: this.email });
if (existing && existing.id !== this.id) {
errors.push(new ValidationError('email', this.email, 'already registered'));
}
}
// Check password strength
if (this.password && this.password.length < 8) {
errors.push(new ValidationError('password', this.password, 'must be at least 8 characters'));
}
return {
valid: errors.length === 0,
errors,
};
}
@BeforeCreate()
@BeforeUpdate()
async validateBeforeSave() {
const result = await this.customValidation();
if (!result.valid) {
throw new ValidationError('model', this, 'custom validation failed', result.errors);
}
}
}
```
## Configuration Interfaces
### ModelConstructor Type
```typescript
type ModelConstructor<T extends BaseModel> = new () => T;
```
### Options Interfaces
```typescript
interface CreateOptions {
validate?: boolean;
skipHooks?: boolean;
userId?: string; // For user-scoped models
}
interface FindOptions {
with?: string[]; // Relationship names to eager load
cache?: boolean | number; // Cache result
userId?: string; // For user-scoped models
}
interface SaveOptions {
validate?: boolean;
skipHooks?: boolean;
upsert?: boolean;
}
interface DeleteOptions {
skipHooks?: boolean;
cascade?: boolean; // Delete related records
}
interface ReloadOptions {
with?: string[]; // Relationship names to eager load
}
```
### Validation Types
```typescript
interface ValidationResult {
valid: boolean;
errors: ValidationError[];
}
class ValidationError extends Error {
constructor(
public field: string,
public value: any,
public constraint: string,
public details?: any,
) {
super(`Validation failed for field '${field}': ${constraint}`);
}
}
```
## Error Handling
### Common Errors
```typescript
// Validation errors
try {
const user = await User.create({
username: 'a', // Too short
email: 'invalid',
});
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Field ${error.field} failed: ${error.constraint}`);
}
}
// Not found errors
const user = await User.findById('non-existent-id');
if (!user) {
throw new NotFoundError('User', 'non-existent-id');
}
// Database errors
try {
await user.save();
} catch (error) {
if (error instanceof DatabaseError) {
console.log('Database operation failed:', error.message);
}
}
```
## Complete Example
### Blog Post Model
```typescript
import {
BaseModel,
Model,
Field,
BelongsTo,
HasMany,
BeforeCreate,
AfterCreate,
} from '@debros/network';
import { User } from './User';
import { Comment } from './Comment';
@Model({
scope: 'user',
type: 'docstore',
sharding: {
strategy: 'user',
count: 2,
key: 'authorId',
},
})
export class Post extends BaseModel {
@Field({
type: 'string',
required: true,
minLength: 1,
maxLength: 200,
})
title: string;
@Field({
type: 'string',
required: true,
minLength: 1,
maxLength: 10000,
})
content: string;
@Field({ type: 'string', required: true })
authorId: string;
@Field({
type: 'array',
required: false,
default: [],
transform: (tags: string[]) => tags.map((tag) => tag.toLowerCase()),
})
tags: string[];
@Field({ type: 'boolean', required: false, default: false })
isPublished: boolean;
@Field({ type: 'number', required: false, default: 0 })
viewCount: number;
@Field({ type: 'number', required: false })
publishedAt?: number;
// Relationships
@BelongsTo(() => User, 'authorId')
author: User;
@HasMany(() => Comment, 'postId')
comments: Comment[];
@BeforeCreate()
setupNewPost() {
this.createdAt = Date.now();
this.updatedAt = Date.now();
this.viewCount = 0;
}
@AfterCreate()
async afterPostCreated() {
console.log(`New post created: ${this.title}`);
// Update author's post count
const author = await User.findById(this.authorId);
if (author) {
author.postCount = (author.postCount || 0) + 1;
await author.save();
}
}
// Custom methods
async publish(): Promise<void> {
this.isPublished = true;
this.publishedAt = Date.now();
await this.save();
}
async incrementViews(): Promise<void> {
this.viewCount += 1;
await this.save({ skipHooks: true }); // Skip hooks for performance
}
async getCommentCount(): Promise<number> {
return await Comment.query().where('postId', this.id).count();
}
async getTopComments(limit: number = 5): Promise<Comment[]> {
return await Comment.query()
.where('postId', this.id)
.orderBy('likeCount', 'desc')
.limit(limit)
.with(['author'])
.find();
}
}
// Usage examples
async function blogExamples() {
// Create a post
const post = await Post.create({
title: 'My First Post',
content: 'This is the content of my first post...',
authorId: 'user123',
tags: ['JavaScript', 'Web Development'],
});
// Find posts by author
const userPosts = await Post.query()
.where('authorId', 'user123')
.where('isPublished', true)
.orderBy('publishedAt', 'desc')
.with(['author', 'comments.author'])
.find();
// Publish a post
await post.publish();
// Get post with comments
const postWithComments = await Post.findById(post.id, {
with: ['author', 'comments.author'],
});
console.log('Post:', postWithComments?.title);
console.log('Author:', postWithComments?.author.username);
console.log('Comments:', postWithComments?.comments.length);
}
```
This comprehensive BaseModel documentation covers all the essential functionality for working with models in Debros Network, including CRUD operations, validation, relationships, hooks, and real-world usage examples.

View File

@ -0,0 +1,828 @@
---
sidebar_position: 4
---
# QueryBuilder Class
The `QueryBuilder` class provides a fluent API for constructing complex database queries. It supports filtering, sorting, relationships, pagination, and caching with type safety throughout.
## Class Definition
```typescript
class QueryBuilder<T extends BaseModel> {
// Filtering methods
where(field: string, value: any): QueryBuilder<T>;
where(field: string, operator: QueryOperator, value: any): QueryBuilder<T>;
whereIn(field: string, values: any[]): QueryBuilder<T>;
whereNotIn(field: string, values: any[]): QueryBuilder<T>;
whereNull(field: string): QueryBuilder<T>;
whereNotNull(field: string): QueryBuilder<T>;
whereBetween(field: string, min: any, max: any): QueryBuilder<T>;
whereRaw(condition: string, parameters?: any[]): QueryBuilder<T>;
// Logical operators
and(): QueryBuilder<T>;
or(): QueryBuilder<T>;
not(): QueryBuilder<T>;
// Sorting
orderBy(field: string, direction?: 'asc' | 'desc'): QueryBuilder<T>;
orderByRaw(orderClause: string): QueryBuilder<T>;
// Limiting and pagination
limit(count: number): QueryBuilder<T>;
offset(count: number): QueryBuilder<T>;
paginate(page: number, perPage: number): Promise<PaginatedResult<T>>;
// Relationships
with(relationships: string[]): QueryBuilder<T>;
withCount(relationships: string[]): QueryBuilder<T>;
whereHas(relationship: string, callback?: (query: QueryBuilder<any>) => void): QueryBuilder<T>;
whereDoesntHave(relationship: string): QueryBuilder<T>;
// Aggregation
count(): Promise<number>;
sum(field: string): Promise<number>;
avg(field: string): Promise<number>;
min(field: string): Promise<any>;
max(field: string): Promise<any>;
// Caching
cache(ttl?: number): QueryBuilder<T>;
fresh(): QueryBuilder<T>;
// Execution
find(): Promise<T[]>;
findOne(): Promise<T | null>;
first(): Promise<T | null>;
get(): Promise<T[]>;
// Advanced features
distinct(field?: string): QueryBuilder<T>;
groupBy(field: string): QueryBuilder<T>;
having(field: string, operator: QueryOperator, value: any): QueryBuilder<T>;
// Query info
toSQL(): string;
getParameters(): any[];
explain(): Promise<QueryPlan>;
}
```
## Filtering Methods
### where(field, value) / where(field, operator, value)
Adds a WHERE condition to the query.
**Parameters:**
- `field`: Field name to filter on
- `value`: Value to compare (when using equals operator)
- `operator`: Comparison operator
- `value`: Value to compare against
**Returns:** `QueryBuilder<T>` - Builder instance for chaining
**Example:**
```typescript
// Basic equality
const activeUsers = await User.query().where('isActive', true).find();
// With operators
const highScoreUsers = await User.query()
.where('score', '>', 1000)
.where('registeredAt', '>=', Date.now() - 30 * 24 * 60 * 60 * 1000)
.find();
// String operations
const usersWithAlice = await User.query().where('username', 'like', '%alice%').find();
// Array operations
const adminUsers = await User.query().where('roles', 'includes', 'admin').find();
```
### whereIn(field, values)
Filters records where field value is in the provided array.
**Parameters:**
- `field`: Field name
- `values`: Array of values to match
**Returns:** `QueryBuilder<T>`
**Example:**
```typescript
// Find users with specific IDs
const users = await User.query().whereIn('id', ['user1', 'user2', 'user3']).find();
// Find posts with specific tags
const posts = await Post.query().whereIn('category', ['tech', 'programming', 'tutorial']).find();
```
### whereNotIn(field, values)
Filters records where field value is NOT in the provided array.
**Parameters:**
- `field`: Field name
- `values`: Array of values to exclude
**Returns:** `QueryBuilder<T>`
**Example:**
```typescript
// Exclude specific users
const users = await User.query().whereNotIn('status', ['banned', 'suspended']).find();
```
### whereNull(field) / whereNotNull(field)
Filters records based on null values.
**Parameters:**
- `field`: Field name to check
**Returns:** `QueryBuilder<T>`
**Example:**
```typescript
// Find users without profile pictures
const usersWithoutAvatars = await User.query().whereNull('avatarUrl').find();
// Find users with profile pictures
const usersWithAvatars = await User.query().whereNotNull('avatarUrl').find();
```
### whereBetween(field, min, max)
Filters records where field value is between min and max.
**Parameters:**
- `field`: Field name
- `min`: Minimum value (inclusive)
- `max`: Maximum value (inclusive)
**Returns:** `QueryBuilder<T>`
**Example:**
```typescript
// Find users with scores between 100 and 500
const moderateUsers = await User.query().whereBetween('score', 100, 500).find();
// Find posts from last week
const lastWeek = Date.now() - 7 * 24 * 60 * 60 * 1000;
const recentPosts = await Post.query().whereBetween('createdAt', lastWeek, Date.now()).find();
```
### whereRaw(condition, parameters?)
Adds a raw WHERE condition.
**Parameters:**
- `condition`: Raw SQL-like condition string
- `parameters`: Optional parameters for the condition
**Returns:** `QueryBuilder<T>`
**Example:**
```typescript
// Complex condition
const users = await User.query()
.whereRaw('score > ? AND (registeredAt > ? OR isPremium = ?)', [100, lastWeek, true])
.find();
```
## Logical Operators
### and() / or() / not()
Combines conditions with logical operators.
**Returns:** `QueryBuilder<T>`
**Example:**
```typescript
// AND (default behavior)
const premiumActiveUsers = await User.query()
.where('isActive', true)
.and()
.where('isPremium', true)
.find();
// OR condition
const eligibleUsers = await User.query()
.where('isPremium', true)
.or()
.where('score', '>', 1000)
.find();
// NOT condition
const nonAdminUsers = await User.query().not().where('role', 'admin').find();
// Complex combinations
const complexQuery = await User.query()
.where('isActive', true)
.and()
.group((query) => query.where('isPremium', true).or().where('score', '>', 500))
.find();
```
## Sorting Methods
### orderBy(field, direction?)
Orders results by specified field.
**Parameters:**
- `field`: Field name to sort by
- `direction`: Sort direction ('asc' or 'desc', defaults to 'asc')
**Returns:** `QueryBuilder<T>`
**Example:**
```typescript
// Sort by score descending
const topUsers = await User.query().orderBy('score', 'desc').limit(10).find();
// Multiple sort criteria
const users = await User.query().orderBy('score', 'desc').orderBy('username', 'asc').find();
// Sort by date
const recentPosts = await Post.query().orderBy('createdAt', 'desc').find();
```
### orderByRaw(orderClause)
Orders results using a raw ORDER BY clause.
**Parameters:**
- `orderClause`: Raw order clause
**Returns:** `QueryBuilder<T>`
**Example:**
```typescript
const users = await User.query().orderByRaw('score DESC, RANDOM()').find();
```
## Limiting and Pagination
### limit(count)
Limits the number of results.
**Parameters:**
- `count`: Maximum number of records to return
**Returns:** `QueryBuilder<T>`
**Example:**
```typescript
// Get top 10 users
const topUsers = await User.query().orderBy('score', 'desc').limit(10).find();
```
### offset(count)
Skips a number of records.
**Parameters:**
- `count`: Number of records to skip
**Returns:** `QueryBuilder<T>`
**Example:**
```typescript
// Get users 11-20 (skip first 10)
const nextBatch = await User.query().orderBy('score', 'desc').offset(10).limit(10).find();
```
### paginate(page, perPage)
Paginates results and returns pagination info.
**Parameters:**
- `page`: Page number (1-based)
- `perPage`: Number of records per page
**Returns:** `Promise<PaginatedResult<T>>`
**Example:**
```typescript
// Get page 2 with 20 users per page
const result = await User.query().where('isActive', true).orderBy('score', 'desc').paginate(2, 20);
console.log('Users:', result.data);
console.log('Total:', result.total);
console.log('Page:', result.page);
console.log('Per page:', result.perPage);
console.log('Total pages:', result.totalPages);
console.log('Has next:', result.hasNext);
console.log('Has previous:', result.hasPrev);
```
## Relationship Methods
### with(relationships)
Eager loads relationships.
**Parameters:**
- `relationships`: Array of relationship names or dot-notation for nested relationships
**Returns:** `QueryBuilder<T>`
**Example:**
```typescript
// Load single relationship
const usersWithPosts = await User.query().with(['posts']).find();
// Load multiple relationships
const usersWithData = await User.query().with(['posts', 'profile', 'followers']).find();
// Load nested relationships
const usersWithCommentsAuthors = await User.query().with(['posts.comments.author']).find();
// Mixed relationships
const complexLoad = await User.query()
.with(['posts.comments.author', 'profile', 'followers.profile'])
.find();
```
### withCount(relationships)
Loads relationship counts without loading the actual relationships.
**Parameters:**
- `relationships`: Array of relationship names
**Returns:** `QueryBuilder<T>`
**Example:**
```typescript
const usersWithCounts = await User.query().withCount(['posts', 'followers', 'following']).find();
// Access counts
usersWithCounts.forEach((user) => {
console.log(`${user.username}: ${user.postsCount} posts, ${user.followersCount} followers`);
});
```
### whereHas(relationship, callback?)
Filters records that have related records matching criteria.
**Parameters:**
- `relationship`: Relationship name
- `callback`: Optional callback to add conditions to the relationship query
**Returns:** `QueryBuilder<T>`
**Example:**
```typescript
// Users who have posts
const usersWithPosts = await User.query().whereHas('posts').find();
// Users who have published posts
const usersWithPublishedPosts = await User.query()
.whereHas('posts', (query) => {
query.where('isPublished', true);
})
.find();
// Users who have recent posts
const usersWithRecentPosts = await User.query()
.whereHas('posts', (query) => {
query.where('createdAt', '>', Date.now() - 7 * 24 * 60 * 60 * 1000);
})
.find();
```
### whereDoesntHave(relationship)
Filters records that don't have related records.
**Parameters:**
- `relationship`: Relationship name
**Returns:** `QueryBuilder<T>`
**Example:**
```typescript
// Users without posts
const usersWithoutPosts = await User.query().whereDoesntHave('posts').find();
```
## Aggregation Methods
### count()
Returns the count of matching records.
**Returns:** `Promise<number>`
**Example:**
```typescript
// Count all active users
const activeUserCount = await User.query().where('isActive', true).count();
console.log(`Active users: ${activeUserCount}`);
```
### sum(field) / avg(field) / min(field) / max(field)
Performs aggregation operations on a field.
**Parameters:**
- `field`: Field name to aggregate
**Returns:** `Promise<number>` or `Promise<any>` for min/max
**Example:**
```typescript
// Calculate statistics
const totalScore = await User.query().sum('score');
const averageScore = await User.query().avg('score');
const highestScore = await User.query().max('score');
const lowestScore = await User.query().min('score');
console.log(`Total: ${totalScore}, Average: ${averageScore}`);
console.log(`Range: ${lowestScore} - ${highestScore}`);
// With conditions
const premiumAverage = await User.query().where('isPremium', true).avg('score');
```
## Caching Methods
### cache(ttl?)
Caches query results.
**Parameters:**
- `ttl`: Time to live in seconds (optional, uses default if not provided)
**Returns:** `QueryBuilder<T>`
**Example:**
```typescript
// Cache for default duration
const cachedUsers = await User.query().where('isActive', true).cache().find();
// Cache for 10 minutes
const cachedPosts = await Post.query().where('isPublished', true).cache(600).find();
```
### fresh()
Forces a fresh query, bypassing cache.
**Returns:** `QueryBuilder<T>`
**Example:**
```typescript
// Always fetch fresh data
const freshUsers = await User.query().where('isActive', true).fresh().find();
```
## Execution Methods
### find()
Executes the query and returns all matching records.
**Returns:** `Promise<T[]>`
**Example:**
```typescript
const users = await User.query().where('isActive', true).orderBy('score', 'desc').find();
```
### findOne() / first()
Executes the query and returns the first matching record.
**Returns:** `Promise<T | null>`
**Example:**
```typescript
// Find the highest scoring user
const topUser = await User.query().orderBy('score', 'desc').findOne();
// Find specific user
const user = await User.query().where('username', 'alice').first();
```
### get()
Alias for `find()`.
**Returns:** `Promise<T[]>`
## Advanced Methods
### distinct(field?)
Returns distinct values.
**Parameters:**
- `field`: Optional field to get distinct values for
**Returns:** `QueryBuilder<T>`
**Example:**
```typescript
// Get users with distinct emails
const uniqueUsers = await User.query().distinct('email').find();
// Get all distinct tags from posts
const uniqueTags = await Post.query().distinct('tags').find();
```
### groupBy(field)
Groups results by field.
**Parameters:**
- `field`: Field to group by
**Returns:** `QueryBuilder<T>`
**Example:**
```typescript
// Group users by registration date
const usersByDate = await User.query().groupBy('registeredAt').withCount(['posts']).find();
```
### having(field, operator, value)
Adds HAVING condition (used with groupBy).
**Parameters:**
- `field`: Field name
- `operator`: Comparison operator
- `value`: Value to compare
**Returns:** `QueryBuilder<T>`
**Example:**
```typescript
// Find dates with more than 10 user registrations
const busyDays = await User.query().groupBy('registeredAt').having('COUNT(*)', '>', 10).find();
```
## Query Inspection
### toSQL()
Returns the SQL representation of the query.
**Returns:** `string`
**Example:**
```typescript
const query = User.query().where('isActive', true).orderBy('score', 'desc').limit(10);
console.log('SQL:', query.toSQL());
```
### getParameters()
Returns the parameters for the query.
**Returns:** `any[]`
**Example:**
```typescript
const query = User.query().where('score', '>', 100);
console.log('Parameters:', query.getParameters());
```
### explain()
Returns the query execution plan.
**Returns:** `Promise<QueryPlan>`
**Example:**
```typescript
const plan = await User.query().where('isActive', true).explain();
console.log('Query plan:', plan);
```
## Type Definitions
### QueryOperator
```typescript
type QueryOperator =
| 'eq'
| '='
| 'ne'
| '!='
| '<>'
| 'gt'
| '>'
| 'gte'
| '>='
| 'lt'
| '<'
| 'lte'
| '<='
| 'in'
| 'not in'
| 'like'
| 'not like'
| 'regex'
| 'is null'
| 'is not null'
| 'includes'
| 'includes any'
| 'includes all';
```
### PaginatedResult
```typescript
interface PaginatedResult<T> {
data: T[];
total: number;
page: number;
perPage: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
firstPage: number;
lastPage: number;
}
```
### QueryPlan
```typescript
interface QueryPlan {
estimatedCost: number;
estimatedRows: number;
indexesUsed: string[];
operations: QueryOperation[];
warnings: string[];
}
interface QueryOperation {
type: string;
description: string;
cost: number;
}
```
## Complex Query Examples
### Advanced Filtering
```typescript
// Complex user search
const searchResults = await User.query()
.where('isActive', true)
.and()
.group((query) =>
query
.where('username', 'like', `%${searchTerm}%`)
.or()
.where('email', 'like', `%${searchTerm}%`)
.or()
.where('bio', 'like', `%${searchTerm}%`),
)
.and()
.group((query) => query.where('isPremium', true).or().where('score', '>', 500))
.orderBy('score', 'desc')
.with(['profile', 'posts.comments'])
.paginate(page, 20);
```
### Analytics Query
```typescript
// User activity analytics
const analytics = await User.query()
.where('registeredAt', '>', startDate)
.where('registeredAt', '<', endDate)
.whereHas('posts', (query) => {
query.where('isPublished', true);
})
.withCount(['posts', 'comments', 'followers'])
.orderBy('score', 'desc')
.cache(300) // Cache for 5 minutes
.find();
// Process analytics data
const stats = {
totalUsers: analytics.length,
averageScore: analytics.reduce((sum, user) => sum + user.score, 0) / analytics.length,
totalPosts: analytics.reduce((sum, user) => sum + user.postsCount, 0),
totalComments: analytics.reduce((sum, user) => sum + user.commentsCount, 0),
};
```
### Dashboard Query
```typescript
async function getDashboardData(userId: string) {
// Multiple optimized queries
const [userStats, recentPosts, topComments, followingActivity] = await Promise.all([
// User statistics
User.query()
.where('id', userId)
.withCount(['posts', 'comments', 'followers', 'following'])
.cache(60) // Cache for 1 minute
.first(),
// Recent posts
Post.query()
.where('authorId', userId)
.orderBy('createdAt', 'desc')
.limit(5)
.with(['comments.author'])
.find(),
// Top comments by user
Comment.query()
.where('authorId', userId)
.orderBy('likeCount', 'desc')
.limit(10)
.with(['post.author'])
.find(),
// Activity from people user follows
Post.query()
.whereHas('author', (query) => {
query.whereHas('followers', (followQuery) => {
followQuery.where('followerId', userId);
});
})
.orderBy('createdAt', 'desc')
.limit(20)
.with(['author', 'comments'])
.find(),
]);
return {
userStats,
recentPosts,
topComments,
followingActivity,
};
}
```
The QueryBuilder provides a powerful, type-safe way to construct complex database queries with support for relationships, caching, pagination, and advanced filtering options.

691
docs/docs/videos/index.md Normal file
View File

@ -0,0 +1,691 @@
---
sidebar_position: 6
---
# Video Tutorials
Learn Debros Network through comprehensive video tutorials. These step-by-step guides will help you master the framework from basic concepts to advanced implementations.
## 🎬 **Getting Started Series**
### 1. Introduction to Debros Network
**Duration: 15 minutes**
A complete overview of Debros Network, its architecture, and core concepts. Perfect for developers new to decentralized applications.
**What You'll Learn:**
- Framework architecture and components
- Key differences from traditional frameworks
- When to use Debros Network
- Development environment overview
```typescript
// Code examples from this video
import { DebrosFramework, BaseModel, Model, Field } from '@debros/network';
@Model({
scope: 'global',
type: 'docstore',
})
class User extends BaseModel {
@Field({ type: 'string', required: true })
username: string;
}
```
[**▶️ Watch Introduction Video**](https://youtube.com/watch?v=VIDEO_ID_HERE)
---
### 2. Setting Up Your Development Environment
**Duration: 20 minutes**
Step-by-step guide to setting up Debros Network in your development environment.
**What You'll Learn:**
- Installing dependencies
- Project structure setup
- IDE configuration
- Development tools
**Prerequisites:**
- Node.js 18+
- npm or pnpm
- TypeScript knowledge
```bash
# Commands from this video
npm create debros-app my-app
cd my-app
npm install
npm run dev
```
[**▶️ Watch Setup Video**](https://youtube.com/watch?v=VIDEO_ID_HERE)
---
### 3. Your First Debros Application
**Duration: 25 minutes**
Build your first decentralized application with user management and basic CRUD operations.
**What You'll Learn:**
- Creating models with decorators
- Database initialization
- Basic CRUD operations
- Error handling
**Project Files:**
- `models/User.ts`
- `models/Post.ts`
- `app.ts`
```typescript
// Final code from this tutorial
@Model({
scope: 'global',
type: 'docstore',
sharding: { strategy: 'hash', count: 4, key: 'id' },
})
export class User extends BaseModel {
@Field({ type: 'string', required: true, unique: true })
username: string;
@Field({ type: 'string', required: true, unique: true })
email: string;
@HasMany(() => Post, 'authorId')
posts: Post[];
}
```
[**▶️ Watch First App Video**](https://youtube.com/watch?v=VIDEO_ID_HERE)
---
## 🏗️ **Core Concepts Series**
### 4. Understanding Models and Decorators
**Duration: 30 minutes**
Deep dive into the model system, decorators, and data validation.
**Topics Covered:**
- Model configuration options
- Field types and validation
- Custom validators
- Lifecycle hooks
- Best practices
```typescript
// Advanced model example from video
@Model({
scope: 'user',
type: 'docstore',
sharding: { strategy: 'user', count: 2, key: 'userId' },
})
export class BlogPost extends BaseModel {
@Field({
type: 'string',
required: true,
minLength: 5,
maxLength: 200,
validate: (title: string) => !title.includes('spam'),
})
title: string;
@BeforeCreate()
async validatePost() {
// Custom validation logic
}
}
```
[**▶️ Watch Models Deep Dive**](https://youtube.com/watch?v=VIDEO_ID_HERE)
---
### 5. Mastering the Query System
**Duration: 35 minutes**
Complete guide to building complex queries with relationships and optimization.
**Topics Covered:**
- Query builder API
- Filtering and sorting
- Relationship loading
- Pagination
- Query optimization
- Caching strategies
```typescript
// Complex query example from video
const results = await User.query()
.where('isActive', true)
.where('score', '>', 100)
.whereHas('posts', (query) => {
query.where('isPublished', true).where('createdAt', '>', Date.now() - 30 * 24 * 60 * 60 * 1000);
})
.with(['posts.comments.author', 'profile'])
.orderBy('score', 'desc')
.cache(300)
.paginate(1, 20);
```
[**▶️ Watch Query Mastery**](https://youtube.com/watch?v=VIDEO_ID_HERE)
---
### 6. Working with Relationships
**Duration: 25 minutes**
Understanding and implementing relationships between models.
**Topics Covered:**
- Relationship types (HasMany, BelongsTo, etc.)
- Eager vs lazy loading
- Nested relationships
- Performance considerations
```typescript
// Relationship examples from video
@Model({ scope: 'global', type: 'docstore' })
export class User extends BaseModel {
@HasMany(() => Post, 'authorId')
posts: Post[];
@ManyToMany(() => User, 'followers', 'following')
followers: User[];
@HasOne(() => UserProfile, 'userId')
profile: UserProfile;
}
```
[**▶️ Watch Relationships Video**](https://youtube.com/watch?v=VIDEO_ID_HERE)
---
## 🚀 **Advanced Features Series**
### 7. Database Sharding and Scaling
**Duration: 40 minutes**
Learn how to scale your application with automatic sharding strategies.
**Topics Covered:**
- Sharding strategies
- User-scoped vs global databases
- Performance optimization
- Monitoring and metrics
```typescript
// Sharding configuration examples
@Model({
scope: 'user',
type: 'docstore',
sharding: {
strategy: 'hash',
count: 8,
key: 'userId',
},
})
export class UserData extends BaseModel {
// Model implementation
}
```
[**▶️ Watch Sharding Guide**](https://youtube.com/watch?v=VIDEO_ID_HERE)
---
### 8. Migrations and Schema Evolution
**Duration: 30 minutes**
Managing database schema changes and data migrations.
**Topics Covered:**
- Creating migrations
- Data transformations
- Rollback strategies
- Production deployment
```typescript
// Migration example from video
const migration = createMigration('add_user_profiles', '1.1.0')
.addField('User', 'profilePicture', {
type: 'string',
required: false,
})
.addField('User', 'bio', {
type: 'string',
required: false,
})
.transformData('User', (user) => ({
...user,
displayName: user.username || 'Anonymous',
}))
.build();
```
[**▶️ Watch Migrations Video**](https://youtube.com/watch?v=VIDEO_ID_HERE)
---
### 9. Real-time Features and PubSub
**Duration: 25 minutes**
Implementing real-time functionality with the built-in PubSub system.
**Topics Covered:**
- Event publishing
- Real-time subscriptions
- WebSocket integration
- Performance considerations
```typescript
// Real-time examples from video
@Model({ scope: 'global', type: 'docstore' })
export class ChatMessage extends BaseModel {
@AfterCreate()
async publishMessage() {
await this.publish('message:created', {
roomId: this.roomId,
message: this,
});
}
}
```
[**▶️ Watch Real-time Video**](https://youtube.com/watch?v=VIDEO_ID_HERE)
---
## 🛠️ **Project Tutorials**
### 10. Building a Complete Blog Application
**Duration: 60 minutes**
Build a full-featured blog application with authentication, posts, and comments.
**Features Built:**
- User authentication
- Post creation and editing
- Comment system
- User profiles
- Admin dashboard
**Final Project Structure:**
```
blog-app/
├── models/
│ ├── User.ts
│ ├── Post.ts
│ ├── Comment.ts
│ └── Category.ts
├── services/
│ ├── AuthService.ts
│ └── BlogService.ts
└── app.ts
```
[**▶️ Watch Blog Tutorial**](https://youtube.com/watch?v=VIDEO_ID_HERE)
---
### 11. Creating a Social Media Platform
**Duration: 90 minutes**
Build a decentralized social media platform with advanced features.
**Features Built:**
- User profiles and following
- Feed generation
- Real-time messaging
- Content moderation
- Analytics dashboard
**Part 1: User System and Profiles** (30 min)
[**▶️ Watch Part 1**](https://youtube.com/watch?v=VIDEO_ID_HERE)
**Part 2: Posts and Feed** (30 min)
[**▶️ Watch Part 2**](https://youtube.com/watch?v=VIDEO_ID_HERE)
**Part 3: Real-time Features** (30 min)
[**▶️ Watch Part 3**](https://youtube.com/watch?v=VIDEO_ID_HERE)
---
### 12. E-commerce Platform with Debros Network
**Duration: 75 minutes**
Build a decentralized e-commerce platform with product management and orders.
**Features Built:**
- Product catalog
- Shopping cart
- Order processing
- Inventory management
- Payment integration
**Part 1: Product Management** (25 min)
[**▶️ Watch Part 1**](https://youtube.com/watch?v=VIDEO_ID_HERE)
**Part 2: Shopping and Orders** (25 min)
[**▶️ Watch Part 2**](https://youtube.com/watch?v=VIDEO_ID_HERE)
**Part 3: Advanced Features** (25 min)
[**▶️ Watch Part 3**](https://youtube.com/watch?v=VIDEO_ID_HERE)
---
## 🔧 **Development Workflow Series**
### 13. Testing Strategies for Debros Applications
**Duration: 35 minutes**
Comprehensive testing approaches for decentralized applications.
**Topics Covered:**
- Unit testing models
- Integration testing
- Mocking strategies
- Performance testing
```typescript
// Testing example from video
describe('User Model', () => {
it('should create user with valid data', async () => {
const user = await User.create({
username: 'testuser',
email: 'test@example.com',
});
expect(user.id).toBeDefined();
expect(user.username).toBe('testuser');
});
});
```
[**▶️ Watch Testing Video**](https://youtube.com/watch?v=VIDEO_ID_HERE)
---
### 14. Deployment and Production Best Practices
**Duration: 45 minutes**
Deploy your Debros Network applications to production environments.
**Topics Covered:**
- Production configuration
- Docker containers
- Monitoring and logging
- Performance optimization
- Security considerations
```dockerfile
# Docker example from video
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
```
[**▶️ Watch Deployment Video**](https://youtube.com/watch?v=VIDEO_ID_HERE)
---
### 15. Performance Optimization Techniques
**Duration: 40 minutes**
Advanced techniques for optimizing Debros Network applications.
**Topics Covered:**
- Query optimization
- Caching strategies
- Database indexing
- Memory management
- Profiling tools
```typescript
// Optimization examples from video
// Efficient query with proper indexing
const optimizedQuery = await User.query()
.where('isActive', true) // Indexed field
.with(['posts']) // Eager load to avoid N+1
.cache(300) // Cache frequently accessed data
.limit(50) // Reasonable limits
.find();
```
[**▶️ Watch Optimization Video**](https://youtube.com/watch?v=VIDEO_ID_HERE)
---
## 📱 **Integration Series**
### 16. Frontend Integration with React
**Duration: 50 minutes**
Integrate Debros Network with React applications for full-stack development.
**Topics Covered:**
- React hooks for Debros
- State management
- Real-time updates
- Error boundaries
```typescript
// React integration example from video
import { useDebrosQuery, useDebrosModel } from '@debros/react';
function UserProfile({ userId }: { userId: string }) {
const { user, loading, error } = useDebrosModel(User, userId, {
with: ['posts', 'profile']
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{user.username}</h1>
<p>Posts: {user.posts.length}</p>
</div>
);
}
```
[**▶️ Watch React Integration**](https://youtube.com/watch?v=VIDEO_ID_HERE)
---
### 17. Building APIs with Express and Debros
**Duration: 35 minutes**
Create REST APIs using Express.js with Debros Network as the backend.
**Topics Covered:**
- Express middleware
- API route design
- Authentication
- Error handling
- API documentation
```typescript
// Express API example from video
app.get('/api/users/:id', async (req, res) => {
try {
const user = await User.findById(req.params.id, {
with: ['posts'],
});
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
```
[**▶️ Watch Express Integration**](https://youtube.com/watch?v=VIDEO_ID_HERE)
---
### 18. Mobile Development with React Native
**Duration: 45 minutes**
Build mobile applications using React Native and Debros Network.
**Topics Covered:**
- React Native setup
- Offline synchronization
- Mobile-specific optimizations
- Push notifications
[**▶️ Watch Mobile Development**](https://youtube.com/watch?v=VIDEO_ID_HERE)
---
## 🎓 **Masterclass Series**
### 19. Architecture Patterns and Best Practices
**Duration: 60 minutes**
Advanced architectural patterns for large-scale Debros Network applications.
**Topics Covered:**
- Domain-driven design
- CQRS patterns
- Event sourcing
- Microservices architecture
[**▶️ Watch Architecture Masterclass**](https://youtube.com/watch?v=VIDEO_ID_HERE)
---
### 20. Contributing to Debros Network
**Duration: 30 minutes**
Learn how to contribute to the Debros Network open-source project.
**Topics Covered:**
- Development setup
- Code standards
- Testing requirements
- Pull request process
[**▶️ Watch Contributing Guide**](https://youtube.com/watch?v=VIDEO_ID_HERE)
---
## 📚 **Additional Resources**
### Video Playlists
**🎬 [Complete Beginner Series](https://youtube.com/playlist?list=PLAYLIST_ID)**
Videos 1-6: Everything you need to get started
**🏗️ [Advanced Development](https://youtube.com/playlist?list=PLAYLIST_ID)**
Videos 7-12: Advanced features and patterns
**🛠️ [Project Tutorials](https://youtube.com/playlist?list=PLAYLIST_ID)**
Videos 10-12: Complete project walkthroughs
**🔧 [Production Ready](https://youtube.com/playlist?list=PLAYLIST_ID)**
Videos 13-15: Testing, deployment, and optimization
### Community Videos
**Community Showcase**
- [Building a Decentralized Chat App](https://youtube.com/watch?v=COMMUNITY_VIDEO_1)
- [E-learning Platform with Debros](https://youtube.com/watch?v=COMMUNITY_VIDEO_2)
- [IoT Data Management](https://youtube.com/watch?v=COMMUNITY_VIDEO_3)
### Interactive Learning
**🎮 [Interactive Tutorials](https://learn.debros.io)**
Hands-on coding exercises with instant feedback
**💬 [Discord Community](https://discord.gg/debros)**
Get help and discuss videos with other developers
**📖 [Workshop Materials](https://github.com/debros/workshops)**
Download code samples and workshop materials
---
## 📅 **Release Schedule**
New video tutorials are released every **Tuesday and Friday**:
- **Tuesdays**: Core concepts and feature deep-dives
- **Fridays**: Project tutorials and real-world applications
**Subscribe to our [YouTube channel](https://youtube.com/@debrosnetwork)** and enable notifications to stay updated with the latest tutorials.
## 🤝 **Request a Tutorial**
Have a specific topic you'd like covered? Request a tutorial:
- **[GitHub Discussions](https://github.com/debros/network/discussions)** - Community requests
- **[Email Us](mailto:tutorials@debros.io)** - Direct requests
- **[Discord](https://discord.gg/debros)** - Community suggestions
We prioritize tutorials based on community demand and framework updates.
---
_All videos include closed captions, downloadable code samples, and companion blog posts for reference._

View File

@ -40,6 +40,13 @@ const sidebars: SidebarsConfig = {
'examples/basic-usage', 'examples/basic-usage',
], ],
}, },
{
type: 'category',
label: 'Video Tutorials',
items: [
'videos/index',
],
},
{ {
type: 'category', type: 'category',
label: 'Contributing', label: 'Contributing',
@ -56,9 +63,18 @@ const sidebars: SidebarsConfig = {
'api/overview', 'api/overview',
{ {
type: 'category', type: 'category',
label: 'Framework Classes', label: 'Core Classes',
items: [ items: [
'api/debros-framework', 'api/debros-framework',
'api/base-model',
'api/query-builder',
],
},
{
type: 'category',
label: 'Network API',
items: [
'api/network-api',
], ],
}, },
], ],