create-oke 0.2.6 → 0.3.2
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 +26 -35
- package/package.json +10 -12
- package/src/cli.test.ts +124 -144
- package/src/cli.ts +62 -160
- package/src/docs-origin.ts +2 -3
- package/src/index.ts +1 -2
- package/src/scaffold.ts +52 -32
- package/src/templates.ts +12 -91
- package/src/transform.ts +60 -10
- package/template/.env.example +56 -0
- package/template/README.md +77 -0
- package/template/bun.lock +845 -0
- package/template/drizzle.config.ts +25 -0
- package/{templates/standard → template}/oke.config.ts +22 -25
- package/{templates/standard → template}/package.json +3 -0
- package/{templates/minimal → template}/src/core.ts +1 -1
- package/template/src/schema.decl.ts +12 -0
- package/examples/linkly/oke.config.ts +0 -10
- package/examples/linkly/package.json +0 -15
- package/examples/linkly/src/app.ts +0 -21
- package/examples/linkly/src/core.ts +0 -4
- package/examples/linkly/src/flows/analytics/index.ts +0 -24
- package/examples/linkly/src/flows/links/index.ts +0 -59
- package/examples/linkly/src/flows/links/shapes.ts +0 -22
- package/examples/linkly/src/flows/links/signals.ts +0 -13
- package/examples/linkly/src/gates.ts +0 -8
- package/examples/linkly/src/schema.ts +0 -17
- package/examples/linkly/tests/linkly.test.ts +0 -19
- package/examples/notes/oke.config.ts +0 -14
- package/examples/notes/package.json +0 -16
- package/examples/notes/src/app.ts +0 -13
- package/examples/notes/src/core.ts +0 -4
- package/examples/notes/src/flows/notes/index.ts +0 -44
- package/examples/notes/src/schema.ts +0 -9
- package/examples/notes/tests/notes.test.ts +0 -10
- package/examples/provisions/oke.config.ts +0 -17
- package/examples/provisions/package.json +0 -15
- package/examples/provisions/src/app.ts +0 -60
- package/examples/provisions/src/channels.ts +0 -14
- package/examples/provisions/src/core.ts +0 -4
- package/examples/provisions/src/flows/notifications/index.ts +0 -19
- package/examples/provisions/src/flows/orders/index.ts +0 -70
- package/examples/provisions/src/flows/orders/shapes.ts +0 -21
- package/examples/provisions/src/flows/orders/signals.ts +0 -19
- package/examples/provisions/src/flows/payments/index.ts +0 -37
- package/examples/provisions/src/flows/payments/shapes.ts +0 -3
- package/examples/provisions/src/flows/payments/stripe.ts +0 -18
- package/examples/provisions/src/gates.ts +0 -4
- package/examples/provisions/src/locales/ar.ts +0 -1
- package/examples/provisions/src/locales/en.ts +0 -1
- package/examples/provisions/src/plugins/audit-schema.ts +0 -2
- package/examples/provisions/src/plugins/audit.ts +0 -14
- package/examples/provisions/src/schema.ts +0 -16
- package/examples/provisions/src/vault.ts +0 -16
- package/examples/provisions/tests/orders.test.ts +0 -18
- package/examples/skyport/evals/triage.jsonl +0 -1
- package/examples/skyport/oke.config.ts +0 -44
- package/examples/skyport/oke.images.lock +0 -4
- package/examples/skyport/package.json +0 -16
- package/examples/skyport/src/ai.ts +0 -25
- package/examples/skyport/src/app.ts +0 -57
- package/examples/skyport/src/channels.ts +0 -12
- package/examples/skyport/src/core.ts +0 -4
- package/examples/skyport/src/flows/bookings/index.ts +0 -50
- package/examples/skyport/src/flows/bookings/shapes.ts +0 -6
- package/examples/skyport/src/flows/bookings/signals.ts +0 -9
- package/examples/skyport/src/flows/notifications/index.ts +0 -16
- package/examples/skyport/src/flows/payments/index.ts +0 -14
- package/examples/skyport/src/flows/payments/shapes.ts +0 -5
- package/examples/skyport/src/flows/support/index.ts +0 -38
- package/examples/skyport/src/flows/users/elements.ts +0 -2
- package/examples/skyport/src/flows/users/index.ts +0 -12
- package/examples/skyport/src/flows/users/shapes.ts +0 -6
- package/examples/skyport/src/gates.ts +0 -10
- package/examples/skyport/src/journeys.ts +0 -9
- package/examples/skyport/src/locales/ar.ts +0 -4
- package/examples/skyport/src/locales/en.ts +0 -4
- package/examples/skyport/src/plugins/audit-schema.ts +0 -7
- package/examples/skyport/src/plugins/audit.ts +0 -14
- package/examples/skyport/src/plugins/panel.tsx +0 -4
- package/examples/skyport/src/schema.ts +0 -22
- package/examples/skyport/src/vault.ts +0 -24
- package/examples/skyport/tests/support.test.ts +0 -16
- package/src/sync-templates.ts +0 -86
- package/templates/full/README.md +0 -49
- package/templates/full/oke.config.ts +0 -68
- package/templates/full/package.json +0 -15
- package/templates/full/src/ai.ts +0 -11
- package/templates/full/src/app.ts +0 -34
- package/templates/full/src/channels.ts +0 -9
- package/templates/full/src/core.ts +0 -4
- package/templates/full/src/flows/main/index.ts +0 -45
- package/templates/full/src/flows/main/shapes.ts +0 -3
- package/templates/full/src/flows/main/signals.ts +0 -8
- package/templates/full/src/gates.ts +0 -4
- package/templates/full/src/locales/ar.ts +0 -3
- package/templates/full/src/locales/en.ts +0 -3
- package/templates/full/src/schema.ts +0 -8
- package/templates/full/src/vault.ts +0 -9
- package/templates/full/tests/full.test.ts +0 -11
- package/templates/hello/README.md +0 -38
- package/templates/hello/oke.config.ts +0 -3
- package/templates/hello/package.json +0 -14
- package/templates/hello/src/app.ts +0 -10
- package/templates/hello/src/flows/hello/index.ts +0 -27
- package/templates/hello/tests/hello.test.ts +0 -10
- package/templates/minimal/README.md +0 -41
- package/templates/minimal/oke.config.ts +0 -14
- package/templates/minimal/package.json +0 -15
- package/templates/minimal/src/app.ts +0 -13
- package/templates/minimal/src/flows/main/index.ts +0 -49
- package/templates/minimal/src/schema.ts +0 -8
- package/templates/minimal/tests/minimal.test.ts +0 -13
- package/templates/standard/README.md +0 -47
- package/templates/standard/src/core.ts +0 -4
- package/templates/standard/src/schema.ts +0 -9
- /package/{templates/standard → template}/src/app.ts +0 -0
- /package/{templates/standard → template}/src/channels.ts +0 -0
- /package/{templates/standard → template}/src/flows/main/index.ts +0 -0
- /package/{templates/standard → template}/src/flows/main/shapes.ts +0 -0
- /package/{templates/standard → template}/src/flows/main/signals.ts +0 -0
- /package/{templates/standard → template}/src/gates.ts +0 -0
- /package/{templates/standard → template}/src/locales/ar.ts +0 -0
- /package/{templates/standard → template}/src/locales/en.ts +0 -0
- /package/{templates/standard → template}/src/vault.ts +0 -0
- /package/{templates/standard → template}/tests/standard.test.ts +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineConfig } from "drizzle-kit";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Domain schema sync for `oke db push|generate|migrate`.
|
|
5
|
+
*
|
|
6
|
+
* Dialect comes from OKE's `store.sql` driver map (resolved by the CLI and
|
|
7
|
+
* injected as `OKE_DRIZZLE_DIALECT`) — never inferred from `DATABASE_URL`
|
|
8
|
+
* presence. `src/schema.generated.ts` is emitted from `src/schema.decl.ts`
|
|
9
|
+
* for the active dialect as a pre-step of `oke db` / `oke dev`.
|
|
10
|
+
*
|
|
11
|
+
* Versioned prod migrations land in `./drizzle` — never mix with `.oke/`.
|
|
12
|
+
*/
|
|
13
|
+
const dialect = (process.env.OKE_DRIZZLE_DIALECT ?? "sqlite") as "sqlite" | "postgresql";
|
|
14
|
+
|
|
15
|
+
export default defineConfig({
|
|
16
|
+
dialect,
|
|
17
|
+
schema: "./src/schema.generated.ts",
|
|
18
|
+
out: "./drizzle",
|
|
19
|
+
dbCredentials: {
|
|
20
|
+
url:
|
|
21
|
+
dialect === "postgresql"
|
|
22
|
+
? (process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL!)
|
|
23
|
+
: (process.env.OKE_SQLITE_URL ?? "file:.oke/app.sqlite"),
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -4,60 +4,57 @@ export default defineConfig({
|
|
|
4
4
|
drivers: {
|
|
5
5
|
store: {
|
|
6
6
|
sql: {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
local: "sqlite",
|
|
8
|
+
docker: "postgres",
|
|
9
9
|
test: "memory",
|
|
10
10
|
prod: "postgres",
|
|
11
11
|
},
|
|
12
12
|
kv: {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
local: "memory",
|
|
14
|
+
docker: "redis",
|
|
15
15
|
test: "memory",
|
|
16
16
|
prod: "redis",
|
|
17
17
|
},
|
|
18
|
+
files: {
|
|
19
|
+
local: "fs",
|
|
20
|
+
docker: "s3",
|
|
21
|
+
test: "memory",
|
|
22
|
+
prod: "s3",
|
|
23
|
+
},
|
|
18
24
|
},
|
|
19
25
|
signal: {
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
local: "memory",
|
|
27
|
+
docker: "postgres",
|
|
22
28
|
test: "memory",
|
|
23
29
|
prod: "postgres",
|
|
24
30
|
},
|
|
25
31
|
clock: {
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
local: "memory",
|
|
33
|
+
docker: "postgres",
|
|
28
34
|
test: "frozen",
|
|
29
35
|
prod: "postgres",
|
|
30
36
|
},
|
|
31
37
|
vault: {
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
local: "dotenv",
|
|
39
|
+
docker: "openbao",
|
|
34
40
|
test: "memory",
|
|
35
|
-
prod: "
|
|
41
|
+
prod: "openbao",
|
|
36
42
|
},
|
|
37
43
|
channel: {
|
|
38
44
|
email: {
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
local: "console",
|
|
46
|
+
docker: "smtp",
|
|
41
47
|
test: "console",
|
|
42
48
|
prod: "smtp",
|
|
43
49
|
},
|
|
44
|
-
sms: {
|
|
45
|
-
dev: "console",
|
|
46
|
-
stack: "unifonic",
|
|
47
|
-
test: "console",
|
|
48
|
-
prod: "unifonic",
|
|
49
|
-
},
|
|
50
|
-
whatsapp: {
|
|
51
|
-
dev: "console",
|
|
52
|
-
stack: "wa-cloud",
|
|
53
|
-
test: "console",
|
|
54
|
-
prod: "wa-cloud",
|
|
55
|
-
},
|
|
56
50
|
},
|
|
57
51
|
},
|
|
58
52
|
images: {
|
|
59
53
|
"store.sql": "postgres:18-alpine",
|
|
60
54
|
"store.kv": "redis:8-alpine",
|
|
55
|
+
"store.files": "rustfs/rustfs:1.0.0-beta.11",
|
|
56
|
+
"channel.email": "axllent/mailpit:v1.22.3",
|
|
57
|
+
vault: "openbao/openbao:2.6.1",
|
|
61
58
|
},
|
|
62
59
|
i18n: { locales: ["en", "ar"], default: "en", dir: { ar: "rtl" } },
|
|
63
60
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { store, field, id, now } from "okengine";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Domain schema — abstract declarations, emitted to
|
|
5
|
+
* `src/schema.generated.ts` for the active dialect (sqlite in local mode,
|
|
6
|
+
* postgres in docker) by `oke db` / `oke dev`.
|
|
7
|
+
*/
|
|
8
|
+
export const entries = store.schema.table("entries", {
|
|
9
|
+
id: field.text().primaryKey().defaultFn(id),
|
|
10
|
+
body: field.text().notNull(),
|
|
11
|
+
createdAt: field.integer().notNull().defaultFn(now),
|
|
12
|
+
});
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "okengine/config";
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
drivers: {
|
|
5
|
-
store: { sql: { dev: "sqlite", test: "memory", prod: "postgres" },
|
|
6
|
-
kv: { dev: "memory", test: "memory", prod: "redis" } },
|
|
7
|
-
signal: { dev: "memory", test: "memory", prod: "postgres" },
|
|
8
|
-
clock: { dev: "memory", test: "frozen", prod: "postgres" },
|
|
9
|
-
},
|
|
10
|
-
});
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@oke/example-linkly",
|
|
3
|
-
"version": "0.0.1",
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"dependencies": {
|
|
7
|
-
"okengine": "file:../..",
|
|
8
|
-
"drizzle-orm": "^1.0.0-rc.4",
|
|
9
|
-
"zod": "^4.4.3"
|
|
10
|
-
},
|
|
11
|
-
"scripts": {
|
|
12
|
-
"test": "bun test",
|
|
13
|
-
"dev": "oke dev"
|
|
14
|
-
}
|
|
15
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { db } from "./core";
|
|
2
|
-
import { member, fair } from "./gates";
|
|
3
|
-
import { linkClicked, linkStats } from "./flows/links/signals";
|
|
4
|
-
|
|
5
|
-
import { oke } from "okengine";
|
|
6
|
-
import * as links from "./flows/links";
|
|
7
|
-
import * as analytics from "./flows/analytics";
|
|
8
|
-
|
|
9
|
-
export const app = oke({ name: "linkly" }).adopt({ links, analytics });
|
|
10
|
-
|
|
11
|
-
export type App = typeof app;
|
|
12
|
-
|
|
13
|
-
// Spec test calls `t.api.links.report` though `report` lives in analytics.
|
|
14
|
-
app.adopt({ links: { report: analytics.report } });
|
|
15
|
-
|
|
16
|
-
Object.assign(app.$options, {
|
|
17
|
-
env: "test",
|
|
18
|
-
gates: [member, fair],
|
|
19
|
-
signals: [linkClicked, linkStats],
|
|
20
|
-
stores: [db],
|
|
21
|
-
});
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { on, flow, http } from "okengine";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { eq, and } from "drizzle-orm";
|
|
4
|
-
import { linkClicked } from "../links/signals";
|
|
5
|
-
import { db } from "../../core";
|
|
6
|
-
import { member } from "../../gates";
|
|
7
|
-
import { daily } from "../../schema";
|
|
8
|
-
|
|
9
|
-
on(linkClicked, flow({ // a second consumer of the same signal
|
|
10
|
-
do: async ({ code, at }, fx) => {
|
|
11
|
-
const day = new Date(at).toISOString().slice(0, 10);
|
|
12
|
-
const [row] = await fx.store(db).select().from(daily)
|
|
13
|
-
.where(and(eq(daily.code, code), eq(daily.day, day))).limit(1);
|
|
14
|
-
|
|
15
|
-
if (row) await fx.store(db).increment(daily, row.id, "clicks");
|
|
16
|
-
else await fx.store(db).insert(daily).values({ id: fx.id(), code, day, clicks: 1 });
|
|
17
|
-
},
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
|
-
export const report = on(http.get("/links/:code/report").gate(member), flow({
|
|
21
|
-
out: z.array(z.object({ day: z.string(), clicks: z.number() })),
|
|
22
|
-
do: ({ code }, fx) => fx.store(db).select({ day: daily.day, clicks: daily.clicks })
|
|
23
|
-
.from(daily).where(eq(daily.code, code)),
|
|
24
|
-
}));
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { on, flow, http, every } from "okengine";
|
|
3
|
-
import { eq, lt } from "drizzle-orm";
|
|
4
|
-
import { db } from "../../core";
|
|
5
|
-
import { member, fair } from "../../gates";
|
|
6
|
-
import { linkClicked, linkStats } from "./signals";
|
|
7
|
-
import { NewLink, LinkCode, Link, NotFound, Taken } from "./shapes";
|
|
8
|
-
import { links } from "../../schema";
|
|
9
|
-
|
|
10
|
-
// ① HTTP — "an endpoint"
|
|
11
|
-
export const shorten = on(http.post("/links").gate(member, fair), flow({
|
|
12
|
-
in: NewLink, out: LinkCode, errors: { Taken },
|
|
13
|
-
do: async ({ url, code }, fx) => {
|
|
14
|
-
if (await fx.store(db).exists(links, { code })) return fx.fail("Taken", {});
|
|
15
|
-
const id = fx.id();
|
|
16
|
-
await fx.store(db).insert(links).values(
|
|
17
|
-
{ id, code, url, userId: fx.auth.userId, clicks: 0, createdAt: Date.now() });
|
|
18
|
-
return { code };
|
|
19
|
-
},
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
// ② HTTP — the hot path
|
|
23
|
-
export const redirect = on(http.get("/:code").gate(fair), flow({
|
|
24
|
-
in: LinkCode, out: Link, errors: { NotFound },
|
|
25
|
-
do: async ({ code }, fx) => {
|
|
26
|
-
const [link] = await fx.store(db).select().from(links).where(eq(links.code, code)).limit(1);
|
|
27
|
-
if (!link) return fx.fail("NotFound", {});
|
|
28
|
-
|
|
29
|
-
await fx.emit(linkClicked, { code, at: Date.now() }); // same transaction as any write
|
|
30
|
-
return link;
|
|
31
|
-
},
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
// ③ SIGNAL — "a queue consumer", and the same species as ① and ②
|
|
35
|
-
on(linkClicked, flow({
|
|
36
|
-
do: async ({ code }, fx) => {
|
|
37
|
-
const [link] = await fx.store(db).select().from(links).where(eq(links.code, code)).limit(1);
|
|
38
|
-
const clicks = await fx.store(db).increment(links, link.id, "clicks");
|
|
39
|
-
await fx.emit(linkStats, { code, clicks }); // live: pushed to subscribers
|
|
40
|
-
},
|
|
41
|
-
}));
|
|
42
|
-
|
|
43
|
-
// ④ CLOCK — "a cron job", and still the same species
|
|
44
|
-
on(every("1h"), flow({
|
|
45
|
-
do: (_, fx) => {
|
|
46
|
-
const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
47
|
-
return fx.store(db).delete(links).where(lt(links.createdAt, cutoff));
|
|
48
|
-
},
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
|
-
// ⑤ A plain flow with no trigger — callable, not "private"
|
|
52
|
-
export const stats = flow({
|
|
53
|
-
in: LinkCode, out: z.object({ clicks: z.number() }),
|
|
54
|
-
do: async ({ code }, fx) => {
|
|
55
|
-
const [link] = await fx.store(db).select({ clicks: links.clicks })
|
|
56
|
-
.from(links).where(eq(links.code, code)).limit(1);
|
|
57
|
-
return link ?? { clicks: 0 };
|
|
58
|
-
},
|
|
59
|
-
});
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
export const NewLink = z.object({
|
|
4
|
-
url: z.string().url(),
|
|
5
|
-
code: z.string().min(1).max(32),
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
export const LinkCode = z.object({
|
|
9
|
-
code: z.string(),
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
export const Link = z.object({
|
|
13
|
-
id: z.string(),
|
|
14
|
-
code: z.string(),
|
|
15
|
-
url: z.string(),
|
|
16
|
-
userId: z.string(),
|
|
17
|
-
clicks: z.number(),
|
|
18
|
-
createdAt: z.number(),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
export const Taken = z.object({});
|
|
22
|
-
export const NotFound = z.object({});
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { signal } from "okengine";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
|
|
4
|
-
export const linkClicked = signal("link-clicked", {
|
|
5
|
-
schema: z.object({ code: z.string(), at: z.number(), referrer: z.string().optional() }),
|
|
6
|
-
delivery: "once", // queue physics: one consumer, retries, DLQ
|
|
7
|
-
retries: 3, deadLetter: true,
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
export const linkStats = signal("link-stats", {
|
|
11
|
-
schema: z.object({ code: z.string(), clicks: z.number() }),
|
|
12
|
-
delivery: "live", // stream physics: clients subscribe
|
|
13
|
-
});
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { gate } from "okengine";
|
|
2
|
-
|
|
3
|
-
export const member = gate.policy("member", ({ auth }) => !!auth?.verified);
|
|
4
|
-
|
|
5
|
-
export const fair = gate.rate({
|
|
6
|
-
strategy: "sliding-window-counter", // near-exact, two keys, no boundary bursts
|
|
7
|
-
max: 60, per: "1m", keyBy: "ip",
|
|
8
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
2
|
-
|
|
3
|
-
export const links = sqliteTable("links", {
|
|
4
|
-
id: text("id").primaryKey(), // `increment` targets this column
|
|
5
|
-
code: text("code").notNull().unique(), // the short, human-facing key
|
|
6
|
-
url: text("url").notNull(),
|
|
7
|
-
userId: text("user_id").notNull(),
|
|
8
|
-
clicks: integer("clicks").notNull().default(0),
|
|
9
|
-
createdAt: integer("created_at").notNull(),
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
export const daily = sqliteTable("daily", {
|
|
13
|
-
id: text("id").primaryKey(),
|
|
14
|
-
code: text("code").notNull(),
|
|
15
|
-
day: text("day").notNull(), // "YYYY-MM-DD"
|
|
16
|
-
clicks: integer("clicks").notNull().default(0),
|
|
17
|
-
});
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { test, expect } from "bun:test";
|
|
2
|
-
import { createTestApp } from "okengine/test";
|
|
3
|
-
import { app } from "../src/app";
|
|
4
|
-
|
|
5
|
-
test("shorten → redirect → report · time travel", async () => {
|
|
6
|
-
const t = await createTestApp(app); // memory drivers, frozen clock
|
|
7
|
-
const u = await t.auth.loginAs({});
|
|
8
|
-
|
|
9
|
-
await t.api.links.shorten({ url: "https://example.com", code: "sa" }, { as: u });
|
|
10
|
-
await t.api.links.redirect({ code: "sa" });
|
|
11
|
-
await t.signals.drain(); // run queued work deterministically
|
|
12
|
-
|
|
13
|
-
const { data } = await t.api.links.report({ code: "sa" }, { as: u });
|
|
14
|
-
expect(data![0].clicks).toBe(1);
|
|
15
|
-
|
|
16
|
-
await t.clock.advance("31d");
|
|
17
|
-
await t.cron.run("1h"); // time travel
|
|
18
|
-
await t.close();
|
|
19
|
-
});
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@oke/example-notes",
|
|
3
|
-
"version": "0.0.1",
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"dependencies": {
|
|
7
|
-
"okengine": "file:../..",
|
|
8
|
-
"drizzle-orm": "^1.0.0-rc.4",
|
|
9
|
-
"drizzle-zod": "^0.8.3",
|
|
10
|
-
"zod": "^4.4.3"
|
|
11
|
-
},
|
|
12
|
-
"scripts": {
|
|
13
|
-
"test": "bun test",
|
|
14
|
-
"dev": "oke dev"
|
|
15
|
-
}
|
|
16
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { db } from "./core";
|
|
2
|
-
|
|
3
|
-
import { oke } from "okengine";
|
|
4
|
-
import * as notes from "./flows/notes";
|
|
5
|
-
|
|
6
|
-
export const app = oke({ name: "notes" }).adopt({ notes });
|
|
7
|
-
|
|
8
|
-
export type App = typeof app; // ← the client needs nothing else
|
|
9
|
-
|
|
10
|
-
Object.assign(app.$options, {
|
|
11
|
-
env: "test",
|
|
12
|
-
stores: [db],
|
|
13
|
-
});
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { on, flow, http } from "okengine";
|
|
2
|
-
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { db } from "../../core";
|
|
5
|
-
import { notes } from "../../schema";
|
|
6
|
-
|
|
7
|
-
// Contracts derived from the schema — one source of truth, refined where the API is stricter
|
|
8
|
-
const NewNote = createInsertSchema(notes, { title: (s) => s.min(1).max(120) })
|
|
9
|
-
.omit({ id: true, createdAt: true });
|
|
10
|
-
const Note = createSelectSchema(notes);
|
|
11
|
-
const NoteId = z.object({ id: z.string() });
|
|
12
|
-
const NotFound = z.object({});
|
|
13
|
-
|
|
14
|
-
export const create = on(http.post("/notes"), flow({
|
|
15
|
-
in: NewNote,
|
|
16
|
-
out: NoteId,
|
|
17
|
-
do: async (input, fx) => {
|
|
18
|
-
const [note] = await fx.store(db).insert(notes).values(input).returning();
|
|
19
|
-
return { id: note.id };
|
|
20
|
-
},
|
|
21
|
-
}));
|
|
22
|
-
// effects → writes[sql:notes]
|
|
23
|
-
|
|
24
|
-
export const list = on(http.get("/notes"), flow({
|
|
25
|
-
out: Note.array(),
|
|
26
|
-
do: (_, fx) => fx.store(db).select().from(notes),
|
|
27
|
-
}));
|
|
28
|
-
// effects → reads[sql:notes]
|
|
29
|
-
|
|
30
|
-
export const get = on(http.get("/notes/:id"), flow({
|
|
31
|
-
in: NoteId,
|
|
32
|
-
out: Note,
|
|
33
|
-
errors: { NotFound },
|
|
34
|
-
do: async ({ id }, fx) => (await fx.store(db).findById(notes, id)) ?? fx.fail("NotFound", {}),
|
|
35
|
-
}));
|
|
36
|
-
|
|
37
|
-
export const remove = on(http.delete("/notes/:id"), flow({
|
|
38
|
-
in: NoteId,
|
|
39
|
-
errors: { NotFound },
|
|
40
|
-
do: async ({ id }, fx) => {
|
|
41
|
-
const deleted = await fx.store(db).delete(notes, id);
|
|
42
|
-
if (!deleted) return fx.fail("NotFound", {});
|
|
43
|
-
},
|
|
44
|
-
}));
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
2
|
-
import { id, now } from "okengine/store";
|
|
3
|
-
|
|
4
|
-
export const notes = sqliteTable("notes", {
|
|
5
|
-
id: text("id").primaryKey().$defaultFn(id),
|
|
6
|
-
title: text("title").notNull(),
|
|
7
|
-
body: text("body").notNull(),
|
|
8
|
-
createdAt: integer("created_at").notNull().$defaultFn(now),
|
|
9
|
-
});
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { test, expect } from "bun:test";
|
|
2
|
-
import { createTestApp } from "okengine/test";
|
|
3
|
-
import { app } from "../src/app";
|
|
4
|
-
|
|
5
|
-
test("create then read", async () => {
|
|
6
|
-
const t = await createTestApp(app); // memory driver, automatic
|
|
7
|
-
const { data } = await t.api.notes.create({ title: "First", body: "Hello" });
|
|
8
|
-
const { data: note } = await t.api.notes.get({ id: data!.id });
|
|
9
|
-
expect(note!.title).toBe("First");
|
|
10
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "okengine/config";
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
drivers: {
|
|
5
|
-
store: { sql: { dev: "sqlite", test: "memory", prod: "postgres" },
|
|
6
|
-
kv: { dev: "memory", test: "memory", prod: "redis" } },
|
|
7
|
-
signal: { dev: "memory", test: "memory", prod: "postgres" },
|
|
8
|
-
clock: { dev: "memory", test: "frozen", prod: "postgres" },
|
|
9
|
-
vault: { dev: "dotenv", test: "memory", prod: "sops" },
|
|
10
|
-
channel: {
|
|
11
|
-
email: { dev: "console", test: "console", prod: "smtp" },
|
|
12
|
-
sms: { dev: "console", test: "console", prod: "unifonic" },
|
|
13
|
-
whatsapp: { dev: "console", test: "console", prod: "wa-cloud" },
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
i18n: { locales: ["en", "ar"], default: "ar", dir: { ar: "rtl" } },
|
|
17
|
-
});
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@oke/example-provisions",
|
|
3
|
-
"version": "0.0.1",
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"dependencies": {
|
|
7
|
-
"okengine": "file:../..",
|
|
8
|
-
"drizzle-orm": "^1.0.0-rc.4",
|
|
9
|
-
"zod": "^4.4.3"
|
|
10
|
-
},
|
|
11
|
-
"scripts": {
|
|
12
|
-
"test": "bun test",
|
|
13
|
-
"dev": "oke dev"
|
|
14
|
-
}
|
|
15
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { rateLimit } from "okengine";
|
|
2
|
-
import { member } from "./gates";
|
|
3
|
-
import { canOrder } from "./flows/orders";
|
|
4
|
-
import { stripeKey, dbUrl } from "./vault";
|
|
5
|
-
import { orderPlaced, orderNews } from "./flows/orders/signals";
|
|
6
|
-
import { orderConfirmed, otpCode } from "./channels";
|
|
7
|
-
import { db } from "./core";
|
|
8
|
-
import { products } from "./schema";
|
|
9
|
-
|
|
10
|
-
import { oke } from "okengine";
|
|
11
|
-
import { auth } from "okengine/auth";
|
|
12
|
-
import { audit } from "./plugins/audit";
|
|
13
|
-
import * as orders from "./flows/orders";
|
|
14
|
-
import * as payments from "./flows/payments";
|
|
15
|
-
import * as notifications from "./flows/notifications";
|
|
16
|
-
|
|
17
|
-
export const app = oke({ name: "provisions" })
|
|
18
|
-
.adopt({ orders, payments, notifications })
|
|
19
|
-
.plug(auth()) // zero ceremony: uses your configured store
|
|
20
|
-
.plug(audit) // app-wide
|
|
21
|
-
.hook("onError", (ctx, err, fx) => fx.log.error(err));
|
|
22
|
-
|
|
23
|
-
app.unit("orders").plug(rateLimit({ max: 30 })); // this unit only
|
|
24
|
-
|
|
25
|
-
export type App = typeof app;
|
|
26
|
-
|
|
27
|
-
Object.assign(app.$options, {
|
|
28
|
-
env: "test",
|
|
29
|
-
gates: [member, canOrder],
|
|
30
|
-
secrets: [stripeKey, dbUrl],
|
|
31
|
-
signals: [orderPlaced, orderNews],
|
|
32
|
-
stores: [db],
|
|
33
|
-
channel: {
|
|
34
|
-
templates: [orderConfirmed, otpCode],
|
|
35
|
-
defaultLocale: "ar",
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const boot = app.boot.bind(app);
|
|
40
|
-
app.boot = async (opts) => {
|
|
41
|
-
const result = await boot(opts);
|
|
42
|
-
// `app.boot()` returns the app; the runtime is on `bootResult`.
|
|
43
|
-
const store = app.bootResult?.store;
|
|
44
|
-
if (store) {
|
|
45
|
-
const sql = await store.open(db, {
|
|
46
|
-
effects: { writes: ["sql:provisions"] },
|
|
47
|
-
});
|
|
48
|
-
if ("exists" in sql && "insert" in sql) {
|
|
49
|
-
const existing = await sql.exists(products, { sku: "COFFEE" });
|
|
50
|
-
if (!existing) {
|
|
51
|
-
await sql.insert(products).values({
|
|
52
|
-
sku: "COFFEE",
|
|
53
|
-
name: "Coffee",
|
|
54
|
-
stock: 100,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return result;
|
|
60
|
-
};
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { channel } from "okengine";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
|
|
4
|
-
export const mail = channel.email({ from: "Provisions <no-reply@provisions.sa>" });
|
|
5
|
-
export const sms = channel.sms({ sender: "PROVISIONS" });
|
|
6
|
-
export const wa = channel.whatsapp();
|
|
7
|
-
|
|
8
|
-
export const orderConfirmed = mail.template("order-confirmed", {
|
|
9
|
-
schema: z.object({ name: z.string(), orderId: z.string(), total: z.number() }),
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
export const otpCode = channel.template("otp-code", { // medium-agnostic
|
|
13
|
-
schema: z.object({ code: z.string() }),
|
|
14
|
-
});
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { on, flow } from "okengine";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { orderNews } from "../orders/signals";
|
|
4
|
-
import { getOrder } from "../orders";
|
|
5
|
-
import { orderConfirmed, otpCode, wa, sms } from "../../channels";
|
|
6
|
-
|
|
7
|
-
on(orderNews, flow({
|
|
8
|
-
do: async ({ orderId, status }, fx) => {
|
|
9
|
-
if (status !== "confirmed") return;
|
|
10
|
-
const o = await fx.call(getOrder, { id: orderId });
|
|
11
|
-
await fx.send(orderConfirmed, { to: o.userId, data: { name: o.userName, orderId, total: o.total } });
|
|
12
|
-
},
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
export const sendOtp = flow({
|
|
16
|
-
in: z.object({ userId: z.string(), code: z.string() }),
|
|
17
|
-
do: ({ userId, code }, fx) => fx.send(otpCode, { to: userId, via: [wa, sms], data: { code } }),
|
|
18
|
-
// ↑ fallback chain: WhatsApp, else SMS
|
|
19
|
-
});
|