Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -288,16 +288,6 @@ jobs:
- name: Verify bundled SDKs in both template locks
run: pnpm exec tsx tools/check-template-artifact.ts pr-template

# The scaffold's own vitest suite, run against the tarballs built above
# rather than the published versions template/package.json pins. This is
# the only CI check that exercises the template's example test, and the
# only one that consumes @databricks/appkit/testing the way a customer
# does — through a real npm install of a packed tarball. Before the zip,
# so a broken example fails the build instead of shipping.
- name: Run the template's own tests
working-directory: pr-template
run: npm test

# npm install above runs under the JFrog .npmrc (setup-jfrog-npm), which
# bakes internal registry URLs into the regenerated locks. Rewrite them back
# to public npm and fail-closed if any non-public registry remains, so the
Expand All @@ -312,6 +302,20 @@ jobs:
working-directory: pr-template
run: zip -r "../appkit-template-${{ steps.version.outputs.version }}.zip" . -x 'node_modules/*'

# The scaffold's own vitest suite, run against the tarballs built above
# rather than the published versions template/package.json pins. This is
# the only CI check that exercises the template's example test, and the
# only one that consumes @databricks/appkit/testing the way a customer
# does — through a real npm install of a packed tarball. The example test
# is wrapped in a Go template conditional (`.skipExamples` leaves it
# out), so render it first: after the zip, which must keep the
# conditional, and before the upload, so a broken example fails the build
# instead of shipping.
- name: Run the template's own tests
run: |
pnpm exec tsx tools/template-render.ts pr-template/server/example.test.ts
cd pr-template && npm test

- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
Expand Down
1 change: 1 addition & 0 deletions docs/docs/development/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Files named with a `_` prefix are renamed to `.` prefix (e.g. `_gitignore` → `
| `.dotEnv.example` | Generated `.env.example` content with placeholders |
| `.bundle.*` | Generated `databricks.yml` sections (variables, resources, target variables) |
| `.appEnv` | Generated `app.yaml` env entries |
| `.skipExamples` | When `true`, the template leaves out its example queries (`config/queries/hello_world.sql`, `mocked_sales.sql`) and example test (`server/example.test.ts`), and the Analytics page reads no queries. Meant for app builders that replace the starter files. No released Databricks CLI sets it yet (a `databricks apps init` flag for it is planned), so it's unset and the examples are kept. Test it as `{{if not .skipExamples}}` so an unset variable keeps them. |

### Conditional content

Expand Down
2 changes: 1 addition & 1 deletion packages/appkit/src/plugins/analytics/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"Before init, ensure the SQL Warehouse passed via --set analytics.sql-warehouse.id is running"
],
"should": [
"After init, ensure config/queries/ has at least one .sql file before running npm run typegen"
"Add .sql files to config/queries/ only for queries the app runs; typegen and build work without the folder"
]
}
},
Expand Down
43 changes: 43 additions & 0 deletions template/README.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,49 @@ There are a few commands to help you with code quality:
{{or .packageManager "pnpm"}} run format
{{or .packageManager "pnpm"}} run format:fix
```
{{- if .plugins.analytics}}

## SQL Queries

The Analytics plugin runs the `.sql` files in `config/queries/`. The file name is the query key:

- `config/queries/<query_key>.sql` runs as the app's service principal, with one cache shared by all users
- `config/queries/<query_key>.obo.sql` runs as the signed-in user, with a cache per user

Declare each parameter in a `-- @param <name> <TYPE>` comment and reference it as `:name`:

```sql
-- @param max_month INT
SELECT month, revenue
FROM my_catalog.my_schema.sales
WHERE month_num <= :max_month
ORDER BY month_num;
```

Read the result in the client with `useAnalyticsQuery`, or pass the query key to a chart:

```tsx
import { AreaChart, useAnalyticsQuery } from '@databricks/appkit-ui/react';
import { sql } from '@databricks/appkit-ui/js';

const params = { max_month: sql.number(6) };
const { data, loading, error } = useAnalyticsQuery('sales', params);

<AreaChart queryKey="sales" parameters={params} />;
```

`{{or .packageManager "pnpm"}} run typegen` generates the result types of every query into `shared/appkit-types/`. See the [Analytics plugin documentation](https://developers.databricks.com/docs/appkit/v0/plugins/analytics).
{{- end}}

## Testing

`{{or .packageManager "pnpm"}} test` runs the [Vitest](https://vitest.dev/) suite. `@databricks/appkit/testing` tests plugins with no Databricks workspace, credentials or network:

- `createTestApp({ plugins })` boots the app on an ephemeral port so you can call its routes over HTTP. Call `close()` when done.
- `createTestPluginContext()` attaches a plugin to a real context with faked edges, for unit tests without a server.
- `expectStream(source).toEmit(...)` asserts the ordered events of a streaming handler.

See the [testing documentation](https://developers.databricks.com/docs/appkit/v0/plugins/testing).

## Deployment with Databricks Asset Bundles

Expand Down
2 changes: 1 addition & 1 deletion template/appkit.plugins.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions template/client/src/pages/analytics/AnalyticsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
{{if .plugins.analytics -}}
{{if .skipExamples -}}
import { Card, CardContent, CardHeader, CardTitle } from '@databricks/appkit-ui/react';

export function AnalyticsPage() {
return (
<div className="w-full max-w-7xl mx-auto">
<Card className="shadow-lg">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#653 removes shadow-lg from the other Analytics cards. Whichever of the two lands second should drop it here too, so the placeholder matches.

<CardHeader>
<CardTitle>Analytics</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">
No queries yet. Add a SQL file such as <code>config/queries/my_query.sql</code> and read it with{' '}
<code>{"useAnalyticsQuery('my_query')"}</code>.
</CardContent>
</Card>
</div>
);
}
{{- else -}}
import {
useAnalyticsQuery,
AreaChart,
Expand Down Expand Up @@ -106,3 +125,4 @@ export function AnalyticsPage() {
);
}
{{- end}}
{{- end}}
2 changes: 1 addition & 1 deletion template/config/queries/hello_world.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{if .plugins.analytics -}}
{{if and .plugins.analytics (not .skipExamples) -}}
-- @param message STRING
SELECT :message AS value;
{{- end}}
2 changes: 1 addition & 1 deletion template/config/queries/mocked_sales.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{if .plugins.analytics -}}
{{if and .plugins.analytics (not .skipExamples) -}}
-- @param max_month_num INT
WITH mock_data AS (
SELECT 'January' AS month, 1 AS month_num, 65000 AS revenue, 45000 AS expenses, 850 AS customers
Expand Down
11 changes: 8 additions & 3 deletions template/server/example.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{{if not .skipExamples -}}
import { Plugin, type PluginManifest, toPlugin } from '@databricks/appkit';
import { createTestApp, createTestPluginContext, expectStream } from '@databricks/appkit/testing';
import { describe, expect, test } from 'vitest';
Expand Down Expand Up @@ -34,15 +35,17 @@ class GreeterPlugin extends Plugin {
resources: { required: [], optional: [] },
} as PluginManifest<'greeter'>;

async setup() {
setup() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#733 makes the same require-await fix here. Keep it in one of the two PRs, or whichever lands second conflicts on this file.

// Routes registered here are captured by createTestPluginContext().routes.
this.context?.addRoute('get', '/hello', (_req, res) => {
res.end();
});
return Promise.resolve();
}

// A stand-in for a streaming handler: yields SSE-style event objects.
async *greet(name: string) {
// expectStream also takes async iterables and SSE responses.
*greet(name: string) {
yield { type: 'greeting_start', name };
yield { type: 'greeting_end', message: `Hello, ${name}!` };
}
Expand All @@ -53,9 +56,10 @@ class GreeterPlugin extends Plugin {
name: 'greet',
method: 'post',
path: '/greet',
handler: async (req, res) => {
handler: (req, res) => {
const { name } = req.body as { name: string };
res.json({ message: `Hello, ${name}!` });
return Promise.resolve();
},
});
}
Expand Down Expand Up @@ -98,3 +102,4 @@ describe('testing kit example', () => {
}
});
});
{{- end}}
45 changes: 23 additions & 22 deletions tools/smoke-test-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@
* - {{.projectName}} → smoke-test-app
* - {{.appDescription}} → Smoke test app
* - {{.appEnv}} → (empty, which means the conditional block is omitted)
* - {{.skipExamples}} → the --skip-examples flag
*
* Usage:
* tsx tools/smoke-test-template.ts [--output-dir <path>] [--package-manager <pm>] [--run]
* tsx tools/smoke-test-template.ts [--output-dir <path>] [--package-manager <pm>] [--run] [--skip-examples]
*
* Options:
* --output-dir Optional. Scratch directory for the scaffolded app.
* Must be under .smoke-test. Defaults to ".smoke-test/app".
* --package-manager Optional. Package manager to scaffold (pnpm or npm).
* Defaults to "pnpm". (short: -p)
* --run Install, build with prebuild, and check HTTP startup.
* --skip-examples Set the `.skipExamples` template variable, as the Builder
* will: no example queries or example test.
*/

import {
Expand All @@ -34,6 +37,10 @@ import {
import { join, resolve } from "node:path";
import { parseArgs } from "node:util";

import {
renderTemplateConditionals,
type TemplateVars,
} from "./template-render";
import {
runTemplateSmoke,
selectSmokePackageManager,
Expand All @@ -46,6 +53,7 @@ const { values } = parseArgs({
"output-dir": { type: "string", default: ".smoke-test/app" },
"package-manager": { type: "string", default: "pnpm", short: "p" },
run: { type: "boolean", default: false },
"skip-examples": { type: "boolean", default: false },
},
strict: true,
});
Expand Down Expand Up @@ -76,6 +84,13 @@ console.log(`✓ Copied template → ${outputDir}`);

selectSmokePackageManager(SCRATCH_DIR, packageManager);

// Server-only scaffold: no plugins selected, so every plugin block is dropped.
const templateVars: TemplateVars = {
plugins: {},
appEnv: "",
skipExamples: values["skip-examples"],
};

// Render placeholders in key files
const placeholders = {
"{{.projectName}}": "smoke-test-app",
Expand All @@ -86,7 +101,7 @@ const placeholders = {
/**
* Recursively process all files in the scaffold directory to:
* 1. Replace simple placeholders ({{.projectName}}, {{.appDescription}})
* 2. Remove Go template conditionals for plugins and appEnv
* 2. Render Go template conditionals on plugins, appEnv and skipExamples
*/
function processAllFiles(dir: string): void {
const entries = readdirSync(dir, { withFileTypes: true });
Expand All @@ -111,6 +126,7 @@ function processAllFiles(dir: string): void {
fullPath.endsWith(".ts") ||
fullPath.endsWith(".json") ||
fullPath.endsWith(".html") ||
fullPath.endsWith(".sql") ||
fullPath.endsWith(".tmpl") ||
fullPath.endsWith(".yaml") ||
fullPath.endsWith(".yml"))
Expand All @@ -126,26 +142,11 @@ function processAllFiles(dir: string): void {
}
}

// Remove Go template conditionals: {{- if .appEnv}} blocks
if (content.includes("{{") && content.includes(".appEnv")) {
const beforeLen = content.length;
content = content.replace(
/{{-?\s*if\s+\.appEnv\s*}}[\s\S]*?{{-?\s*end\s*}}/g,
"",
);
if (beforeLen !== content.length) {
modified = true;
}
}

// Remove Go template conditionals: {{- if .plugins.*}} blocks
if (content.includes("{{") && content.includes(".plugins.")) {
const beforeLen = content.length;
content = content.replace(
/{{-?\s*if\s+\.plugins\.[a-zA-Z0-9_-]+\s*-?\s*}}[\s\S]*?{{-?\s*end\s*-?\s*}}/g,
"",
);
if (beforeLen !== content.length) {
// Render the {{if}} chains on plugins, appEnv and skipExamples
if (content.includes("{{")) {
const rendered = renderTemplateConditionals(content, templateVars);
if (rendered !== content) {
content = rendered;
modified = true;
}
}
Expand Down
Loading
Loading