npm package: @react-ion/ssr
Description: The framework is split into 2 parts. The async core and the server side. The core handles prefetching async resolvers, almost everything is build around this feature.
Author: Dimitri Lilov
Latest version: 0.1.32

# @react-ion/ssr (A server side react framework)

## About
The framework is split into 2 parts. The async core and the server side.
The core handles prefetching async resolvers, almost everything is build around this feature.

# Documentation
## Core:
### Async
#### `Async.create(resolver: Async.Resolver | Api.Method, component: React.FC, options?: Async.Options): React.FC`: 
The first parameter needs to be an function that returns some data.
The second parameter is the `React.FC` itself which will render based on the `Async.State`. A new `React.FC` will be returned with some extra properties such as `prefetch`, `cache` and `invalidator`;
The third parameter is optional and which can set the default `prefetch` and `cache` props if no props are given to the component.
- `prefetch`: `?boolean` to prefetch the data before rendering the whole app set.
- `cache`: `?number | { timeout?: number, prefetch?: boolean }` to auto resolve the resolver when the timeout is reached.
- `invalidator`: `?function` to invalidate the resolved data and resolve again. Usefull for events like `onClick`.

The first parameter of the resolver is the props that are given to the component. The second argument is the api object to make calls to the api endpoints. The reason to use the second argument is to use the same session data on the server as the initial request. Otherwise if no session is used the `global.api` object can be used. 
```tsx
const AsyncTest = Async.create(() => fetch("some url to fetch").then(res => res.json()), ({ data, error, isLoading, invalidate }) => 
{
    if(isLoading)
        return "Loading...";

    if(error)
    {
        console.error(error);
        return null;
    }

    // if isLoading and error is undefined that means that data exists!
    return (
        <div onClick={() => invalidate()}>
            text: {data.text}
        </div>
    );
});

// To use the AsyncTest component
const Test = () => <AsyncTest />;
// Invalidate every 5 seconds
const Test = () => <AsyncTest cache={5000} />;
// Prefetching
const Test = () => <AsyncTest prefetch />;

```

#### `Async.useInvalidator(): Async.Invalidator`:
A hook to create a `invalidate` function which can be called to invalidate the async state.
```tsx
// with the example from above
const InvalidateTest = () => 
{
    const invalidate = Async.useInvalidator();

    return (
        <div>
            <AsyncTest prefetch invalidator={invalidate}>
            <button onClick={() => invalidate()}>Invalidate</button>
        </div>
    );
};
```
<hr/>

### Dynamic
#### `Dynamic.create(importer: Dynamic.Importer)`:
Is usefull if the app needs to be split into seperate chunks. The first parameter needs to be a dynamic import. The second argument can optionally be an `Dynamic.Options` object.
The options is usefull for rendering custom error and loading components.
The only prop that get exposed is the `prefetch` prop which works the same as the `Async` prefetch prop.

```tsx
// with the example from above
const HomePage = Dynamic.create(() => import("./pages/Home"));

const Test = () => <HomePage prefetch />;
```
<hr/>

### Context
#### `Context.create<T>(defaultValue: T)`:
To keep track of all the contexts for each async component you will need to use `Context.create()` instead of the default `React.createContext()` function.
`Context.create` will expose 2 properties. A `Provider` and a `use` method.
```tsx
const CounterContext = Context.create({ count: 1 });

const Counter = () => 
{
    const { count } = CounterContext.use();

    return <h1>{count}</h1>; // renders 100
}

const App = () => 
{
    return (
        <CounterContext.Provider value={{ count: 100 }}>
            <Counter />
        </CounterContext.Provider>
    );
};
```

#### `Context.createAsync(initializer: Async.Resolver | Api.Method)`:
This is a wrapper around the `Context.create()` function which allows for 
```tsx
const AuthContext = Context.create(async (_, api) => 
{
    const { data } = api.auth.login.get();

    return {
        isLoggedIn: !!data
    };
});

const ControlPanel = () => 
{
    const { isLoggedIn } = AuthContext.use();

    if(!isLoggedIn)
        return null;

    return <h1>Yay your logged in!</h1>;
}

const App = () => 
{
    return (
        <AuthContext.Provider>
            <ControlPanel />
        </AuthContext.Provider>
    );
};
```

<hr/>

### Router

#### Route:
The `<Route />` component will render the Component of children if the path matches the current path.
Props:
- path: `string` the path to match to. The path can contain params like `/user/:id` which can be accessed by using the `useParams()` hook.
- exact: `?boolean` says that the path needs to match exactly with the current path.
- title: `?string` a title to set when the route matches.
- withBase: `?boolean` tells to prefix the route with the currents page's base path. This will allow to use the same routes on multiple pages.
- Component: `?React.FC` the component to render when the route matches. Otherwise the children of the Route will be renderer.
```tsx
// Simple example
const App = () => (
	<>
		<Route path="/home" title="Home" exact>
			<h1>Home</h1>
		</Route>
		<Route path="/about" title="About" exact Component={About} />
	</>
);

// The routes can match on multiple pages because of the withBase prop.
// `/admin/auth/login` will match on the `/admin` page.
// `/auth/login` will match on the `/` page.
const AuthRoutes = () => (
	<>
		<Route withBase exact path="/auth/login" title="Login">
			<h1>Login</h1>
		</Route>
		<Route withBase exact path="/auth/register" title="Register">
			<h1>Register</h1>
		</Route>
	</>
);
```

#### Link:
The `<Link />` will route to the given `to` path and won't reload the whole app. If the Link routes to another page it will load the whole new app.
Props:
- to: `string` the path to match to.
- exact: `?boolean` says that the path needs to match exactly with the current path.
- activeClass: `?string` a class name to set when the `to` path matches current route.
- withBase: `?boolean` tells to prefix the route with the currents page's base path. This will allow to use the same routes on multiple pages.
```tsx

export const Navbar = () => (
	<nav>
		<Link to="/home">Home</Link>
		<Link withBase to="/auth/login">Login</Link>
		<Link withBase to="/auth/register">Register</Link>
	</nav>
);
```

#### Redirect:
The `<Redirect />` component will redirect when the `from` prop matches the current route.
Props: 
- from: `string` the path to match with.
- to: `string` the path to redirect to.
- exact: `?boolean` says that the path needs to match exactly with the current path.
- withBase: `?boolean` tells to prefix the `from` prop with the currents page's base path. This will allow to use the same routes on multiple pages.
```tsx

export const RedirectToHome = () => (
	<Redirect from="/" to="/home" exact />
);
```

#### useParams:
The `useParams()` hook will retreive the current routes params.
```tsx
const User = () => 
{
	const { id } = useParams();

	if(!id)
		return <Redirect from="/user" to="/home" />; // redirect if no id is provided

	return (
		<div>
			Show user with id {id}
		</div>
	);
}

const App = () => (
	<Route path="/user/:id" title="User" exact Component={User} />
);
```

#### useTitle:
The `useTitle()` hook will set the document title.
```tsx
const User = () => 
{
	const { id } = useParams();

	useTitle(id && (title) => `${title} ${id}`); // if id is defined the title will be set to "User {id}".

	if(!id)
		return <Redirect from="/user" to="/home" />; // redirect if no id is provided

	return (
		<div>
			Show user with id {id}
		</div>
	);
}

const App = () => (
	<Route path="/user/:id" title="User" exact Component={User} />
);
```

#### useNavigate:
The `useNavigate()` hook retruns a function which can be used to navigate to another page/route.
```tsx
const App = () => 
{
	const navigate = useNavigate();
	
	return (
		<div onClick={_ => navigate("/home")}>
			Navigate to home!
		</div>
	);
};
```

#### useOnNavigateStart/useOnNavigateEnd:
These hooks will call the callback when the router starts/ends navigating.
If an async callback is provided the router will wait till they all are resolved before navigating further.
```tsx
const App = () => 
{
	const [isLoading, setIsLoading] = useState(false);
	useOnNavigateStart(async () => { setIsLoading(true); await wait(500); }); // wait for 500ms to allow animations to take time
	useOnNavigateEnd(() => setIsLoading(false));
	
	return (
		<div>
			{isLoading && "Loading..."}
		</div>
	);
};
```
<hr/>

### Store
To use global states you can use the `Store.create` method. Whenever a property changes on the store all components which uses the store will rerender. No setState or other functions are needed to update and rerender the components.
```tsx
const GlobalStore = Store.create(() => ({ count: 1 }), "GlobalStore");

const App = () => 
{
	const store = GlobalStore.use();

	return (
		<div onClick={_ => store.count++}>{store.count}</div>
	);
};
```
<hr />

## Server:
The server pacakge exposes 2 classes, an Api class and a Model class.
### Api:
The Api class is an abstract class which can be used to create api endpoints. When exported from the api entry the whole app can use the endpoint just like a function (on the global/window object).
When called it returns an `Api.Response<T>` which contains or an error or the data.
To add middleware use the `@Api.middleware()` decorator. It accepts a function with the `(req: Request, res: Response)` arguments. To cancel the request throw an Error.
To validate the api props use the `@Api.validate()` decorator. It accepts a object with a layout to validate against.
```tsx
// src/api/users.ts
class Users extends Api
{
	// checks if the count property is of type number | string
	@Api.validate({ count: ["?number", "?string"] })
	override async get({ count = 10 }: { count?: string | number }): Promise<User[]>
	{
		const response = await fetch(`https://random-data-api.com/api/users/random_user?size=${count}`).then(r => r.json());
		if(response.error)
			throw new Error(response.error);
		return response;
	}

	@Api.middleware(req => { if(!res.session.user?.isAdmin) throw new Error("Not Authorized!"); })
	override async delete({ id }: { id: number })
	{
		return await User.delete({ where: { id } });
	}
}

// src/api.index.ts
export default {
	users: Users
};

// now throughout the whole app you can just use the api like:
const { data, error } = await api.users.get();
const { data, error } = api.users.get({ count: 20 });

// it can also directly be used with Async Components like:
const UsersList = Async.create(api.users.get, ({ data, error, isLoading }) => 
{
	if(isLoading)
		return "Loading...";

	if(error)
	{
		console.error(error);
		return null;
	}

	return (
		<>
			{data.map(({ id, first_name, last_name }) => (
				<div key={id}>
					{id} - {first_name} {last_name}
				</div>
			))}
		</>
	);
});

const App = () => <UsersList />;
// or
const App = () => <UsersList count={20} />;
```

### Model:
The model class can be used the create database models/tables. The tables will automaticly be generated when not existing.
To register the model use the `Model.register(tableName, ?seeder)` function. When a seeder is provided the seeder will run after the table is created.
To defined some column attributes there are some decorators that can be used:
- `@Model.column`: to create a default table column. (the type will be infered from typescript through reflection.)
- `@Model.default`: to set a default value when an entity is created.
- `@Model.unique`: to make the column unique.
- `@Model.required`: to make the column required.
- `@Model.transform`: to make transform the column data on insert/update or select.

```tsx
@Model.register("accounts", Account.seed)
export class Account extends Model
{
	private static readonly seed = async () => 
	{
		const data = await fetch(`https://random-data-api.com/api/users/random_user?size=100`).then(r => r.json());
		
		await Promise.allSettled(data.map(async (data: any) => 
		{
			await Account.create({
				email: data.email,
				password: data.password,
				username: data.username
			})
		}));
	};

	private static readonly generateHash = async () => 
	{
		return createHash("sha512").update(`${Math.random() * Date.now()}`).digest("hex");
	};

	private static readonly hashPassword = (password: string) => 
	{
		return createHash("sha512").update(password).digest("hex");
	};

	@Model.unique
	@Model.required
	public username!: string;

	@Model.unique
	@Model.required
	public email!: string;

	@Model.required
	@Model.transform(Account.hashPassword, Model.Insert)
	public password!: string;

	@Model.default(() => false)
	public readonly isActivated?: boolean = false;

	@Model.default(Account.generateHash)
	public readonly activationHash?: string;

	public checkPassword(password: string)
	{
		return this.password === Account.hashPassword(password);
	}
}

// The models can be referenced inside other models
@Model.register("users")
export class User extends Model
{
	@Model.column
	public firstName?: string;

	@Model.column
	public lastName?: string;

	@Model.column
	public address?: string;

	@Model.column
	public postalCode?: string;

	@Model.column
	public phone?: string;

	@Model.column
	public account?: Account; // This will reference the Account model/table
}

// To select models
User.find({
	// (optional) tells which columns to retreive, if not provided all columns are retreived.
	select: {
		firstName: true,
		lastName: true
	},
	// (optional) where case
	where: { 
		firstName: "foo"
	},
	// or a more complex where case
	where: {
		id: { ">=": 100, "<": 200 } // select where id >= 100 and id < 200
	},
	// (optional) tells which referenced models to include in the found models
	include: {
		account: true
	},
	// (optional) the max number of models to select
	limit: 100,
	// (optional) how to sort the models/rows
	order: {
		firstName: "ASC"
	},
	// or directly sort on id
	order: "ASC",
})
```