create-dql-app 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/bin/create-dql-app.mjs +165 -0
- package/package.json +33 -0
- package/templates/empty/README.md +18 -0
- package/templates/empty/cdql.yaml +13 -0
- package/templates/empty/notebooks/welcome.dql +17 -0
- package/templates/jaffle-shop/README.md +34 -0
- package/templates/jaffle-shop/blocks/finance/revenue_by_segment.dql +23 -0
- package/templates/jaffle-shop/cdql.yaml +17 -0
- package/templates/jaffle-shop/dashboards/overview.dql +22 -0
- package/templates/jaffle-shop/notebooks/welcome.dql +44 -0
- package/templates/jaffle-shop/semantic-layer/dimensions/customer.yaml +14 -0
- package/templates/jaffle-shop/semantic-layer/metrics/revenue.yaml +9 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# create-dql-app
|
|
2
|
+
|
|
3
|
+
The fastest way to start a DQL project.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx create-dql-app my-analytics
|
|
7
|
+
cd my-analytics
|
|
8
|
+
npx @duckcodeailabs/dql-cli notebook
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Opens a running notebook at <http://localhost:5173> in under 5 minutes on a
|
|
12
|
+
clean machine. No global install required.
|
|
13
|
+
|
|
14
|
+
## Templates
|
|
15
|
+
|
|
16
|
+
| Template | What you get |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `jaffle-shop` *(default)* | DuckDB + the Jaffle Shop dataset + a sample notebook, certified block, and dashboard |
|
|
19
|
+
| `empty` | Just a `cdql.yaml` and project layout — bring your own warehouse |
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx create-dql-app finance-reports --template empty
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Flags
|
|
26
|
+
|
|
27
|
+
| Flag | Default | Meaning |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `--template <name>` | `jaffle-shop` | Starter template |
|
|
30
|
+
| `--no-install` | off | Skip downloading the Jaffle Shop seed data |
|
|
31
|
+
|
|
32
|
+
## Docs
|
|
33
|
+
|
|
34
|
+
- [Quickstart](https://docs.duckcode.ai/get-started/quickstart/)
|
|
35
|
+
- [Concepts](https://docs.duckcode.ai/get-started/concepts/)
|
|
36
|
+
- [Connect your own warehouse](https://docs.duckcode.ai/guides/connect-warehouse/)
|
|
37
|
+
|
|
38
|
+
## License
|
|
39
|
+
|
|
40
|
+
MIT
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// create-dql-app — self-contained scaffolder.
|
|
3
|
+
//
|
|
4
|
+
// Contract (from the v1.0 "demo gate"): on a clean machine, `npx
|
|
5
|
+
// create-dql-app` followed by `cd … && npx @duckcodeailabs/dql-cli notebook`
|
|
6
|
+
// produces a running notebook in under 5 minutes.
|
|
7
|
+
//
|
|
8
|
+
// Design: this package writes template files *itself* rather than
|
|
9
|
+
// delegating to `dql init`, so it stays self-contained and installable
|
|
10
|
+
// with zero-peer-dep friction. Templates live under ../templates/ and are
|
|
11
|
+
// copied verbatim.
|
|
12
|
+
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { dirname, join, resolve, basename } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const VERSION = '0.11.0';
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const TEMPLATES_DIR = resolve(__dirname, '..', 'templates');
|
|
21
|
+
|
|
22
|
+
// Tiny ANSI helpers — no dep on chalk/kleur so the bin runs before
|
|
23
|
+
// `npm install` finishes on slow machines.
|
|
24
|
+
const c = {
|
|
25
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
26
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
27
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
28
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
29
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
30
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function usage() {
|
|
34
|
+
console.log(`create-dql-app ${VERSION}
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
npx create-dql-app <project-dir> [options]
|
|
38
|
+
|
|
39
|
+
Options:
|
|
40
|
+
--template <name> Starter template: jaffle-shop (default), empty
|
|
41
|
+
--help, -h Show this help
|
|
42
|
+
--version, -v Show version
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
npx create-dql-app my-analytics
|
|
46
|
+
npx create-dql-app finance-reports --template empty
|
|
47
|
+
`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseArgs(argv) {
|
|
51
|
+
const args = { dir: null, template: 'jaffle-shop' };
|
|
52
|
+
for (let i = 0; i < argv.length; i++) {
|
|
53
|
+
const a = argv[i];
|
|
54
|
+
if (a === '--help' || a === '-h') { usage(); process.exit(0); }
|
|
55
|
+
if (a === '--version' || a === '-v') { console.log(VERSION); process.exit(0); }
|
|
56
|
+
if (a === '--template') { args.template = argv[++i]; continue; }
|
|
57
|
+
if (a.startsWith('--')) { console.error(`Unknown flag: ${a}`); process.exit(2); }
|
|
58
|
+
if (!args.dir) args.dir = a;
|
|
59
|
+
}
|
|
60
|
+
return args;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isEmptyDir(dir) {
|
|
64
|
+
try { return readdirSync(dir).length === 0; } catch { return true; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function copyDir(src, dst) {
|
|
68
|
+
mkdirSync(dst, { recursive: true });
|
|
69
|
+
for (const entry of readdirSync(src)) {
|
|
70
|
+
const s = join(src, entry);
|
|
71
|
+
const d = join(dst, entry);
|
|
72
|
+
if (statSync(s).isDirectory()) copyDir(s, d);
|
|
73
|
+
else writeFileSync(d, readFileSync(s));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function substitute(file, vars) {
|
|
78
|
+
const raw = readFileSync(file, 'utf-8');
|
|
79
|
+
const out = raw.replace(/\{\{(\w+)\}\}/g, (_, k) => vars[k] ?? `{{${k}}}`);
|
|
80
|
+
if (out !== raw) writeFileSync(file, out, 'utf-8');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function walk(dir) {
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const entry of readdirSync(dir)) {
|
|
86
|
+
const p = join(dir, entry);
|
|
87
|
+
if (statSync(p).isDirectory()) out.push(...walk(p));
|
|
88
|
+
else out.push(p);
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function detectDbtSibling(target) {
|
|
94
|
+
// Walk up 2 levels looking for a dbt_project.yml — common layout is
|
|
95
|
+
// myproject/dql + myproject/dbt, or myproject with dbt + dql as siblings.
|
|
96
|
+
for (const rel of ['..', '../..', '../dbt', '../../dbt']) {
|
|
97
|
+
const probe = resolve(target, rel, 'dbt_project.yml');
|
|
98
|
+
if (existsSync(probe)) return resolve(target, rel);
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function main() {
|
|
104
|
+
const args = parseArgs(process.argv.slice(2));
|
|
105
|
+
if (!args.dir) {
|
|
106
|
+
console.error('Error: project directory is required.\n');
|
|
107
|
+
usage();
|
|
108
|
+
process.exit(2);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const target = resolve(process.cwd(), args.dir);
|
|
112
|
+
const projectName = basename(target);
|
|
113
|
+
|
|
114
|
+
if (existsSync(target) && !isEmptyDir(target)) {
|
|
115
|
+
console.error(c.red(`✗ Target directory "${args.dir}" exists and is not empty.`));
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const tplDir = join(TEMPLATES_DIR, args.template);
|
|
120
|
+
if (!existsSync(tplDir)) {
|
|
121
|
+
console.error(c.red(`✗ Unknown template: ${args.template}`));
|
|
122
|
+
console.error(` Available: ${readdirSync(TEMPLATES_DIR).join(', ')}`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log(c.cyan(`\n⌁ create-dql-app ${VERSION}`));
|
|
127
|
+
console.log(` scaffolding ${c.bold(projectName)} (template: ${c.bold(args.template)})\n`);
|
|
128
|
+
|
|
129
|
+
copyDir(tplDir, target);
|
|
130
|
+
|
|
131
|
+
const dbtSibling = detectDbtSibling(target);
|
|
132
|
+
const vars = {
|
|
133
|
+
PROJECT_NAME: projectName,
|
|
134
|
+
YEAR: String(new Date().getFullYear()),
|
|
135
|
+
DBT_PROJECT_DIR: dbtSibling ? resolve(dbtSibling) : '../my-dbt-project',
|
|
136
|
+
DBT_DETECTED: dbtSibling ? 'true' : 'false',
|
|
137
|
+
};
|
|
138
|
+
for (const f of walk(target)) substitute(f, vars);
|
|
139
|
+
|
|
140
|
+
if (dbtSibling) {
|
|
141
|
+
console.log(c.dim(` detected sibling dbt project at ${dbtSibling}`));
|
|
142
|
+
console.log(c.dim(` wired into cdql.yaml — run 'dql sync dbt' to import\n`));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Best-effort: try to run `git init` so users get a clean first commit.
|
|
146
|
+
const gitResult = spawnSync('git', ['init', '-q'], { cwd: target });
|
|
147
|
+
if (gitResult.status === 0) console.log(c.dim(' initialized git repo'));
|
|
148
|
+
|
|
149
|
+
console.log(`
|
|
150
|
+
${c.green('✓ Ready.')} Next steps:
|
|
151
|
+
|
|
152
|
+
${c.bold(`cd ${args.dir}`)}
|
|
153
|
+
${c.bold('npx @duckcodeailabs/dql-cli notebook')}
|
|
154
|
+
|
|
155
|
+
Your notebook will open at ${c.cyan('http://localhost:5173')}.
|
|
156
|
+
|
|
157
|
+
Docs: ${c.cyan('https://docs.duckcode.ai')}
|
|
158
|
+
Issues: ${c.cyan('https://github.com/duckcode-ai/dql/issues')}
|
|
159
|
+
`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
main().catch((e) => {
|
|
163
|
+
console.error(c.red(`\n✗ ${e.message}`));
|
|
164
|
+
process.exit(1);
|
|
165
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-dql-app",
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "Scaffold a new DQL project. Run with: npx create-dql-app <name>",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "DuckCode AI Labs",
|
|
7
|
+
"homepage": "https://docs.duckcode.ai",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/duckcode-ai/dql.git",
|
|
11
|
+
"directory": "packages/create-dql-app"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["dql", "dbt", "analytics", "notebook", "semantic-layer", "lineage"],
|
|
14
|
+
"bin": {
|
|
15
|
+
"create-dql-app": "./bin/create-dql-app.mjs"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin",
|
|
19
|
+
"templates",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "node test/smoke.mjs",
|
|
25
|
+
"build": "echo 'no build step — pure JS'"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
An empty DQL project, scaffolded by `create-dql-app --template empty`.
|
|
4
|
+
|
|
5
|
+
## Connect your warehouse
|
|
6
|
+
|
|
7
|
+
Edit `cdql.yaml` — DQL ships 15 drivers out of the box:
|
|
8
|
+
[docs.duckcode.ai/reference/connectors](https://docs.duckcode.ai/reference/connectors/).
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx @duckcodeailabs/dql-cli test-connection
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Start the notebook
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx @duckcodeailabs/dql-cli notebook
|
|
18
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
project:
|
|
2
|
+
name: {{PROJECT_NAME}}
|
|
3
|
+
version: 1
|
|
4
|
+
|
|
5
|
+
connections:
|
|
6
|
+
# Swap this for your warehouse — postgres, snowflake, bigquery, etc.
|
|
7
|
+
# See https://docs.duckcode.ai/reference/connectors/
|
|
8
|
+
default:
|
|
9
|
+
driver: duckdb
|
|
10
|
+
path: ./warehouse.duckdb
|
|
11
|
+
|
|
12
|
+
governance:
|
|
13
|
+
required_fields: [domain, owner, description]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// dql-format: 1
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
type: markdown
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# {{PROJECT_NAME}}
|
|
8
|
+
|
|
9
|
+
Welcome to DQL. Start by connecting a warehouse (`cdql.yaml`), then replace
|
|
10
|
+
this cell with your first query.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
type: sql
|
|
14
|
+
name: first_query
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
select 1 as hello;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
A DQL analytics project, scaffolded by `create-dql-app`.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @duckcodeailabs/dql-cli notebook
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Opens the notebook at <http://localhost:5173>.
|
|
12
|
+
|
|
13
|
+
## Layout
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
cdql.yaml project config (connections, dbt, governance)
|
|
17
|
+
notebooks/ analytics notebooks (.dql)
|
|
18
|
+
blocks/ certified reusable blocks, grouped by domain
|
|
19
|
+
semantic-layer/ metrics + dimensions authored locally
|
|
20
|
+
dashboards/ compiled static HTML dashboards (git-ignored)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Next steps
|
|
24
|
+
|
|
25
|
+
1. **Run the welcome notebook** — `notebooks/welcome.dql`
|
|
26
|
+
2. **Connect your warehouse** — [docs.duckcode.ai/guides/connect-warehouse](https://docs.duckcode.ai/guides/connect-warehouse/)
|
|
27
|
+
3. **Import your dbt project** — [docs.duckcode.ai/guides/import-dbt](https://docs.duckcode.ai/guides/import-dbt/)
|
|
28
|
+
4. **Author a certified block** — [docs.duckcode.ai/guides/authoring-blocks](https://docs.duckcode.ai/guides/authoring-blocks/)
|
|
29
|
+
|
|
30
|
+
## Learn
|
|
31
|
+
|
|
32
|
+
- [Quickstart](https://docs.duckcode.ai/get-started/quickstart/)
|
|
33
|
+
- [Concepts](https://docs.duckcode.ai/get-started/concepts/)
|
|
34
|
+
- [CLI reference](https://docs.duckcode.ai/reference/cli/)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// dql-format: 1
|
|
2
|
+
|
|
3
|
+
block revenue_by_segment {
|
|
4
|
+
domain: "finance"
|
|
5
|
+
owner: "analytics@{{PROJECT_NAME}}.local"
|
|
6
|
+
tags: ["revenue", "sample"]
|
|
7
|
+
description: "Gross revenue grouped by customer segment."
|
|
8
|
+
|
|
9
|
+
query: |
|
|
10
|
+
select
|
|
11
|
+
c.segment,
|
|
12
|
+
sum(o.order_total) as revenue,
|
|
13
|
+
count(distinct o.customer_id) as customers
|
|
14
|
+
from orders o
|
|
15
|
+
join customers c on c.customer_id = o.customer_id
|
|
16
|
+
group by 1
|
|
17
|
+
order by revenue desc
|
|
18
|
+
|
|
19
|
+
visualization: bar(x: "segment", y: "revenue", title: "Revenue by segment")
|
|
20
|
+
|
|
21
|
+
tests:
|
|
22
|
+
- row_count > 0
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
project:
|
|
2
|
+
name: {{PROJECT_NAME}}
|
|
3
|
+
version: 1
|
|
4
|
+
|
|
5
|
+
connections:
|
|
6
|
+
default:
|
|
7
|
+
driver: duckdb
|
|
8
|
+
path: ./warehouse.duckdb
|
|
9
|
+
|
|
10
|
+
# Remove this block if you aren't using dbt. DQL reads target/manifest.json
|
|
11
|
+
# directly — see https://docs.duckcode.ai/guides/import-dbt/
|
|
12
|
+
dbt:
|
|
13
|
+
projectDir: {{DBT_PROJECT_DIR}}
|
|
14
|
+
manifestPath: target/manifest.json
|
|
15
|
+
|
|
16
|
+
governance:
|
|
17
|
+
required_fields: [domain, owner, description]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// dql-format: 1
|
|
2
|
+
|
|
3
|
+
dashboard: {
|
|
4
|
+
title: "{{PROJECT_NAME}} — Overview"
|
|
5
|
+
description: "Revenue overview generated by create-dql-app."
|
|
6
|
+
layout: "grid"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
type: markdown
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Overview — {{YEAR}}
|
|
14
|
+
|
|
15
|
+
Compiled with `dql compile dashboards/overview.dql --out build/`. Host the
|
|
16
|
+
resulting HTML anywhere.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
type: dql
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
@block("revenue_by_segment")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// dql-format: 1
|
|
2
|
+
|
|
3
|
+
// Welcome to DQL. This notebook runs against a DuckDB-backed Jaffle Shop
|
|
4
|
+
// dataset — the same demo data dbt ships. Press ⌘↵ on each cell.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
type: markdown
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# {{PROJECT_NAME}}
|
|
11
|
+
|
|
12
|
+
A tour of DQL in four cells. Run them top-to-bottom.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
type: sql
|
|
16
|
+
name: orders_overview
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
select
|
|
20
|
+
count(*) as total_orders,
|
|
21
|
+
count(distinct customer_id) as unique_customers,
|
|
22
|
+
sum(order_total) as lifetime_revenue
|
|
23
|
+
from orders;
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
type: sql
|
|
27
|
+
name: daily_revenue
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
select
|
|
31
|
+
order_date,
|
|
32
|
+
sum(order_total) as revenue
|
|
33
|
+
from orders
|
|
34
|
+
group by 1
|
|
35
|
+
order by 1;
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
type: dql
|
|
39
|
+
name: revenue_by_segment
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
// Reference a certified block. The block lives in blocks/finance/ and can
|
|
43
|
+
// be reused across notebooks, dashboards, and other blocks.
|
|
44
|
+
@block("revenue_by_segment")
|