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.xin 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
stringandnumberfrom 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
objectfunctionActFntype 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:
schemais the name of the model to which we want to set an action.actNameis just a simple string to identify the act.fnis the function we call when a request arrives for it.validatoris a superstruct object which is called before calling the act fn and validating the given data. Validator includessetandgetobjects.- An optional key named
validationRunTypethat receives the values ofassertandcreateand 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
preActwhich 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 forauthorizationandauthentication. 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 anobjectin which we define the required input information for each function available on the client side. In the desired function, we can get thesetobject information from this address.body.details.set. Note that this object must be of Superstructobjectfunction 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 of0or1for 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]), });selectStructfunction ofcoreApp.schemas.selectStruct. This function has two inputs. The first input is thenameof 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 theselectStructfunction can be entered as anumberor anobject. If entered as an object, the keys of this object must be thenamesof the selected model relationships, and its value can again be anumberor anobjectof 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
servicekey is used to select one of themicroservicesset on the application. You can read more about this here. - The
modelkey is used to select one of theModelsadded to the application. - The
actkey is used to select one of theActsadded to the application. - The
detailskey 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 calledgetandset, we talked a little about it before.set: It contains the information we need in theActfunction. For this reason, we can extractname,population, andabbfrom 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
dockey receives an object of the pure values of the selected model.OptionalUnlessRequiredIdtype is thedocumenttype in the official MongoDB driver. You can read about it here. - The
relationskey receives an object from the relations of this model. There is no relationship here. We will read about this in the next section. - The
optionskey gets the official MongoDB driver options to insertOne. You can read more about this here - The
projectionkey 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.xin 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:
portused to specify the port used to run the server.polygroundthat receives aBooleanvalue that specifies whether the Polyground is available athttp://{server-address}:{port}/playgroundaddress.typeGeneration, which receives aBooleanvalue 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.staticPaththat receives anarrayof paths as astringand makes the content inside these paths statically serveable. We will read more about this later.corswhich receives either the*value or anarrayof URLs as astring, and makes these addresses have the ability to communicate with the server and not receive thecorserror.
Running App
Now you can run
deno run -A mod.tsfor 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).