Getting started
I copy this simple example from installation page. We will keep this file as mod.ts
and continue to add various models and functions to it.
import { lesan, MongoClient } from "https://deno.land/x/lesan@vx.x.x/mod.ts"; // Please replace `x.x.x` with the latest version in [releases](https://github.com/MiaadTeam/lesan/releases)
const coreApp = lesan();
const client = await new MongoClient("mongodb://127.0.0.1:27017/").connect();
const db = client.db("dbName"); // change dbName to the appropriate name for your project.
coreApp.odm.setDb(db);
coreApp.runServer({ port: 1366, typeGeneration: false, playground: true });
Please replace
x.x.x
in the import link with the latest version in releases
Add New Model
For adding a new model we should call newModel
function from coreApp.odm
. Lets add a country model, please add the following code before coreApp.runServer
:
const countryPure = {
name: string(),
population: number(),
abb: string(),
};
const countryRelations = {};
const countries = coreApp.odm.newModel(
"country",
countryPure,
countryRelations
);
We also need to import
string
andnumber
from lesan. These are validators exported from Superstruct. We use Superstruct to define models and validate function inputs and some other things.
The newModel
function accepts three inputs:
- The first input is to define the name of the new model.
- The second input is to define the pure fields of that model in the database. For this, we use an object whose keys are the names of each of the fields, and the value of these keys is obtained by one of the functions exported from Superstruct.
- The third input is to define the relationship between models. Because we have just one model here, we pass an empty object for that. We will read more about this later.
Finally, the newModel
function returns an object that has services such as insertOne
, insertMany
, updateOne
, deleteOne
, and so on.
Add an access point
Every model needs at least one act
as an access point to communicate and send or receive data. For adding an act
to countries
please add the following code before coreApp.runServer
:
const addCountryValidator = () => {
return object({
set: object(countryPure),
get: coreApp.schemas.selectStruct("country", 1),
});
};
const addCountry: ActFn = async (body) => {
const { name, population, abb } = body.details.set;
return await countries.insertOne({
doc: {
name,
population,
abb,
},
projection: body.details.get,
});
};
coreApp.acts.setAct({
schema: "country",
actName: "addCountry",
validator: addCountryValidator(),
fn: addCountry,
});
We need to import
object
functionActFn
type fromlesan
The setAct
function
As you can see, to add an act to country
, we need to use the setAct
function in coreApp.acts
.
This function receives an object as input that has the following keys:
schema
is the name of the model to which we want to set an action.actName
is just a simple string to identify the act.fn
is the function we call when a request arrives for it.validator
is a superstruct object which is called before calling the act fn and validating the given data. Validator includesset
andget
objects.- An optional key named
validationRunType
that receives the values ofassert
andcreate
and determines the type of validator run so that we can create data or change previous data during validation. You can read about it here. - There is another optional key called
preAct
which receives an array of functions. These functions are executed in the order of the array index before the execution of the main endpoint function. With these functions, we can store information in the context and use it in the main function, or not allow the main function to be executed. We mostly use this key forauthorization
andauthentication
. You can think of that key as middleware in Express. - Like
preAct
, there is another optional key calledpreValidation
. which, likepreAct
, receives an array of functions and executes them in order before executing the validation function.
There is a context
inside Lesan, which is available by contextFns.getContextModel()
function. And we can share information between the functions of an Act
like preAct
, preValidation
, validator
and fn
through this context. By default, the body
and header
of each request are available in this context.
In addition, the fn
function receives an input called body
, which is the body
of the sent request. If we have changed the body in the context
. The body entered in fn
function will be updated and changed.
The Validator function
In the addCountryValidator
function that we wrote for the validator
key, we have returned the object
function from the Superstruct struct.
This object
contains two key:
set
: It is anobject
in which we define the required input information for each function available on the client side. In the desired function, we can get theset
object information from this address.body.details.set
. Note that this object must be of Superstructobject
function type.get
: This key is also a Superstructobject
, where we specify what data can be sent to the client. This object is used in such a way that the client can specify what data he needs with values of0
or1
for eachkey
. Actually, this object can be like this:
But as you can see, we have usedget: object({ name: enums([0, 1]), population: enums([0, 1]), abb: enums([0, 1]), });
selectStruct
function ofcoreApp.schemas.selectStruct
. This function has two inputs. The first input is thename
of the model for which we want to generate this object, and the second input specifies the degree of penetration into eachrelationship
. The second input of theselectStruct
function can be entered as anumber
or anobject
. If entered as an object, the keys of this object must be thenames
of the selected model relationships, and its value can again be anumber
or anobject
of its key relationships. Such as:
As a result, an object will be produced as follows:get: coreApp.schemas.selectStruct("country", { provinces: { cities: 1 }, createdBy: 2, users:{ posts: 1 } }),
We directly send the data received from the get key as a projection to MongoDB.get: object({ name: enums([0, 1]), population: enums([0, 1]), abb: enums([0, 1]), provinces: object({ name: enums([0, 1]), population: enums([0, 1]), abb: enums([0, 1]), cities: object({ name: enums([0, 1]), population: enums([0, 1]), abb: enums([0, 1]), }), }), createdBy: object({ name: enums([0, 1]), family: enums([0, 1]), email: enums([0, 1]), livedCity: object({ name: enums([0, 1]), population: enums([0, 1]), abb: enums([0, 1]), province: object({ name: enums([0, 1]), population: enums([0, 1]), abb: enums([0, 1]), }), }), posts: object({ title: enums([0, 1]), description: enums([0, 1]), photo: enums([0, 1]), auther: object({ name: enums([0, 1]), family: enums([0, 1]), email: enums([0, 1]), }), }), }), users: object({ name: enums([0, 1]), family: enums([0, 1]), email: enums([0, 1]), post: object({ title: enums([0, 1]), description: enums([0, 1]), photo: enums([0, 1]), }), }), });
The fn
function
The fn
key receives the main act
function, we write this function for that:
const addCountry: ActFn = async (body) => {
const { name, population, abb } = body.details.set;
return await countries.insertOne({
doc: {
name,
population,
abb,
},
projection: body.details.get,
});
};
This function receives an input called body
, the body
of the request
sent from the client side is passed to it when this function is called, as a result, we have access to the information sent by users.
The request body sent from the client side should be a JSON
like this:
{
"service": "main",
"model": "country",
"act": "addCountry",
"details": {
"set": {
"name": "Iran",
"population": 85000000,
"abb": "IR"
},
"get": {
"_id": 1,
"name": 1,
"population": 1,
"abb": 1
}
}
}
- The
service
key is used to select one of themicroservices
set on the application. You can read more about this here. - The
model
key is used to select one of theModels
added to the application. - The
act
key is used to select one of theActs
added to the application. - The
details
key is used to receive data to be sent from the client side along with data to be delivered to users. This key has two internal keys calledget
andset
, we talked a little about it before.set
: It contains the information we need in theAct
function. For this reason, we can extractname
,population
, andabb
from withinbody.details.set
.get
: Contains selected information that the user needs to be returned. Therefore, we can pass this object directly to Mongoprojection
.
As you can see, we have used the insertOne
function, which was exported from the countries
model, to add a new document. This function accepts an object as input, which has the following keys:
{
doc: OptionalUnlessRequiredId<InferPureFieldsType>;
relations?: TInsertRelations<TR>;
options?: InsertOptions;
projection?: Projection;
}
- The
doc
key receives an object of the pure values of the selected model.OptionalUnlessRequiredId
type is thedocument
type in the official MongoDB driver. You can read about it here. - The
relations
key receives an object from the relations of this model. There is no relationship here. We will read about this in the next section. - The
options
key gets the official MongoDB driver options to insertOne. You can read more about this here - The
projection
key is used to receive written data. We use native projection in MangoDB. You can read MongoDB's own documentation here. IninsertOne
, you can only penetrate one step in relationships. Here you can get only pure fields because there is no relation. We will read more about this later.
The code
So this is all the code we've written so far (You can also see and download this code from here):
import {
ActFn,
lesan,
MongoClient,
number,
object,
string,
} from "https://deno.land/x/lesan@vx.x.x/mod.ts"; // Please replace `x.x.x` with the latest version in [releases](https://github.com/MiaadTeam/lesan/releases)
const coreApp = lesan();
const client = await new MongoClient("mongodb://127.0.0.1:27017/").connect();
const db = client.db("dbName"); // change dbName to the appropriate name for your project.
coreApp.odm.setDb(db);
const countryPure = {
name: string(),
population: number(),
abb: string(),
};
const countryRelations = {};
const countries = coreApp.odm.newModel(
"country",
countryPure,
countryRelations
);
const addCountryValidator = () => {
return object({
set: object(countryPure),
get: coreApp.schemas.selectStruct("country", { users: 1 }),
});
};
const addCountry: ActFn = async (body) => {
const { name, population, abb } = body.details.set;
return await countries.insertOne({
doc: {
name,
population,
abb,
},
projection: body.details.get,
});
};
coreApp.acts.setAct({
schema: "country",
actName: "addCountry",
validator: addCountryValidator(),
fn: addCountry,
});
coreApp.runServer({ port: 1366, typeGeneration: false, playground: true });
Please replace
x.x.x
in the import link with the latest version in releases
Run Server function
The last thing we want to talk about is the coreApp.runServer
function, this function receives an object
input that has the following keys:
port
used to specify the port used to run the server.polyground
that receives aBoolean
value that specifies whether the Polyground is available athttp://{server-address}:{port}/playground
address.typeGeneration
, which receives aBoolean
value and creates a folder nameddeclarations
, and inside it, the typefaces of the program are generated to be used in various cases, we will read more about this later.staticPath
that receives anarray
of paths as astring
and makes the content inside these paths statically serveable. We will read more about this later.cors
which receives either the*
value or anarray
of URLs as astring
, and makes these addresses have the ability to communicate with the server and not receive thecors
error.
Running App
Now you can run
deno run -A mod.ts
for running the Application with deno
You can use playground: Or postman:
To send a post
request to http://localhost:1366/lesan
with this request body
:
{
"service": "main",
"model": "country",
"act": "addCountry",
"details": {
"set": {
"name": "Iran",
"population": 85000000,
"abb": "IR"
},
"get": {
"_id": 1,
"name": 1,
"population": 1,
"abb": 1
}
}
}
For inserting a new country.
You shuold get this result:
{
"body": {
"_id": "6534d7c6c5dec0be8e7bf751",
"name": "Iran",
"population": 85000000,
"abb": "IR"
},
"success": true
}
Add E2E Test
For adding addCountry
request to E2E section you should click on the E2E button, like below picture.
Then, when you go to the E2E section, you can see 2 sequense that first one is the default sequence that you should delete that. so, you have one sequence that include your request information(like bottom picture).