Introduction
Welcome to Qore Client SDK documentation page, this document will guide you to start hacking with Qore. As of now, Qore Client SDK is only accessible in JavaScript Environment, we will add more soon.
Prerequisites
- Node.js 12+.
npx
andnpm
executable.- Qore account, signup here & don't forget to verify your account.
Features
Document caching
Each read operation is cached by default, any similar read request will share the same data. With qore client you might not need an additional state management.
TypeScript Support
Qore cli can generate the schema of your project in TypeScript, meaning that you'll know what to insert and what to read from your qore client.
Getting Started
Install Qore CLI
Install @feedloop/qore-cli globally via npm or yarn.
Installing via npm
npm install --global @feedloop/qore-cli
Installing via yarn
yarn global add @feedloop/qore-cli
You can also run qore-cli via npx
if you prefer not to pollute your global path.
npx @feedloop/qore-cli --help
# example: login to qore via npx
npx @feedloop/qore-cli login
Authenticate yourself
You will be asked to input your email & password. Choose your default project afterwards.
npx @feedloop/qore-cli login
Setup
npx @feedloop/qore-cli create-project --template https://github.com/feedloop/qore-next-template.git <your-new-project-name>
If you start a new project, this is the recommended way to setup a qore project.
This command will create a new project for you, including a starter-kit project selected (in this case, feedloop/qore-next-template) on your current working directory. This starter project includes common SDK initialization that should get you started.
Once created, navigate to your project from your terminal to install the dependencies using $ npm install
.
Hit $ npx next dev
to start running your project locally.
Open this url from your browser and you should see your email being printed http://localhost:3000. Browse the template code to see how it is done.
Now you are ready to:
Setup manually
If you prefer to setup qore manually to an existing project, please follow this guide all the way through.
1) Create a new qore project from your qore dashboard.
2) Create a new directory for your project.
mkdir my-new-project
cd ./my-new-project
3) Initialize package.json
file on your root project directory by triggering npm init -y
, followed by installing required dependencies.
npm install --save @feedloop/qore-client
npm install --save-dev @feedloop/qore-cli
React users: install @feedloop/qore-react to your project.
npm install --save @feedloop/qore-react
4) Set your newly-created project as the current project.
npx qore set-project
5) On your root project directory, run the codegen command to generate required config files. See codegen
npx qore codegen
6) Initialize qore client by creating the following file.
// Create a new file called client.js that contains the following lines
import { QoreClient } from "@feedloop/qore-client";
import config from "./qore.config.json";
import schema from "./qore.schema.json";
const client = new QoreClient(config);
client.init(schema);
export default client;
// Create a new file called qoreContext.js that contains the following lines
import { QoreClient } from "@feedloop/qore-client";
import createQoreContext from "@feedloop/qore-react";
import config from "./qore.config.json";
import schema from "./qore.schema.json";
const client = new QoreClient(config);
client.init(schema);
const qoreContext = createQoreContext(client);
export default qoreContext;
// Add qore context provider to your root component.
const Root = () => {
return (
<qoreContext.context.Provider
value={{
client: qoreContext.client
}}
>
<YourApp />
</qoreContext.context.Provider>
);
};
With this file created, you are ready to:
TypeScript support
The codegen command will also generate type definitions based on your project schema, exported as ProjectSchema
interface from "@feedloop/qore-client"
. All you need to do is feeding this interface to the QoreClient
class initialization.
import { QoreClient, ProjectSchema } from "@feedloop/qore-client";
import config from "./qore.config.json";
import schema from "./qore-schema.json";
const client = new QoreClient<ProjectSchema>(config);
client.init(schema as any);
Codegen
Generate configuration files
npx @feedloop/qore-cli codegen
Generate configuration files to src directory
npx @feedloop/qore-cli codegen --path src
To ensure that you will have the latest version of your configuration files on your project, run this command when:
- The following files doesn't exist on your root project directory.
- Everytime there are changes on your project structure (includes views, fields, tables and forms).
File name | Description |
---|---|
qore.schema.json |
Contains the schema required to run qore client. |
qore.config.json |
Contains the config required to connect to your project. |
qore-env.d.ts |
TypeScript type definitions of your project schema. |
Tips: Add qoreconfig
to your package.json file to only store your configuration files to your desired path (.i.e src
);
{
"qoreconfig": {
"path": "src"
}
}
Reading data
Once initialized, your project views will be accessible via the client instance. You can start reading the data of your view.
const { data, error } = await client
.view("allTasks")
.readRows({ offset: 10, limit: 10, order: "desc" })
.toPromise();
import qoreContext from "./qoreContext";
const Component = () => {
const { data: allTasks, status } = qoreContext.view("allTasks").useListRow({
offset: 10,
limit: 10,
order: "asc"
});
return (
<ul>
{allTasks.map(task => (
<li>{task.name}</li>
))}
</ul>
);
};
data
will contain rows of your allTasks
view. In case an error occured, data
can be null and error
should contain the cause of error.
You can also specify offset
, limit
and order
when performing a read view operation.
Pagination
const operation = client.view("allTasks").readRows({ offset: 10, limit: 10 });
let allTasks = [];
operation.subscribe(({ data }) => {
allTasks = data;
});
await operation.fetchMore({ offset: data.nodes.length, limit: 10 });
// new items are being pushed to allTasks
import qoreContext from "./qoreContext";
const Component = () => {
const { data: allTasks, status, fetchMore } = qoreContext
.view("allTasks")
.useListRow({
offset: 10,
limit: 10
});
return (
<ul>
{allTasks.map(task => (
<li>{task.name}</li>
))}
<button
onClick={() => {
// new items are being pushed to allTasks
fetchMore({ offset: allTasks.length, limit: 10 });
}}
>
Load more
</button>
</ul>
);
};
Fetching more rows can be done by calling the fetchMore
method as demonstrated above. It accepts a pagination config to match your desired items size of the next page data to be fetched.
Reading individual row
const { data, error } = await client
.view("allTasks")
.readRow("some-task-id")
.toPromise();
import qoreContext from "./qoreContext";
const Component = () => {
const { data: someTask, status, error } = qoreContext
.view("allTasks")
.useGetRow("some-task-id");
return (
<ul>
{allTasks.map(task => (
<li>{task.name}</li>
))}
</ul>
);
};
Oftentimes we want to get the detail of a specific row by the ID. Assuming the id is some-task-id, you can fetch it this way:
data
will contain either a single row or null if an error has occured, error
object will tell you the cause.
Caching data
const { data, error } = await client
.view("allTasks")
.readRows(
{ offset: 10, limit: 10, order: "desc" },
{ networkPolicy: "cache-only" }
)
.toPromise();
import qoreContext from "./qoreContext";
const Component = () => {
const { data: allTasks, status, error } = qoreContext
.view("allTasks")
.useListRow(
{
offset: 10,
limit: 10,
order: "asc"
},
{ networkPolicy: "cache-only" }
);
return (
<ul>
{allTasks.map(task => (
<li>{task.name}</li>
))}
</ul>
);
};
A qore client has an internal storage that acts as a cache that is turned on by default to minimize http request.
By setting the networkPolicy
option to cache-only
, you are telling qore client to only get the data from the cache instead of getting it from the server.
networkPolicy
option accepts the following values:
Value | Description |
---|---|
cache-only | Read data only from the cache |
network-only | Read data only from the network |
network-and-cache | Read data from the cache first, then a network request will follow |
Reading data from network-and-cache
may require you to subscribe to the read operation because there will be a follow up result from the network after the operation hits the cache.
const operation = client
.view("allTasks")
.readRows(
{ offset: 10, limit: 10, order: "desc" },
{ networkPolicy: "network-and-cache" }
);
const subscription = operation.subscribe(({ data, error, stale }) => {
if (data && !stale) {
doSomething(data);
subscription.unsubscribe();
}
});
stale
will be true
when it hits the cache, false
when it hits the network. Indicating that the data might be obsolete due to a follow up network request.
Revalidating data
const operation = client
.view("allTasks")
.readRows(
{ offset: 10, limit: 10, order: "desc" },
{ networkPolicy: "network-and-cache" }
);
const subscription = operation.subscribe(({ data, error, stale }) => {
doSomething(data);
});
operation.revalidate();
import qoreContext from "./qoreContext";
const Component = () => {
const { data: allTasks, revalidate } = qoreContext.view("allTasks").useListRow(
{
offset: 10,
limit: 10,
order: "asc",
},
{ networkPolicy: "network-and-cache" }
);
return (
<>
<button onClick={revalidate}>refresh</button>
<ul>
{allTasks.map((task) => (
<li>{task.name}</li>
))}
</ul>
<>
);
};
Oftentimes you want to get the most up-to-date state of your data from the network.
By calling revalidate()
, you are asking qore client to send a network-only
mode to your operation, giving you the most recent state of the data. Think of it as a reload button of your browser tab.
Polling interval
const operation = client
.view("allTasks")
.readRows(
{ offset: 10, limit: 10, order: "desc" },
{ networkPolicy: "network-and-cache", pollInterval: 5000 }
);
const subscription = operation.subscribe(({ data, error, stale }) => {
doSomething(data);
});
import qoreContext from "./qoreContext";
const Component = () => {
const { data: allTasks, revalidate } = qoreContext.view("allTasks").useListRow(
{
offset: 10,
limit: 10,
order: "asc",
},
{ networkPolicy: "network-and-cache", pollInterval: 5000 }
);
return (
<>
<button onClick={revalidate}>refresh</button>
<ul>
{allTasks.map((task) => (
<li>{task.name}</li>
))}
</ul>
<>
);
};
Instead of calling operation.revalidate()
periodically, you can ask qore client to send request periodically by specifying a polling interval option in milisecond.
This operation will be refreshed every 5 seconds, a nice near-realtime effect to your users.
Writing data
Similar to reading data, writing data is accessible from each view object.
Insert a new row
const newRow = await client.view("allTasks").insertRow({ ...data });
import qoreContext from "./qoreContext";
const Component = () => {
const { insertRow, status } = qoreContext.view("allTasks").useInsertRow();
return (
<button
onClick={async () => {
await insertRow({ ...data });
}}
>
insert
</button>
);
};
Insert a data to allTasks
view.
data
must be compliant to the schema of the view, excluding the id
field.
Update a row
await client.view("allTasks").updateRow("some-task-id", {
...data
});
import qoreContext from "./qoreContext";
const Component = () => {
const { updateRow, status } = qoreContext.view("allTasks").useUpdateRow();
return (
<button
onClick={async () => {
await updateRow("some-task-id", { ...data });
}}
>
update
</button>
);
};
Update a data of allTasks
view with an id of some-task-id.
data
must be compliant to the schema of the view, excluding the id
field.
Add & remove relationships
await client.view("allTasks").addRelation(taskId, {
person: [member.id],
links: links.map(link => link.id)
});
await client.view("allTasks").removeRelation(taskId, {
person: [member.id]
});
import qoreContext from "./qoreContext";
const Component = () => {
const { addRelation, removeRelation, statuses, errors } = qoreContext
.view("allTasks")
.useRelation(taskId);
return (
<div>
<button
disabled={statuses.addRelation === "loading"}
onClick={async () => {
await addRelation({
person: [member.id],
links: links.map(link => link.id)
});
}}
>
add relation
</button>
<button
disabled={statuses.removeRelation === "loading"}
onClick={async () => {
await removeRelation({ person: [member.id] });
}}
>
remove relation
</button>
</div>
);
};
Both addRelation
and removeRelation
accept the id
of the target row, followed by an object with the key being the relation name and the value is an array of reference id of the relationship.
In this example we are adding member.id
to the relationship of a specific row on the allTasks
view and then removing it.
Update a row
await client.view("allTasks").updateRow("some-task-id", {
...data
});
import qoreContext from "./qoreContext";
const Component = () => {
const { updateRow, status } = qoreContext.view("allTasks").useUpdateRow();
return (
<button
onClick={async () => {
await updateRow("some-task-id", { ...data });
}}
>
update
</button>
);
};
Update a data of allTasks
view with an id of some-task-id.
data
must be compliant to the schema of the view, excluding the id
field.
Upload a file
const file = document.getElementById('fileInput').files[0];
const url = await client.view("allTasks").upload(file);
await client.view("allTasks").updateRow("some-task-id", {
...data
avatar: url
});
import qoreContext from "./qoreContext";
const Component = () => {
const { updateRow, status } = qoreContext.view("allTasks").useUpdateRow();
const handleUpload = async event => {
const file = e.currentTarget.files?.item(0);
if (!file) return;
const url = await client.view("allTasks").upload(file);
await updateRow("some-task-id", { ...data, avatar: url });
};
return <input type="file" onChange={handleUpload} />;
};
Adding files to a row requires you to upload the file first. The file type of the uploaded files must match with the field target, unwanted file types will be ignored.
The upload()
method accepts a file
variable that is a File item of a FileList object from a file input html element.
<input type="file" id="fileInput" />
Delete a row
await client.view("allTasks").deleteRow("some-task-id");
import qoreContext from "./qoreContext";
const Component = () => {
const { deleteRow, status } = qoreContext.view("allTasks").useDeleteRow();
return (
<button
onClick={async () => {
await deleteRow("some-task-id", { ...data });
}}
>
delete
</button>
);
};
Remove a data of allTasks
view with an id of some-task-id.
Trigger actions
await client.view("allTasks").action("archiveTask").trigger("some-task-id", {
someParams: "someValue"
});
import qoreContext from "./qoreContext";
const Component = () => {
const { action, statuses } = qoreContext
.view("allTasks")
.useActions("some-task-id");
return (
<button
onClick={async () => {
await action("archiveTask").trigger({
someParams: "someValue"
});
}}
>
archive task
</button>
);
};
Each qore row can have one or more action triggers, an action trigger may require parameters.
Send form inputs
await client
.view("allTasks")
.form("newTask")
.sendForm({ task: "Some task", done: true });
import qoreContext from "./qoreContext";
const Component = () => {
const { send, status } = qoreContext.view("allTasks").useForm("newTask");
return (
<button
onClick={async () => {
await send({ task: "Some task", done: true });
}}
>
Add new task
</button>
);
};
Each qore view has one or more forms, sending forms may require parameters.
Authenticating your user
// give qore client a way to access user token
const client = new QoreClient({..config, getToken: () => cookies.get("token")})
const token = await client.authenticate(
"email@yourcompany.com",
"plain password"
);
// save token to somewhere safe
cookies.set("token", token);
// log a user out by removing the token from your storage
cookies.remove("token");
// qoreContext.js
// give qore client a way to access user token
const client = new QoreClient({..config, getToken: () => cookies.get("token")})
// YourComponent.js
const YourComponent = () => {
const client = qoreContext.useClient();
const handleLogout = () => {
// log a user out by removing the token from your storage
cookies.remove("token");
}
const handleLogin = async (email, password) => {
const token = await client.authenticate(
"email@yourcompany.com",
"plain password"
);
// save token to somewhere safe
cookies.set("token", token);
};
// call handleLogin whenever your form is ready
return <form onSubmit={handleLogin}>Some form</form>;
};
As you can register new users to qore, you might need to log them in to your application.
Get current user
const Component = () => {
const { user } = qoreContext.useCurrentUser();
return <div>{user ? user.email : "Loading..."}</div>;
};
const currentUser = await client.currentUser();
If the token is valid, an object that describes the current user will be returned from this call.
Error handling
const client = new QoreClient({..config, onError: (error) => {
switch (error.message) {
case "Request failed with status code 500":
modal.message("An error has occured");
break;
case "Request failed with status code 401":
router.push("/login");
break;
}
})})
Any error that occurs along the lifetime of a qore client will be emitted via the onError
callback supplied during initialization.