Author: Wang Jie

BLITZ. JS and REDWOOD. JS

With the development of cross-end technology, the rise of TS and the advent of THE 5G era, all these promote the front end field to the big front end, and the front end in the “technical level” has gradually developed to a stable period, so the front end -> API -> back end -> database, as well as the type safety on the link, has become a front end development direction.

Blitz.js and Redwood-js are moving in this direction.

blitz.js

We all know that next.js is primarily a front-end framework, designed for building front ends that connect to other apis. Next. Js is not really a full-stack framework out of the box. Blitz is a full-stack framework based on React (Next-.js framework) front end and Prisma back end, without middleware GraphQL. It adds all the missing functions of Next-js, and it has “zero API” data layer abstraction. When apis need to be provided to more ends, Combined with related libraries to generate apis (useQuery, useMutation functions, as a bridge between the front and back ends). But of course, we can continue to fetch data through REST or GraphQL as usual, and blitz. Js doesn’t limit this in any way. These features make Blitz a true full-stack framework. These key features include direct access to databases, middleware, and authentication. In blitz.js, the code that belongs to the back end is pulled to the back end during compilation and build, and the whole front and back end is treated as a whole during development.

redwood.js

Jamstack-based full-stack framework uses react front end, PRISma2 back end, Apollo GraphQL in the middle, and wraps the concept of a cell component front end, which is a simpler way to get data asynchronously. The Cell is a high-level component that contains graphQL query, load, empty, error, or success states. Each of these states enables automatic awareness of the state the cell is in. It has built-in features Redwood Router provides powerful and simple routing for Redwood applications. This is a better solution than the React Router — more straightforward and easier to use than the React Router.

Both of the two new full-stack frameworks are based on PRISma as a back-end technology. The pros and cons of the two frameworks or those who want to know more about them can be found on the official website. They are not the focus of this article. This article will focus on:

Why did the next generation of new full-stack frameworks choose PRISma as the back-end technology, and what are its advantages as a new ORM?

ORM introduction

We know that databases interact in three main ways: ORM, SQL Query Builder, and native SQL.

  • Native SQL: With native SQL you have complete control over database operations, but because sending pure SQL strings to the database is cumbersome and expensive, productivity is low.

  • SQL Query Builder: This is a semi-automatic SQL framework (such as knex.js). The constructor may have chain calls with multiple methods, which are actually closer to SQL statements and generate SQL from tool-wrapped apis. The advantage of constructors is that they have a lot of control, but the disadvantage is that application developers still have to think about their data in terms of SQL, which can lead to cognitive and real-world differences in the data.

  • ORM: The full name of ORM is object-relationl Mapping. It makes a Mapping between an Object and a database. It can be said that it is an automatic SQl framework. Instead of using complex SQL statements, users can manipulate objects as they normally would, and ORM generates executable SQL to perform operations on the database. The advantage of ORM is that it is very productive, but it is very inefficient when it comes to complex SQl.

A simple comparison of the three codes:

Comparison of productivity and control force of the three:

Compared with TYPEORM

The ORM model

TypeORM is a traditional and popular ORM in the TypeScript ecosystem. If we open TypeORM, the official documentation states:

Both DataMapper and ActiveRecord are supported

These two modes are common in ORM and differ in the way they transfer data between objects and databases:

  • ActiveRecord: This maps model classes to database tables where each field has a matching column. Simply put, it is a way to access a database in a model.
  • DataMapper: It is an intermediate layer (mapper class) between objects and database records, designed to isolate them while providing two-way data transfer like ActiveRecord. This means that with Data Mapper, objects in memory (representing Data in the database) don’t even know that the database exists.

But TypeORM’s DataMapper does not strictly follow patterns such as DataMapper ORM. It takes the following approach:

  • Entity classes use a decorator (@column) to map class attributes to table columns and sense changes to the database.

  • It replaces the mapper class with a repository class that queries databases and may contain custom queries. The repository uses decorators to determine the mapping between entity attributes and database columns.

    import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
    
    @Entity()
    export class User {
      @PrimaryGeneratedColumn()
      id: number;
    
      @Column({ name: 'first_name' })
      firstName: string
      
      @Column({ name: 'last_name' })
      lastName: string
    }
    
    const userRepository = connection.getRepository(User);
    const user = new User();
    Copy the code

TypeORM He takes this approach to ActiveRecord:

  • Entity classes use a decorator (@column) to map class attributes to table columns and sense changes to the database.

  • You need to have the entity class inherit from the BaseEntity class so that you have methods on the entity class.

    import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from "typeorm"; @Entity() export Class User extends BaseEntity {// All active-Record entities must extend the BaseEntity class, which provides methods to be used with the Entity. @PrimaryGeneratedColumn() id: number; @Column({ name: 'first_name' }) firstName: string @Column({ name: 'last_name' }) lastName: string } const user = new User();Copy the code

For the Active Record ORM pattern, the TypeORM approach typically results in complex model instances that become difficult to maintain as the application grows.

Prisma follows the DataMapper ORM pattern rather than Active Record. Prisma provides a brief comparison of how to implement DataMapper:

concept describe Traditional ORM Prisma Prisma real data source
The object model In-memory data structures in applications Model classes Model classes Generated TypeScript types Models in the Prisma model
Data Mapper Code to convert between the object schema and the database Mapper classes Mapper classes A function generated by PrismaClient The @map attribute in Prisma schema
Database architecture The structure of data in a database, such as tables and columns SQL written manually or with programming apis SQL generated by Prisma Migrate Prisma mode

Prisma follows the DataMapper pattern and comes with some advantages: for example, it can generate Prisma clients based on Prisma pattern, reducing the boilerplate that defines classes and mapping logic, and eliminating synchronization issues between program objects and database schemas.

The migration

Also, because ORMs sit between the developer and the database, most orMs provide a migration tool to assist in creating and modifying database schemas. Migration is a set of steps to move a database schema from one state to another. The first migration typically creates tables and indexes. Subsequent migrations may add or remove columns, introduce new indexes, or create new tables. Depending on the migration tool, the migration may take the form of SQL statements or program code that will be converted to SQL statements.

We now have the entity class User:

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
​
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;
​
  @Column({ name: 'first_name' })
  firstName: string
​
  @Column({ name: 'last_name' })
  lastname: string 
}
Copy the code

Then we create a new migration in TypeORM:

Typeorm migration:create -n new_migrate // new_migrate indicates the migration name, which can be used at willCopy the code

It will generate a Migrate file:

Where up contains all the code that performed the migration, down is used to restore the last migration. QueryRunner can perform all database operations.

We change the lastName field to name and write to this file:

import {MigrationInterface, QueryRunner} from "typeorm"; export class newMigrate1630909887647 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "lastName" RENAME TO "name"`); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "name" RENAME TO "lastName"`); }}Copy the code

After changing the database schema, a new migration needs to be performed:

typeorm migration:generate -n rename_name
Copy the code

Migrate generates a new file under migrate:

Once we performed the migration and changed the database schema, we had to update the entity and mapper classes to accommodate the new changes. This means you need to change lastName to name to the User entity class:

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @column ({name: 'first_name'}) firstName: string @column ({name: 'last_name'}) name: string // change lastName to name}Copy the code

Alter table user alter table user alter table user alter table user alter table user alter table user alter table user alter table user

For ORM, the workflow involves creating the migration using the migration tool and then updating the corresponding entity and mapper classes. These changes are applied manually, so synchronizing changes can be a challenge. Prisma takes a different approach, using Prisma Migrate to eliminate this synchronization problem (Django’s Makemigrations similarly eliminate synchronization problems).

Prisma Migrate is a CLI for declarative data modeling and migration. Unlike most migration tools that are part of ORM, you only need to describe the current schema, not the action of moving from one state to another. Prisma Migrate inferences operations, generates SQL, and performs migrations.

For any further changes to the database schema, simply run the Prisma Migrate directive again, and the Prisma client will automatically regenerate and then update the schema.

Prisma Migrate Automatically generates fully customizable database schema migrations using Prisma schema changes with the following features:

  • The migration is automatically generated, so there is no need to write SQL manually.
  • Migrate generates SQL migrations, ensuring that the migration will always result in the same database schema across environments.
  • The generated SQL migration can be fully customized, with full control over the exact changes.

For example, we define a data model in Prisma:

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
}
Copy the code

Perform:

Prisma Migrate dev --name init // init indicates the migration nameCopy the code

When executed, a new folder is generated:

The generated directory structure is:

Migrations / └ ─ 20210906023619 _init / └ ─ migration. The SQLCopy the code

The generated folder is the real data source for the data model history. At this point, the PRISMA schema is synchronized with the database schema and the migration is initiated.

Of course you can continue with the migration by adding a field to the user table:

model User { id Int @id @default(autoincrement()) email String @unique name String? phone Int? // Add new field}Copy the code

Perform:

prisma migrate dev --name add_phone
Copy the code

Generated file:

At this point, the Prisma schema is again automatically synchronized with the database schema, and the Migrate folder contains two historical migrations:

Migrations / └ ─ 20210906023619 _init / └ ─ migration. The SQL └ ─ 20210906024134 _added_phone / └ ─ migration. The SQLCopy the code

Open database: Prisma has automatically updated the phone field to the database.

It is important to note that you do not modify automatically generated files, as we did in one of the migrate.

CREATE TABLE `User` ( `id` INTEGER NOT NULL AUTO_INCREMENT, `email` VARCHAR(500) NOT NULL, // change 191 to 500 'name' VARCHAR(191), UNIQUE INDEX 'user.email_unique' (' email '), PRIMARY KEY (`id`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;Copy the code

After execution, Prisma asks:

Prisma Migate has detected migration and has changed, and prisma Model is out of sync with migration history. Do you want to reset? If yes, the database is reset and all migrations (including edit migrations) are relocated, and all current database data is lost.

Raw SQL

TypeORM can optionally use QueryBuilder in cases where raw SQL queries need to be executed:

import {getConnection} from "typeorm";
await getConnection()
   .createQueryBuilder()
   .update(User)
   .set({ 
       firstName: "Timber", 
       lastName: "Saw",
       age: () => "'age' + 1"
   })
   .where("id = :id", { id: 1 })
   .execute();
Copy the code

In PRsima, Prisma Client exposes two ways to send raw SQL queries to your database:

  • The actual record returned using $queryRaw (for example, using SELECT)
  • Use $executeRaw to return the number of rows affected (for example, later UPDATE or DELETE)
const email = '[email protected]' const result = await prisma.$queryRaw`SELECT * FROM User WHERE email = ${email}; ` const result: number = await prisma.$executeRaw( 'UPDATE User SET active = true WHERE emailValidated = true; ')Copy the code

Type safety

TypeOrm is one of the first ORMs in the NodeJS ecosystem to fully adopt TypeScript, and does an excellent job of providing some level of type safety for its database queries. However, TypeOrm is not completely type safe in many cases. For example, We make each object in the hiredUsers array return only ID and name at run time, but the TypeScript compiler knows nothing about this. We can access all attributes defined on the entity by typing:

If you run the code, an error is reported:

This is because the TypeScript compiler only sees the types of objects returned by User, but doesn’t know what fields those objects actually carry at run time. Therefore, it does not protect you from accessing fields that have not yet been retrieved in database queries, resulting in runtime errors.

But in PRISma, Prisma Client can guarantee full type safety in the same case and protect you from accessing fields that are not retrieved from the database:

Prisma only allows us to access the fields that publishedPosts returns. If we continue to access the fields, the TypeScript compiler will throw the following error at compile time:

This is because Prisma Client dynamically generates return types for its queries.

Of course, there are other types of security and API differences, which you are welcome to experience for yourself. I will not list them all here.

Visualization tool

Prisma Studio is a visual editor of data in a database. This is prisma’s own simple table interface that allows you to quickly view your local database and do CRUD operations as well as filtering, paging, and so on.

Prisma as a new ORM, although it is ORM in nature, it is much more powerful than the traditional ORM:As you can see, it will be more productive than traditional ORM.

It provides a type-safe and modern API for data access to ensure that developers can read and write data in a convenient, efficient, and secure way that saves a lot of time.

Like any other tool, it has its trade-offs. Here is a brief introduction to Prisma.

Constituent parts

prisma Client

Prisma Client JS is a type-safe database Client that is automatically generated based on the data model definition, which is a declarative representation of the database schema. That’s what prisma is all about. It’s automatically generated code that allows secure access to data, which is the equivalent of an ORM.

From a technical perspective, Prisma Client consists of three main components:

  • JavaScript client library

  • TypeScript type definitions

  • A query engine (as a binary file)

    • Query engine workflow:

    1. Call $connect() to start the query engine process.

    2. The query engine establishes a connection to the database and creates a connection pool.

    3. PrismsClient is ready to send a query to the database.

    PrismsClient sends a findMany() query to the query engine.

    5. The query engine translates the query into SQL and sends it to the database.

    6. The query engine receives the response from the DATABASE SQL.

    7. The query engine returns the result to prismsClient as a JS object.

    8. Call $disconnect().

    9. The query engine closes the database connection.

    10. The query engine process stops.

Prisma Migrate

It converts Prisma schemas into the SQl needed to create and change tables in a database. It uses powerful SDL syntax for intuitive data modeling and seamless database migration to manage database structures.

Prisma Studio

It can display a database in a graphical interface, which is essentially a DATABASE GUI.

Basic knowledge of

schema.prisma

Schema. prisma is the main configuration file of prisma. Using vscode can be used with the prisma plug-in (highlighting, syntax checking, formatting) to make the developer experience better. Schema. prisma consists of the following parts:

  • Data source: How Prisma connects to data sources. Save learning costs by using sqLite in file form:

    datasource db {
      provider = "sqlite"
      url      = "file:./dev.db"
    }
    Copy the code

    Or connect to local Mysql or other database:

    datasource db {
      provider = "mysql"
      url      = "mysql://root:root1234@localhost:3306/prisma"
    }
    Copy the code
  • Generator (Optional) : Which data model to base the client on.

    generator client {
      provider = "prisma-client-js"
    }
    Copy the code
  • Data model definition: Describe your application model. For relational databases: model maps a table; Non-relational databases: Map to a collection/resource with model.

    model User {
      id    Int     @id @default(autoincrement())
      email String  @unique
      name  String?
      posts Post[]
    }
    Copy the code
    • With PrismaClient, each model in Prisma schema is converted to a proprietary TypeScript type to make database access fully type-safe.

    • Type modifiers:

      • [] : Sets the field to a list
      • ? : Sets the field to optional. Don’t use? Type modifiers annotate fields that are required for each record in the model.
    • Attribute definition:

      • @id: Defines the ID field.

        • @@id([firstName, lastName])
      • @default: Defines the default value.

        • Represents the default value in the underlying database (relational database only)

        • Use the Prisma function. For example, cuID () and uuid() are query functions provided by Prisma’s company:

          • Cuid () : Generates globally unique identifiers according to the CUID specification.
          • Uuid () : generates globally unique identifiers according to the UUID specification.
          • Autoincrement () : Creates a sequence of integers from which incrementing values are assigned to the ID value of the created record.
          • Now (): Sets the timestamp of the record creation time.
      • @unique: unique identifier.

    Associations between tables are constrained by foreign keys.

    model Post {
      id        String @id @default(cuid())
      title     String
      content   String?
      published Boolean @default(false)
      author    User?   @relation(fields: [authorId], references: [id])
      authorId  Int?
    }
    Copy the code
    • Prisma has three different types (or cardinal numbers) of relationships: there are one-to-one, one-to-many, and many-to-many relationships. The example is a one-to-many relationship. In Prisma, the foreign key/primary key relationship is represented by an attribute on the field @relation:

      • The Fields property is in the current table, and references is on the other side of the relationship.
      • AuthorId is the foreign key of Post, and ID is the primary key of User.
      • In addition to fields and References, you can use name to define the relationship name to avoid ambiguity.
      • If the relation is one-to-one or one-to-many, @relation must be used. Many-to-many can declare relationships without @relation, which uses the @ ids of both parties to establish a cascading relationship.
      • Of course, you can also relate your own model, and this relationship is called “ego relationship.” Self relation also always requires @relation property.

CRUD operations

  • FindMany: Gets all records

  • FindUnique: Returns a single record by a unique identifier or ID

  • Create: Creates a field

    • Such as:

      const user = await prisma.user.create({
        data: {
          email: '[email protected]',
          name: 'deserve',
        },
      })
      Copy the code
  • Update: Updates a single record

  • Upsert: Updates or creates records

  • Delete: deletes a single record

  • UpdateMany: Updates multiple records

  • DeleteMany: deletes multiple records

  • Where: filter

  • Orderby: sorting

  • Skip, take: paging

    • For example, return records 4-7

      const results = await prisma.post.findMany({
        skip: 3,
        take: 4,
      })
      Copy the code

      No scaling is done at the database level. Reading records will traverse all skipped records. If there is a large amount of data, performance will be adversely affected.

  • -Blair: Aggregate.

    • Such as:

       const aggregations = await prisma.user.aggregate({
          _avg: {
            age: true,
          },
        })
        
        console.log('Average age:' + aggregations._avg.age)
      Copy the code
  • GroupBy: polymerization

    • Where: Filters a single record
    • Having: Filters data groups

Here is a brief list of some of the apis. For more information, please refer to the official documentation.

Prisma.validator

It takes the generated type and returns a type-safe object that follows the generated type model field.

If the email address of user 3 needs to be returned, null is returned if the user does not exist:

import { Prisma } from '@prisma/client'
​
const userEmail: Prisma.UserSelect = {
  email: true,
}
​
const user = await prisma.user.findUnique({
  where: {
    id: 3,
  },
  select: userEmail,
})
Copy the code

This works fine, but if you hover over userEmail, TypeScript can’t infer an object’s key or value:

Or if we click userEmail in findUnique, we’ll see all the properties available to the select object:

It is typed, but not type-safe, and it still has access to other available properties.

We can use prisma.validator to validate generated types to ensure they are type-safe:

import { Prisma } from '@prisma/client'
​
const userEmail = Prisma.validator<Prisma.UserSelect>()({
  email: true,
})
​
const user = await prisma.user.findUnique({
  where: {
    id: 3,
  },
  select: userEmail,
})
Copy the code

Hover over userEmail,

If you click userEmail in findUnique, you can only see email:

This passes the type generated by UserSelect to Prisma.Validator, and userEmail is now type-safe.

Prisma.UserGetPayload

If you need a type that only passes the email and name fields of User, you can use prisma. UserGetPayload to ensure the type security and easy maintenance:

import { Prisma } from '@prisma/client'
​
const userWithPosts = Prisma.validator<Prisma.UserArgs>()({
  include: { posts: true },
})
​
const userPersonalData = Prisma.validator<Prisma.UserArgs>()({
  select: { email: true, name: true },
})
​
type UserWithPosts = Prisma.UserGetPayload<typeof userWithPosts>
Copy the code

Here you use prisma. validator to create two type-safe objects, and then use the prisma. UserGetPayload utility function to create a type that can be used to return all users and their posts.

PRISMA+RESTFUL

REST, or Representational State Transfer, is essentially used to define URIs and obtain resources through apis. Universal system architecture, no language restrictions.

Prisma was used to create the RestApi, which has many advantages such as type safety, advanced apis, and free reading and writing of relational data. When building REST apis, Prisma Client can be used in routing to send database queries.

The Express framework is used here to implement Rest+Prsima.

Introduce PrismaClient with Express, and instantiate PrismaClient:

import { PrismaClient } from "@prisma/client";
import express from "express";
​
const prisma = new PrismaClient();
const app = express();
​
app.use(express.json());
Copy the code

Define some interfaces according to prisma’s preset data model:

App.get ('/users', async (req, res) => {const result = await prisma.user.findmany (); res.json(result); }); // New article app.post(' /post ', async (req, res) => {const {title, content, rules} = req.body; const result = await prisma.post.create({ data: { title, content, author: { connect: { email: authorEmail, }, }, }, }); res.json(result); }); .Copy the code

Define express startup port:

App.listen (3000, () => console.log(' 🚀 Server ready at: http://localhost:3000 '));Copy the code

Start the Express scaffolding, invoke the interface, and get the data.

PRISMA+GRAPHQL

Graphql is a query language for apis and a server-side runtime that executes queries based on a type system defined by your data. For example, SQL is short for structured query Language, so Graph+QL = graphical (visual) query language is an API syntax that describes how a client requests data from a server. It’s not a database itself, it’s just an interface to the data, it doesn’t manipulate the database directly (hence the manipulation API), it just lets you get exactly what you want.

Unlike restful, which only returns one resource per interface, GraphQL can fetch multiple resources at once. And REST uses different urls to differentiate resources. Graphql distinguishes resources by type. It is often used as a substitute for RESTful apis, but can also be used as an additional “gateway” layer on top of existing RESTful services.

Prisma is a database abstraction layer that converts a database into the GraphQL API with CRUD operations and real-time capabilities. It is the glue between the database and the GraphQL server. Prisma Client works well with the Apollo framework ecosystem (it is an open source, standards-compliant GraphQL server). It is also one of the most widely used GraphQL server frameworks. Has default support for GraphQL Subscriptions, Relay style paging support, end-to-end type-safe and built-in Dataloader to solve N+1 problems (for Dataloader, See GraphQL N+1 to DataLoader source code parsing.

We can mount the default Prisma Client in Context (context.ts) :

import { PrismaClient } from '@prisma/client'
​
const prisma = new PrismaClient()
​
export interface Context {
  prisma: PrismaClient
}
​
export const context: Context = {
  prisma: prisma
}
Copy the code

Configuring the ApolloServer service:

import { ApolloServer } from "apollo-server";
import { Context, context } from "./context";
const server = new ApolloServer({ typeDefs, resolvers, context });
Copy the code

We added graphlL type definitions typeDefs, parser, and PRISma to ApolloServer as data sources.

Apollo Server calls this data source for each incoming operation. The Context object is passed between the server’s parsers. This method allows you to access prisma data sources from shared Context objects and use them to retrieve data. This eliminates the need to import Prisma Client once for each Resolver.

TypeDefs are defined:

const typeDefs = ` type Query { allUsers: [User!] ! draftsByUser(id: Int!) : [Post] } type Mutation { signupUser(name: String, email: String!) : User! createDraft(title: String! , content: String, authorEmail: String): Post } type User { id: Int! email: String! name: String posts: [Post!] ! } type Post { id: Int! createdAt: DateTime! updatedAt: DateTime! title: String! content: String published: Boolean! viewCount: Int! author: User } scalar DateTime `;Copy the code

Configure the parser function:

const resolvers = {
  Query: {
    allUsers: (_parent, _args, context: Context) => {
      return context.prisma.user.findMany()
    },
    draftsByUser: (_parent, args: { id: number }, context: Context) => {
      return context.prisma.user.findUnique({
        where: {
          id: args.id
        }
      }).posts({
        where: {
          published: true
        }
      })
    },
    },
   Mutation: {
    createDraft: (
      _parent,
      args: { title: string; content: string | undefined; authorEmail: string },
      context: Context
    ) => {
      return context.prisma.post.create({
        data:{
          title: args.title,
          content: args.content,
          author: {
            connect: {
              email: args.authorEmail
            }
          }
        }
      })
    },
  }
  }
Copy the code

Corresponding query statement:

-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- query all users {allUsers {id name email posts title} {id}} -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- all query a user unpublished articles { draftsByUser(id: 3) {id title content published viewCount author {id name email}}} ----------------- createDraft(title: "Hello World", authorEmail: "[email protected]") { id published viewCount author { id email name } } }Copy the code

The parser can choose to accept four positional arguments: parent, args, Context, info. This context is the object shared between all parsers performing for a particular operation, namely the Prisma object.

Refer to the link

  • Prisma website
  • TypeOrm website
  • Apollo’s official website
  • Redwood’s official website
  • The Blitz’s official website
  • Next generation ORM, not just ORM
  • Hands-on Introduction Prisma
  • What problems should the next generation of front-end frameworks solve