├── .gitignore
├── Readme.md
├── images
├── add-query.png
├── department-panel.png
├── double-mustache.png
├── dynamic-fields.png
└── file-label.png
├── package-lock.json
├── package.json
└── src
├── custom-query-panel
├── index.js
└── panel.vue
└── custom-query
├── index.js
└── utils
└── config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | dist
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # Directus Custom Query Panel
2 |
3 | Behold the magic 🎩✨ of our simple panel! View your data without the hassle of writing custom endpoints or preparing views - it's like having your cake 🍰 and eating it too, but for data! 💾🎉
4 |
5 |
6 | ## Details
7 |
8 | - Execute custom SQL queries directly from the panel.
9 | - Use dynamic parameters to reuse queries with different values.
10 | - Display query results in a simple tabular structure for easy readability.
11 | - Enhance data visibility and understanding by enabling users to extract meaningful insights from their data which is crucial for CRM and data-driven applications.
12 |
13 |
14 | ## 👀Set Up Instructions
15 |
16 | - Either install through NPM [npm install @7span/directus-extension-custom-query-panel](https://www.npmjs.com/) or [Installing Through the Extensions Folder](https://docs.directus.io/extensions/installing-extensions.html#installing-through-the-extensions-folder).
17 | - Once Installed/configured extension you need hit the below curl request to create a default table(`cqp_queries`) to store the queries.
18 |
19 | **NOTE**: Replace `localhost:8055` with your domain
20 |
21 | ```bash
22 | curl --location --request POST 'http://localhost:8055/custom-query-panel/create-table'
23 | ```
24 |
25 | - Table named `cqp_queries` will be available in your project.
26 |
27 | ## How To use This Extension
28 |
29 | 1. Create the queries in the table (`cqp_queries`) as explained in below example.
30 |
31 | ```bash
32 | select first_name, last_name from employees where department = ${department}
33 | ```
34 |
35 |
36 | 2. This extension provides support of *`global variables`* added in insights like department or week.
37 |
38 |
39 | 3. Use `variables` field give in panel settings below `fields`.
40 |
42 |
43 | 4. Use `double mustache` syntax for entering `value` field to get value from variables.
44 |
45 |
46 | ## 👀 Environment Variables
47 | - You can provide collection name in CUSTOM_QUERY_COLLECTION as per your needs.
48 | - It Also provide support for custom query length using CUSTOM_QUERY_FIELD_LENGTH
49 | ```bash
50 | # default value CUSTOM_QUERY_COLLECTION = "cqp_queries"
51 | CUSTOM_QUERY_COLLECTION="custom_query"
52 | # default value CUSTOM_QUERY_FIELD_LENGTH = 5000
53 | CUSTOM_QUERY_FIELD_LENGTH=10000
54 | ```
55 |
56 |
57 | ## Problem
58 |
59 | - Getting data from database query and show it in a insights panel was missing.
60 |
61 | - The ability to extract and display data from a database query within an insights panel is crucial for CRM and data-driven applications.
62 | - Insights panels serve as a hub for users to access valuable information, gain actionable insights, and make informed decisions.
63 | - This feature bridges the gap between raw data stored in the database and its meaningful interpretation, making Directus an even more versatile platform for managing customer relationships and data-driven business operations.
64 |
65 | ## Extension Type
66 |
67 | - 📦 Bundle ( Panel + Custom Endpoint )
68 |
69 | ## Screenshots
70 |
71 | 
72 | 
73 | 
74 | 
75 | 
76 | ## Collaborators
77 |
78 | - [Harsh Kansagara](https://github.com/theharshin)
79 | - [Jay Bharadia](https://github.com/jay-p-b-7span)
80 | - [Bhagyesh Radiya](https://github.com/bhagyesh-7span)
81 |
82 | ## Contact Details
83 |
84 | - [Linkedin](https://www.linkedin.com/company/7span)
85 | - [Gmail](mailto:yo@7span.com)
86 |
87 | ## 🚧 Please note
88 |
89 | - this extension uses raw query. Use with caution. It might do uninteded actions.
90 | - Roles and permission check for query
91 |
92 | ### Table Fields
93 |
94 | - We have repeater interface with multiple columns support
95 |
96 |
--------------------------------------------------------------------------------
/images/add-query.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/7span/directus-extension-custom-query-panel/fdce7837ef2d8d0be4ce4e66fd6f1eefaa56e471/images/add-query.png
--------------------------------------------------------------------------------
/images/department-panel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/7span/directus-extension-custom-query-panel/fdce7837ef2d8d0be4ce4e66fd6f1eefaa56e471/images/department-panel.png
--------------------------------------------------------------------------------
/images/double-mustache.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/7span/directus-extension-custom-query-panel/fdce7837ef2d8d0be4ce4e66fd6f1eefaa56e471/images/double-mustache.png
--------------------------------------------------------------------------------
/images/dynamic-fields.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/7span/directus-extension-custom-query-panel/fdce7837ef2d8d0be4ce4e66fd6f1eefaa56e471/images/dynamic-fields.png
--------------------------------------------------------------------------------
/images/file-label.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/7span/directus-extension-custom-query-panel/fdce7837ef2d8d0be4ce4e66fd6f1eefaa56e471/images/file-label.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@7span/directus-extension-custom-query-panel",
3 | "version": "1.1.7",
4 | "scripts": {
5 | "build": "directus-extension build",
6 | "dev": "directus-extension build -w --no-minify",
7 | "link": "directus-extension link",
8 | "add": "directus-extension add",
9 | "prepublishOnly": "npm run build"
10 | },
11 | "directus:extension": {
12 | "host": "^9.22.4",
13 | "type": "bundle",
14 | "path": {
15 | "app": "dist/app.js",
16 | "api": "dist/api.js"
17 | },
18 | "entries": [
19 | {
20 | "type": "panel",
21 | "name": "custom-query-panel",
22 | "source": "src/custom-query-panel/index.js"
23 | },
24 | {
25 | "type": "endpoint",
26 | "name": "custom-query",
27 | "source": "src/custom-query/index.js"
28 | }
29 | ]
30 | },
31 | "devDependencies": {
32 | "@directus/extensions-sdk": "9.22.4",
33 | "vue": "^3.2.47"
34 | },
35 | "author": {
36 | "name": "7span",
37 | "email": "yo@7span.com"
38 | },
39 | "repository": {
40 | "type": "git",
41 | "url": "https://github.com/7span/directus-extension-custom-query-panel.git"
42 | },
43 | "homepage": "https://github.com/7span/directus-extension-custom-query-panel#Readme",
44 | "publishConfig": {
45 | "access": "public"
46 | },
47 | "dependencies": {
48 | "dotenv": "^16.3.1",
49 | "paraphrase": "^3.1.1"
50 | },
51 | "files": [
52 | "dist"
53 | ]
54 | }
55 |
--------------------------------------------------------------------------------
/src/custom-query-panel/index.js:
--------------------------------------------------------------------------------
1 | import PanelComponent from "./panel.vue";
2 |
3 | export default {
4 | id: "custom-query",
5 | name: "Custom Query Panel",
6 | icon: "dynamic_form",
7 | description: "This is my custom Query panel!",
8 | component: PanelComponent,
9 | options: [
10 | {
11 | field: "query_id",
12 | name: "Query ID",
13 | type: "string",
14 | meta: {
15 | note: "Enter your Query ID here to retrieve data",
16 | interface: "input",
17 | width: "full",
18 | },
19 | },
20 | {
21 | field: "fields",
22 | name: "Fields",
23 | type: "standard",
24 | meta: {
25 | note: "Enter Table Column(s) for displaying data",
26 | interface: "list",
27 | width: "full",
28 | options: {
29 | fields: [
30 | {
31 | field: "key",
32 | name: "Key",
33 | type: "string",
34 | meta: {
35 | field: "key",
36 | width: "half",
37 | type: "string",
38 | interface: "input",
39 | options: {
40 | placeholder: "first_name",
41 | },
42 | },
43 | },
44 |
45 | {
46 | field: "label",
47 | name: "Label",
48 | type: "string",
49 | meta: {
50 | field: "label",
51 | width: "half",
52 | options: {
53 | placeholder: "First Name",
54 | },
55 | type: "string",
56 | interface: "input",
57 | },
58 | },
59 | {
60 | field: "width",
61 | name: "Width",
62 | type: "string",
63 | schema: {
64 | default_value: "300",
65 | },
66 | meta: {
67 | field: "width",
68 | width: "half",
69 | type: "string",
70 | interface: "input",
71 | },
72 | },
73 | ],
74 | },
75 | },
76 | },
77 |
78 | // Variables
79 | {
80 | field: "variables",
81 | name: "Variables",
82 | type: "standard",
83 | meta: {
84 | interface: "list",
85 | width: "full",
86 | options: {
87 | fields: [
88 | {
89 | field: "key",
90 | name: "Key",
91 | type: "string",
92 | meta: {
93 | field: "key",
94 | width: "half",
95 | options: {
96 | placeholder:
97 | "Enter Dynamic Variable key here",
98 | },
99 | type: "string",
100 | interface: "input",
101 | },
102 | },
103 | {
104 | field: "value",
105 | name: "Value",
106 | type: "string",
107 | meta: {
108 | note: "Enter {{ variable_name }} to work properly",
109 | field: "value",
110 | width: "half",
111 | options: {
112 | placeholder: "{{department}}",
113 | },
114 | type: "string",
115 | interface: "input",
116 | },
117 | },
118 | ],
119 | },
120 | },
121 | },
122 | ],
123 | minWidth: 12,
124 | minHeight: 8,
125 | };
126 |
--------------------------------------------------------------------------------
/src/custom-query-panel/panel.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
95 |
96 |
110 |
--------------------------------------------------------------------------------
/src/custom-query/index.js:
--------------------------------------------------------------------------------
1 | import { dollar as phrase } from "paraphrase";
2 | import { CUSTOM_QUERY_COLLECTION, CUSTOM_QUERY_FIELD_LENGTH, DIRECTUS_FIELDS_OBJ } from "./utils/config";
3 |
4 | export default {
5 | id: "custom-query-panel",
6 | handler: (router, { services, logger, database }) => {
7 | const { ItemsService } = services;
8 |
9 | // Endpoint to execute a custom query
10 | router.get("/execute", async (req, res) => {
11 | try {
12 | // Check if query_id is provided
13 | if (!req.query.query_id) {
14 | return res.status(400).json({ error: "Query ID is required" });
15 | }
16 |
17 | // Initialize the custom query service
18 | const customQueryService = new ItemsService(CUSTOM_QUERY_COLLECTION, { schema: req.schema });
19 |
20 | // Retrieve the custom query based on the provided ID
21 | const customQueryData = await customQueryService.readOne(req.query.query_id);
22 |
23 | // Check if custom query data is retrieved
24 | if (!customQueryData) {
25 | return res.status(404).json({ error: "Custom query not found" });
26 | }
27 |
28 | // Prepare the variables for the custom query
29 | const queryVariables = req.query.variables || [];
30 | const preparedVariables = prepareVariables(queryVariables);
31 |
32 | // Execute the custom query
33 | const RAW_QUERY = phrase(`${customQueryData.query}`, preparedVariables);
34 | logger.debug(`Raw query: ${RAW_QUERY}`);
35 | const executedQueryData = await database.raw(RAW_QUERY);
36 | const fetchedQueryData = executedQueryData[0];
37 |
38 | logger.debug("Custom Query Executed");
39 | return res.status(200).json({ data: fetchedQueryData });
40 | } catch (error) {
41 | logger.error(`error: ${error}`);
42 | return res.status(500).json({ error: `${error.message}` });
43 | }
44 | });
45 |
46 | // Endpoint to create a table for storing custom queries
47 | router.post("/create-table", async (req, res) => {
48 | try {
49 | const customQueryService = new ItemsService('directus_fields', { schema: req.schema });
50 |
51 | // Create a table for custom queries
52 | await database.schema.createTable(CUSTOM_QUERY_COLLECTION, (table) => {
53 | table.increments(), table.string("name"), table.varchar("query", CUSTOM_QUERY_FIELD_LENGTH);
54 | });
55 |
56 | // Create a collection field of type code editor for MYSQL query
57 | await customQueryService.createOne(DIRECTUS_FIELDS_OBJ);
58 |
59 | return res
60 | .status(200)
61 | .json({
62 | message: `Table Created for Custom Query Extension ${CUSTOM_QUERY_COLLECTION}`,
63 | });
64 | } catch (error) {
65 | logger.error(`error: ${error}`);
66 | return res.status(500).json({ error: `${error.message}` });
67 | }
68 | });
69 | },
70 | };
71 |
72 | // Function to prepare the variables for the custom query
73 | function prepareVariables(variables) {
74 | const preparedVariables = {};
75 | variables.forEach((item) => {
76 | const { key, value } = item;
77 | preparedVariables[key] = value
78 | // if (Number.isInteger(value)) {
79 | // preparedVariables[key] = `${parseInt(value)}`;
80 | // } else if (!isNaN(parseFloat(value))) {
81 | // preparedVariables[key] = `${parseFloat(value)}`;
82 | // } else if (typeof value === "string") {
83 | // preparedVariables[key] = `'${value}'`;
84 | // } else {
85 | // console.log(`Unsupported data type for key ${key}`)
86 | // }
87 | });
88 | return preparedVariables;
89 | }
--------------------------------------------------------------------------------
/src/custom-query/utils/config.js:
--------------------------------------------------------------------------------
1 | import dotenv from "dotenv";
2 | dotenv.config();
3 |
4 | const CUSTOM_QUERY_COLLECTION =
5 | process.env.CUSTOM_QUERY_COLLECTION || "cqp_queries";
6 |
7 | const DIRECTUS_FIELDS_OBJ = {
8 | collection: CUSTOM_QUERY_COLLECTION,
9 | field: "query",
10 | interface: "input-code",
11 | options: { language: "sql" },
12 | readonly: 0,
13 | hidden: 0,
14 | sort: 2,
15 | width: "full",
16 | required: 0,
17 | };
18 |
19 | // Setting the length of the custom query field, default is 5000
20 | const CUSTOM_QUERY_FIELD_LENGTH = parseInt(process.env.CUSTOM_QUERY_FIELD_LENGTH) || 5000;
21 |
22 | export { CUSTOM_QUERY_COLLECTION, CUSTOM_QUERY_FIELD_LENGTH, DIRECTUS_FIELDS_OBJ };
23 |
--------------------------------------------------------------------------------