@vaiftech/cli 1.2.0 → 1.3.1
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/dist/chunk-7XA2HKEQ.js +2067 -0
- package/dist/cli.cjs +1196 -279
- package/dist/cli.js +36 -30
- package/dist/index.cjs +1171 -260
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-VD3KS4ZK.js +0 -1156
package/dist/index.cjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
'use strict';var
|
|
1
|
+
'use strict';var v=require('fs'),S=require('path'),M=require('dotenv'),N=require('pg'),L=require('ora'),u=require('chalk'),O=require('prettier'),k=require('readline');function _interopDefault(e){return e&&e.__esModule?e:{default:e}}var v__default=/*#__PURE__*/_interopDefault(v);var S__default=/*#__PURE__*/_interopDefault(S);var M__default=/*#__PURE__*/_interopDefault(M);var N__default=/*#__PURE__*/_interopDefault(N);var L__default=/*#__PURE__*/_interopDefault(L);var u__default=/*#__PURE__*/_interopDefault(u);var O__default=/*#__PURE__*/_interopDefault(O);var k__default=/*#__PURE__*/_interopDefault(k);M__default.default.config();async function x(r){let a=S__default.default.resolve(r);if(!v__default.default.existsSync(a))return null;try{let t=v__default.default.readFileSync(a,"utf-8"),e=JSON.parse(t);return e.database?.url&&(e.database.url=F(e.database.url)),e.api?.apiKey&&(e.api.apiKey=F(e.api.apiKey)),e}catch{throw new Error(`Failed to parse config file: ${r}`)}}function F(r){return r.replace(/\$\{([^}]+)\}/g,(a,t)=>process.env[t]||a)}async function j(r,a){let t=await r.query(`
|
|
2
2
|
SELECT table_name, table_type
|
|
3
3
|
FROM information_schema.tables
|
|
4
4
|
WHERE table_schema = $1
|
|
5
5
|
AND table_type = 'BASE TABLE'
|
|
6
6
|
ORDER BY table_name
|
|
7
|
-
`,[
|
|
7
|
+
`,[a]),e=await r.query(`
|
|
8
8
|
SELECT
|
|
9
9
|
table_name,
|
|
10
10
|
column_name,
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
FROM information_schema.columns
|
|
20
20
|
WHERE table_schema = $1
|
|
21
21
|
ORDER BY table_name, ordinal_position
|
|
22
|
-
`,[
|
|
22
|
+
`,[a]),o=await r.query(`
|
|
23
23
|
SELECT
|
|
24
24
|
tc.constraint_name,
|
|
25
25
|
tc.table_name,
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
AND ccu.table_schema = tc.table_schema
|
|
36
36
|
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
37
37
|
AND tc.table_schema = $1
|
|
38
|
-
`,[
|
|
38
|
+
`,[a]),i=await r.query(`
|
|
39
39
|
SELECT
|
|
40
40
|
t.typname as enum_name,
|
|
41
41
|
e.enumlabel as enum_value
|
|
@@ -44,22 +44,113 @@
|
|
|
44
44
|
JOIN pg_namespace n ON n.oid = t.typnamespace
|
|
45
45
|
WHERE n.nspname = $1
|
|
46
46
|
ORDER BY t.typname, e.enumsortorder
|
|
47
|
-
`,[
|
|
47
|
+
`,[a]),l=new Map;for(let n of t.rows)l.set(n.table_name,[]);for(let n of e.rows){let p=l.get(n.table_name);p&&p.push(n);}let s=new Map;for(let n of i.rows){let p=s.get(n.enum_name)||[];p.push(n.enum_value),s.set(n.enum_name,p);}return {tables:l,enums:s,foreignKeys:o.rows}}var w={smallint:"number",integer:"number",bigint:"string",int2:"number",int4:"number",int8:"string",decimal:"string",numeric:"string",real:"number",float4:"number",float8:"number","double precision":"number",money:"string",boolean:"boolean",bool:"boolean",text:"string",varchar:"string",char:"string",character:"string","character varying":"string",name:"string",citext:"string",date:"string",time:"string",timetz:"string","time without time zone":"string","time with time zone":"string",timestamp:"string",timestamptz:"string","timestamp without time zone":"string","timestamp with time zone":"string",interval:"string",bytea:"Buffer",uuid:"string",json:"unknown",jsonb:"unknown",inet:"string",cidr:"string",macaddr:"string",macaddr8:"string",point:"{ x: number; y: number }",line:"string",lseg:"string",box:"string",path:"string",polygon:"string",circle:"string",ARRAY:"unknown[]"};function $(r,a){let{data_type:t,udt_name:e,is_nullable:o}=r;if(a.has(e)){let s=a.get(e).map(n=>`"${n}"`).join(" | ");return o==="YES"?`(${s}) | null`:s}if(t==="ARRAY"){let l=e.replace(/^_/,"");if(a.has(l)){let p=a.get(l).map(c=>`"${c}"`).join(" | ");return o==="YES"?`(${p})[] | null`:`(${p})[]`}let s=w[l]||"unknown";return o==="YES"?`${s}[] | null`:`${s}[]`}let i=w[t]||w[e]||"unknown";return o==="YES"&&(i=`${i} | null`),i}function T(r){return r.split(/[_\-\s]+/).map(a=>a.charAt(0).toUpperCase()+a.slice(1).toLowerCase()).join("")}function K(r,a){let t=T(r),e=a.map(o=>` | "${o}"`).join(`
|
|
48
48
|
`);return `export type ${t} =
|
|
49
|
-
${e};`}function
|
|
50
|
-
${
|
|
49
|
+
${e};`}function B(r,a,t){let e=T(r),o=[],i=[],l=[];for(let c of a){let d=$(c,t),f=c.column_name,_=c.column_default!==null||c.is_identity==="YES",I=c.is_nullable==="YES";o.push(` ${f}: ${d};`),_||c.column_name==="id"?i.push(` ${f}?: ${d.replace(" | null","")} | null;`):I?i.push(` ${f}?: ${d};`):i.push(` ${f}: ${d.replace(" | null","")};`),l.push(` ${f}?: ${d.replace(" | null","")} | null;`);}let s=`export interface ${e} {
|
|
50
|
+
${o.join(`
|
|
51
51
|
`)}
|
|
52
|
-
}`,
|
|
53
|
-
${
|
|
52
|
+
}`,n=`export interface ${e}Insert {
|
|
53
|
+
${i.join(`
|
|
54
54
|
`)}
|
|
55
55
|
}`,p=`export interface ${e}Update {
|
|
56
|
-
${
|
|
56
|
+
${l.join(`
|
|
57
57
|
`)}
|
|
58
|
-
}`;return {base:s,insert:
|
|
59
|
-
`)}async function
|
|
60
|
-
Provide a connection string via:`)),console.log(
|
|
61
|
-
Error: ${t.message}`)),t.message.includes("ECONNREFUSED")&&console.log(
|
|
62
|
-
Make sure your database is running and accessible.`))),process.exit(1);}}var
|
|
58
|
+
}`;return {base:s,insert:n,update:p}}function Y(r,a,t){let e=["/**"," * Auto-generated TypeScript types from database schema"," * Generated by @vaiftech/cli",` * Generated at: ${new Date().toISOString()}`," * "," * DO NOT EDIT MANUALLY - changes will be overwritten"," */",""];if(a.size>0){e.push("// ============ ENUMS ============"),e.push("");for(let[i,l]of a)e.push(K(i,l)),e.push("");}e.push("// ============ TABLES ============"),e.push("");let o=[];for(let[i,l]of r){let{base:s,insert:n,update:p}=B(i,l,a);o.push(i),e.push(s),e.push(""),e.push(n),e.push(""),e.push(p),e.push("");}e.push("// ============ DATABASE SCHEMA ============"),e.push(""),e.push("export interface Database {");for(let i of o){let l=T(i);e.push(` ${i}: {`),e.push(` Row: ${l};`),e.push(` Insert: ${l}Insert;`),e.push(` Update: ${l}Update;`),e.push(" };");}return e.push("}"),e.push(""),e.push("export type TableName = keyof Database;"),e.push(""),e.push("// ============ HELPER TYPES ============"),e.push(""),e.push('export type Row<T extends TableName> = Database[T]["Row"];'),e.push('export type Insert<T extends TableName> = Database[T]["Insert"];'),e.push('export type Update<T extends TableName> = Database[T]["Update"];'),e.push(""),e.join(`
|
|
59
|
+
`)}async function q(r){let a=L__default.default("Loading configuration...").start();try{let t=await x(r.config),e=r.connection||t?.database?.url||process.env.DATABASE_URL;e||(a.fail("No database connection string provided"),console.log(u__default.default.yellow(`
|
|
60
|
+
Provide a connection string via:`)),console.log(u__default.default.gray(" --connection <url>")),console.log(u__default.default.gray(" DATABASE_URL environment variable")),console.log(u__default.default.gray(" vaif.config.json database.url")),process.exit(1)),a.text="Connecting to database...";let o=new N__default.default.Client({connectionString:e});await o.connect(),a.text="Introspecting schema...";let{tables:i,enums:l,foreignKeys:s}=await j(o,r.schema);if(await o.end(),i.size===0){a.warn(`No tables found in schema "${r.schema}"`);return}a.text=`Generating types for ${i.size} tables...`;let n=Y(i,l,s),p=await O__default.default.format(n,{parser:"typescript",semi:!0,singleQuote:!1,trailingComma:"es5",printWidth:100});if(r.dryRun){a.succeed("Generated types (dry run):"),console.log(""),console.log(u__default.default.gray("\u2500".repeat(60))),console.log(p),console.log(u__default.default.gray("\u2500".repeat(60)));return}let c=S__default.default.resolve(r.output),d=S__default.default.dirname(c);v__default.default.existsSync(d)||v__default.default.mkdirSync(d,{recursive:!0}),v__default.default.writeFileSync(c,p,"utf-8"),a.succeed(`Generated types for ${i.size} tables \u2192 ${u__default.default.cyan(r.output)}`),console.log(""),console.log(u__default.default.green("Generated:")),console.log(u__default.default.gray(` Tables: ${i.size}`)),console.log(u__default.default.gray(` Enums: ${l.size}`)),console.log(""),console.log(u__default.default.gray("Import in your code:")),console.log(u__default.default.cyan(` import type { Database, Row, Insert, Update } from "${r.output.replace(/\.ts$/,"")}";`));}catch(t){a.fail("Failed to generate types"),t instanceof Error&&(console.error(u__default.default.red(`
|
|
61
|
+
Error: ${t.message}`)),t.message.includes("ECONNREFUSED")&&console.log(u__default.default.yellow(`
|
|
62
|
+
Make sure your database is running and accessible.`))),process.exit(1);}}var h=[{name:"database",label:"Database",description:"CRUD queries, type-safe operations"},{name:"auth",label:"Authentication",description:"login, signup, OAuth, sessions"},{name:"realtime",label:"Realtime",description:"live subscriptions, presence"},{name:"storage",label:"Storage",description:"file uploads, signed URLs"},{name:"functions",label:"Functions",description:"serverless function calls"}],z={"nextjs-fullstack":{name:"Next.js Full-Stack",description:"Next.js app with server/client VAIF client, auth middleware, and React hooks",tag:"Next.js",defaultFeatures:["database","auth"],files:[{path:"package.json",content:`{
|
|
63
|
+
"name": "my-vaif-app",
|
|
64
|
+
"private": true,
|
|
65
|
+
"version": "0.1.0",
|
|
66
|
+
"scripts": {
|
|
67
|
+
"dev": "next dev",
|
|
68
|
+
"build": "next build",
|
|
69
|
+
"start": "next start",
|
|
70
|
+
"lint": "next lint"
|
|
71
|
+
},
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"@vaiftech/client": "^1.0.0",
|
|
74
|
+
"@vaiftech/auth": "^1.0.0",
|
|
75
|
+
"@vaiftech/react": "^1.0.0",
|
|
76
|
+
"next": "^15.0.0",
|
|
77
|
+
"react": "^19.0.0",
|
|
78
|
+
"react-dom": "^19.0.0"
|
|
79
|
+
},
|
|
80
|
+
"devDependencies": {
|
|
81
|
+
"@types/node": "^22.0.0",
|
|
82
|
+
"@types/react": "^19.0.0",
|
|
83
|
+
"@types/react-dom": "^19.0.0",
|
|
84
|
+
"typescript": "^5.7.0"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
`},{path:"tsconfig.json",content:`{
|
|
88
|
+
"compilerOptions": {
|
|
89
|
+
"target": "ES2017",
|
|
90
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
91
|
+
"allowJs": true,
|
|
92
|
+
"skipLibCheck": true,
|
|
93
|
+
"strict": true,
|
|
94
|
+
"noEmit": true,
|
|
95
|
+
"esModuleInterop": true,
|
|
96
|
+
"module": "esnext",
|
|
97
|
+
"moduleResolution": "bundler",
|
|
98
|
+
"resolveJsonModule": true,
|
|
99
|
+
"isolatedModules": true,
|
|
100
|
+
"jsx": "preserve",
|
|
101
|
+
"incremental": true,
|
|
102
|
+
"plugins": [{ "name": "next" }],
|
|
103
|
+
"paths": { "@/*": ["./*"] }
|
|
104
|
+
},
|
|
105
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
106
|
+
"exclude": ["node_modules"]
|
|
107
|
+
}
|
|
108
|
+
`},{path:"next.config.ts",content:`import type { NextConfig } from "next";
|
|
109
|
+
|
|
110
|
+
const nextConfig: NextConfig = {};
|
|
111
|
+
|
|
112
|
+
export default nextConfig;
|
|
113
|
+
`},{path:"app/layout.tsx",content:`import type { Metadata } from "next";
|
|
114
|
+
import { VaifProvider } from "@vaiftech/react";
|
|
115
|
+
import { vaif } from "@/lib/vaif";
|
|
116
|
+
import "./globals.css";
|
|
117
|
+
|
|
118
|
+
export const metadata: Metadata = {
|
|
119
|
+
title: "My VAIF App",
|
|
120
|
+
description: "Built with VAIF Studio",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
124
|
+
return (
|
|
125
|
+
<html lang="en">
|
|
126
|
+
<body>
|
|
127
|
+
<VaifProvider client={vaif}>{children}</VaifProvider>
|
|
128
|
+
</body>
|
|
129
|
+
</html>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
`},{path:"app/page.tsx",content:`export default function Home() {
|
|
133
|
+
return (
|
|
134
|
+
<main style={{ maxWidth: 600, margin: "80px auto", textAlign: "center" }}>
|
|
135
|
+
<h1>Welcome to VAIF</h1>
|
|
136
|
+
<p>Your Next.js app is ready. Start building!</p>
|
|
137
|
+
</main>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
`},{path:"app/globals.css",content:`*,
|
|
141
|
+
*::before,
|
|
142
|
+
*::after {
|
|
143
|
+
box-sizing: border-box;
|
|
144
|
+
margin: 0;
|
|
145
|
+
padding: 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
body {
|
|
149
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
150
|
+
-webkit-font-smoothing: antialiased;
|
|
151
|
+
-moz-osx-font-smoothing: grayscale;
|
|
152
|
+
}
|
|
153
|
+
`},{path:"lib/vaif.ts",content:`import { createClient } from "@vaiftech/client";
|
|
63
154
|
import { createServerClient } from "@vaiftech/client/server";
|
|
64
155
|
|
|
65
156
|
// Browser client \u2013 safe to use in Client Components
|
|
@@ -81,21 +172,22 @@ export function createVaifServer() {
|
|
|
81
172
|
NEXT_PUBLIC_VAIF_PROJECT_ID=your-project-id
|
|
82
173
|
NEXT_PUBLIC_VAIF_API_KEY=your-anon-key
|
|
83
174
|
VAIF_SECRET_KEY=your-secret-key
|
|
84
|
-
`},{path:"
|
|
175
|
+
`},{path:".gitignore",content:`node_modules
|
|
176
|
+
.next
|
|
177
|
+
out
|
|
178
|
+
.env
|
|
179
|
+
.env.local
|
|
180
|
+
*.local
|
|
181
|
+
`}],featureFiles:{auth:[{path:"middleware.ts",content:`import { NextResponse, type NextRequest } from "next/server";
|
|
85
182
|
import { createServerClient } from "@vaiftech/client/server";
|
|
86
183
|
import { authMiddleware } from "@vaiftech/auth/nextjs";
|
|
87
184
|
|
|
88
|
-
// Routes that require authentication
|
|
89
185
|
const protectedRoutes = ["/dashboard", "/settings", "/api/protected"];
|
|
90
186
|
|
|
91
187
|
export async function middleware(request: NextRequest) {
|
|
92
188
|
const { pathname } = request.nextUrl;
|
|
93
|
-
|
|
94
|
-
// Skip auth for public routes
|
|
95
189
|
const isProtected = protectedRoutes.some((route) => pathname.startsWith(route));
|
|
96
|
-
if (!isProtected)
|
|
97
|
-
return NextResponse.next();
|
|
98
|
-
}
|
|
190
|
+
if (!isProtected) return NextResponse.next();
|
|
99
191
|
|
|
100
192
|
const vaif = createServerClient({
|
|
101
193
|
projectId: process.env.NEXT_PUBLIC_VAIF_PROJECT_ID!,
|
|
@@ -115,29 +207,552 @@ export async function middleware(request: NextRequest) {
|
|
|
115
207
|
export const config = {
|
|
116
208
|
matcher: ["/dashboard/:path*", "/settings/:path*", "/api/protected/:path*"],
|
|
117
209
|
};
|
|
118
|
-
`}
|
|
210
|
+
`},{path:"app/(auth)/login/page.tsx",content:`"use client";
|
|
211
|
+
|
|
212
|
+
import { useState } from "react";
|
|
213
|
+
import { useRouter } from "next/navigation";
|
|
214
|
+
import { vaif } from "@/lib/vaif";
|
|
215
|
+
|
|
216
|
+
export default function LoginPage() {
|
|
217
|
+
const router = useRouter();
|
|
218
|
+
const [email, setEmail] = useState("");
|
|
219
|
+
const [password, setPassword] = useState("");
|
|
220
|
+
const [error, setError] = useState<string | null>(null);
|
|
221
|
+
const [loading, setLoading] = useState(false);
|
|
222
|
+
|
|
223
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
224
|
+
e.preventDefault();
|
|
225
|
+
setLoading(true);
|
|
226
|
+
setError(null);
|
|
227
|
+
|
|
228
|
+
const { error } = await vaif.auth.signInWithPassword({ email, password });
|
|
229
|
+
if (error) {
|
|
230
|
+
setError(error.message);
|
|
231
|
+
setLoading(false);
|
|
232
|
+
} else {
|
|
233
|
+
router.push("/dashboard");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div style={{ maxWidth: 400, margin: "80px auto" }}>
|
|
239
|
+
<h1>Log In</h1>
|
|
240
|
+
<form onSubmit={handleSubmit}>
|
|
241
|
+
<div style={{ marginBottom: 12 }}>
|
|
242
|
+
<label htmlFor="email">Email</label>
|
|
243
|
+
<input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required style={{ display: "block", width: "100%", padding: 8 }} />
|
|
244
|
+
</div>
|
|
245
|
+
<div style={{ marginBottom: 12 }}>
|
|
246
|
+
<label htmlFor="password">Password</label>
|
|
247
|
+
<input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required style={{ display: "block", width: "100%", padding: 8 }} />
|
|
248
|
+
</div>
|
|
249
|
+
{error && <p style={{ color: "red" }}>{error}</p>}
|
|
250
|
+
<button type="submit" disabled={loading} style={{ padding: "8px 24px" }}>
|
|
251
|
+
{loading ? "Logging in..." : "Log In"}
|
|
252
|
+
</button>
|
|
253
|
+
</form>
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
`},{path:"app/(auth)/signup/page.tsx",content:`"use client";
|
|
258
|
+
|
|
259
|
+
import { useState } from "react";
|
|
260
|
+
import { useRouter } from "next/navigation";
|
|
261
|
+
import { vaif } from "@/lib/vaif";
|
|
262
|
+
|
|
263
|
+
export default function SignupPage() {
|
|
264
|
+
const router = useRouter();
|
|
265
|
+
const [email, setEmail] = useState("");
|
|
266
|
+
const [password, setPassword] = useState("");
|
|
267
|
+
const [error, setError] = useState<string | null>(null);
|
|
268
|
+
const [loading, setLoading] = useState(false);
|
|
269
|
+
|
|
270
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
271
|
+
e.preventDefault();
|
|
272
|
+
setLoading(true);
|
|
273
|
+
setError(null);
|
|
274
|
+
|
|
275
|
+
const { error } = await vaif.auth.signUp({ email, password });
|
|
276
|
+
if (error) {
|
|
277
|
+
setError(error.message);
|
|
278
|
+
setLoading(false);
|
|
279
|
+
} else {
|
|
280
|
+
router.push("/");
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<div style={{ maxWidth: 400, margin: "80px auto" }}>
|
|
286
|
+
<h1>Sign Up</h1>
|
|
287
|
+
<form onSubmit={handleSubmit}>
|
|
288
|
+
<div style={{ marginBottom: 12 }}>
|
|
289
|
+
<label htmlFor="email">Email</label>
|
|
290
|
+
<input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required style={{ display: "block", width: "100%", padding: 8 }} />
|
|
291
|
+
</div>
|
|
292
|
+
<div style={{ marginBottom: 12 }}>
|
|
293
|
+
<label htmlFor="password">Password</label>
|
|
294
|
+
<input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required minLength={8} style={{ display: "block", width: "100%", padding: 8 }} />
|
|
295
|
+
</div>
|
|
296
|
+
{error && <p style={{ color: "red" }}>{error}</p>}
|
|
297
|
+
<button type="submit" disabled={loading} style={{ padding: "8px 24px" }}>
|
|
298
|
+
{loading ? "Creating account..." : "Sign Up"}
|
|
299
|
+
</button>
|
|
300
|
+
</form>
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
`}],storage:[{path:"lib/storage.ts",content:`import { createVaifServer } from "./vaif";
|
|
305
|
+
|
|
306
|
+
export async function uploadFile(bucket: string, file: Buffer | Blob, filePath: string) {
|
|
307
|
+
const vaif = createVaifServer();
|
|
308
|
+
const { data, error } = await vaif.storage.from(bucket).upload(filePath, file);
|
|
309
|
+
if (error) throw error;
|
|
310
|
+
const { data: urlData } = vaif.storage.from(bucket).getPublicUrl(data.path);
|
|
311
|
+
return { path: data.path, publicUrl: urlData.publicUrl };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export async function getSignedUrl(bucket: string, filePath: string, expiresIn = 3600) {
|
|
315
|
+
const vaif = createVaifServer();
|
|
316
|
+
const { data, error } = await vaif.storage.from(bucket).createSignedUrl(filePath, expiresIn);
|
|
317
|
+
if (error) throw error;
|
|
318
|
+
return data.signedUrl;
|
|
319
|
+
}
|
|
320
|
+
`},{path:"app/api/upload/route.ts",content:`import { NextRequest, NextResponse } from "next/server";
|
|
321
|
+
import { uploadFile } from "@/lib/storage";
|
|
322
|
+
|
|
323
|
+
export async function POST(request: NextRequest) {
|
|
324
|
+
const formData = await request.formData();
|
|
325
|
+
const file = formData.get("file") as Blob | null;
|
|
326
|
+
if (!file) return NextResponse.json({ error: "No file provided" }, { status: 400 });
|
|
327
|
+
|
|
328
|
+
const fileName = (formData.get("fileName") as string) || "upload";
|
|
329
|
+
const bucket = (formData.get("bucket") as string) || "uploads";
|
|
330
|
+
const filePath = \`\${Date.now()}-\${fileName}\`;
|
|
331
|
+
|
|
332
|
+
const result = await uploadFile(bucket, file, filePath);
|
|
333
|
+
return NextResponse.json(result);
|
|
334
|
+
}
|
|
335
|
+
`}],realtime:[{path:"hooks/useRealtime.ts",content:`"use client";
|
|
336
|
+
|
|
337
|
+
import { useEffect, useState, useCallback } from "react";
|
|
338
|
+
import { vaif } from "@/lib/vaif";
|
|
339
|
+
|
|
340
|
+
interface UseRealtimeOptions<T> {
|
|
341
|
+
table: string;
|
|
342
|
+
schema?: string;
|
|
343
|
+
filter?: string;
|
|
344
|
+
initialData?: T[];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function useRealtime<T extends { id: string }>({
|
|
348
|
+
table,
|
|
349
|
+
schema = "public",
|
|
350
|
+
filter,
|
|
351
|
+
initialData = [],
|
|
352
|
+
}: UseRealtimeOptions<T>) {
|
|
353
|
+
const [data, setData] = useState<T[]>(initialData);
|
|
354
|
+
|
|
355
|
+
useEffect(() => {
|
|
356
|
+
vaif.from(table).select("*").then(({ data: rows }) => {
|
|
357
|
+
if (rows) setData(rows as T[]);
|
|
358
|
+
});
|
|
359
|
+
}, [table]);
|
|
360
|
+
|
|
361
|
+
useEffect(() => {
|
|
362
|
+
const channelConfig: Record<string, string> = { event: "*", schema, table };
|
|
363
|
+
if (filter) channelConfig.filter = filter;
|
|
364
|
+
|
|
365
|
+
const channel = vaif
|
|
366
|
+
.channel(\`\${table}-changes\`)
|
|
367
|
+
.on("postgres_changes", channelConfig, (payload) => {
|
|
368
|
+
if (payload.eventType === "INSERT") {
|
|
369
|
+
setData((prev) => [...prev, payload.new as T]);
|
|
370
|
+
} else if (payload.eventType === "UPDATE") {
|
|
371
|
+
setData((prev) => prev.map((item) => (item.id === (payload.new as T).id ? (payload.new as T) : item)));
|
|
372
|
+
} else if (payload.eventType === "DELETE") {
|
|
373
|
+
setData((prev) => prev.filter((item) => item.id !== (payload.old as T).id));
|
|
374
|
+
}
|
|
375
|
+
})
|
|
376
|
+
.subscribe();
|
|
377
|
+
|
|
378
|
+
return () => { channel.unsubscribe(); };
|
|
379
|
+
}, [table, schema, filter]);
|
|
380
|
+
|
|
381
|
+
const refresh = useCallback(async () => {
|
|
382
|
+
const { data: rows } = await vaif.from(table).select("*");
|
|
383
|
+
if (rows) setData(rows as T[]);
|
|
384
|
+
}, [table]);
|
|
385
|
+
|
|
386
|
+
return { data, refresh };
|
|
387
|
+
}
|
|
388
|
+
`}]},dependencies:["@vaiftech/client","@vaiftech/auth","@vaiftech/react","next","react","react-dom"],devDependencies:["@types/node","@types/react","@types/react-dom","typescript"],postInstructions:["cd my-vaif-app","npm install","# Copy .env.local.example to .env.local and add your VAIF credentials","npm run dev"]},"react-spa":{name:"React SPA",description:"Single-page React app with Vite, VAIF client, and provider wrapper",tag:"React + Vite",defaultFeatures:["database","auth"],files:[{path:"package.json",content:`{
|
|
389
|
+
"name": "my-vaif-app",
|
|
390
|
+
"private": true,
|
|
391
|
+
"version": "0.1.0",
|
|
392
|
+
"type": "module",
|
|
393
|
+
"scripts": {
|
|
394
|
+
"dev": "vite",
|
|
395
|
+
"build": "tsc -b && vite build",
|
|
396
|
+
"preview": "vite preview"
|
|
397
|
+
},
|
|
398
|
+
"dependencies": {
|
|
399
|
+
"@vaiftech/client": "^1.0.0",
|
|
400
|
+
"@vaiftech/react": "^1.0.0",
|
|
401
|
+
"react": "^19.0.0",
|
|
402
|
+
"react-dom": "^19.0.0",
|
|
403
|
+
"react-router-dom": "^7.0.0"
|
|
404
|
+
},
|
|
405
|
+
"devDependencies": {
|
|
406
|
+
"@types/react": "^19.0.0",
|
|
407
|
+
"@types/react-dom": "^19.0.0",
|
|
408
|
+
"@vitejs/plugin-react": "^4.4.0",
|
|
409
|
+
"typescript": "^5.7.0",
|
|
410
|
+
"vite": "^6.0.0"
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
`},{path:"tsconfig.json",content:`{
|
|
414
|
+
"compilerOptions": {
|
|
415
|
+
"target": "ES2020",
|
|
416
|
+
"useDefineForClassFields": true,
|
|
417
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
418
|
+
"module": "ESNext",
|
|
419
|
+
"skipLibCheck": true,
|
|
420
|
+
"moduleResolution": "bundler",
|
|
421
|
+
"allowImportingTsExtensions": true,
|
|
422
|
+
"isolatedModules": true,
|
|
423
|
+
"moduleDetection": "force",
|
|
424
|
+
"noEmit": true,
|
|
425
|
+
"jsx": "react-jsx",
|
|
426
|
+
"strict": true,
|
|
427
|
+
"noUnusedLocals": true,
|
|
428
|
+
"noUnusedParameters": true,
|
|
429
|
+
"noFallthroughCasesInSwitch": true,
|
|
430
|
+
"noUncheckedSideEffectImports": true,
|
|
431
|
+
"paths": {
|
|
432
|
+
"@/*": ["./src/*"]
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
"include": ["src"]
|
|
436
|
+
}
|
|
437
|
+
`},{path:"vite.config.ts",content:`import { defineConfig } from "vite";
|
|
438
|
+
import react from "@vitejs/plugin-react";
|
|
439
|
+
import path from "path";
|
|
440
|
+
|
|
441
|
+
export default defineConfig({
|
|
442
|
+
plugins: [react()],
|
|
443
|
+
resolve: {
|
|
444
|
+
alias: {
|
|
445
|
+
"@": path.resolve(__dirname, "./src"),
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
`},{path:"index.html",content:`<!DOCTYPE html>
|
|
450
|
+
<html lang="en">
|
|
451
|
+
<head>
|
|
452
|
+
<meta charset="UTF-8" />
|
|
453
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
454
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
455
|
+
<title>My VAIF App</title>
|
|
456
|
+
</head>
|
|
457
|
+
<body>
|
|
458
|
+
<div id="root"></div>
|
|
459
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
460
|
+
</body>
|
|
461
|
+
</html>
|
|
462
|
+
`},{path:"src/main.tsx",content:`import React from "react";
|
|
463
|
+
import ReactDOM from "react-dom/client";
|
|
464
|
+
import { BrowserRouter } from "react-router-dom";
|
|
465
|
+
import { VaifProvider } from "@vaiftech/react";
|
|
466
|
+
import { vaif } from "./lib/vaif";
|
|
467
|
+
import App from "./App";
|
|
468
|
+
|
|
469
|
+
ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
470
|
+
<React.StrictMode>
|
|
471
|
+
<BrowserRouter>
|
|
472
|
+
<VaifProvider client={vaif}>
|
|
473
|
+
<App />
|
|
474
|
+
</VaifProvider>
|
|
475
|
+
</BrowserRouter>
|
|
476
|
+
</React.StrictMode>,
|
|
477
|
+
);
|
|
478
|
+
`},{path:"src/App.tsx",content:`import { Routes, Route } from "react-router-dom";
|
|
479
|
+
|
|
480
|
+
export default function App() {
|
|
481
|
+
return (
|
|
482
|
+
<Routes>
|
|
483
|
+
<Route path="/" element={<Home />} />
|
|
484
|
+
</Routes>
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function Home() {
|
|
489
|
+
return (
|
|
490
|
+
<div style={{ maxWidth: 600, margin: "80px auto", textAlign: "center" }}>
|
|
491
|
+
<h1>Welcome to VAIF</h1>
|
|
492
|
+
<p>Your app is ready. Start building!</p>
|
|
493
|
+
</div>
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
`},{path:"src/lib/vaif.ts",content:`import { createClient } from "@vaiftech/client";
|
|
119
497
|
|
|
120
498
|
export const vaif = createClient({
|
|
121
499
|
projectId: import.meta.env.VITE_VAIF_PROJECT_ID,
|
|
122
500
|
apiKey: import.meta.env.VITE_VAIF_API_KEY,
|
|
123
501
|
});
|
|
124
|
-
`},{path:"src/
|
|
125
|
-
import { vaif } from "../lib/vaif";
|
|
126
|
-
import type { ReactNode } from "react";
|
|
502
|
+
`},{path:"src/vite-env.d.ts",content:`/// <reference types="vite/client" />
|
|
127
503
|
|
|
128
|
-
interface
|
|
129
|
-
|
|
504
|
+
interface ImportMetaEnv {
|
|
505
|
+
readonly VITE_VAIF_PROJECT_ID: string;
|
|
506
|
+
readonly VITE_VAIF_API_KEY: string;
|
|
130
507
|
}
|
|
131
508
|
|
|
132
|
-
|
|
133
|
-
|
|
509
|
+
interface ImportMeta {
|
|
510
|
+
readonly env: ImportMetaEnv;
|
|
134
511
|
}
|
|
135
512
|
`},{path:".env.example",content:`# VAIF Configuration
|
|
136
513
|
# Get these values from https://vaif.studio/dashboard \u2192 Project Settings \u2192 API Keys
|
|
137
514
|
|
|
138
515
|
VITE_VAIF_PROJECT_ID=your-project-id
|
|
139
516
|
VITE_VAIF_API_KEY=your-anon-key
|
|
140
|
-
`}
|
|
517
|
+
`},{path:".gitignore",content:`node_modules
|
|
518
|
+
dist
|
|
519
|
+
.env
|
|
520
|
+
.env.local
|
|
521
|
+
*.local
|
|
522
|
+
`}],featureFiles:{auth:[{path:"src/pages/Login.tsx",content:`import { useState } from "react";
|
|
523
|
+
import { useNavigate } from "react-router-dom";
|
|
524
|
+
import { vaif } from "../lib/vaif";
|
|
525
|
+
|
|
526
|
+
export default function Login() {
|
|
527
|
+
const navigate = useNavigate();
|
|
528
|
+
const [email, setEmail] = useState("");
|
|
529
|
+
const [password, setPassword] = useState("");
|
|
530
|
+
const [error, setError] = useState<string | null>(null);
|
|
531
|
+
const [loading, setLoading] = useState(false);
|
|
532
|
+
|
|
533
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
534
|
+
e.preventDefault();
|
|
535
|
+
setLoading(true);
|
|
536
|
+
setError(null);
|
|
537
|
+
|
|
538
|
+
const { error } = await vaif.auth.signInWithPassword({ email, password });
|
|
539
|
+
if (error) {
|
|
540
|
+
setError(error.message);
|
|
541
|
+
setLoading(false);
|
|
542
|
+
} else {
|
|
543
|
+
navigate("/");
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return (
|
|
548
|
+
<div style={{ maxWidth: 400, margin: "80px auto" }}>
|
|
549
|
+
<h1>Log In</h1>
|
|
550
|
+
<form onSubmit={handleSubmit}>
|
|
551
|
+
<div style={{ marginBottom: 12 }}>
|
|
552
|
+
<label htmlFor="email">Email</label>
|
|
553
|
+
<input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required style={{ display: "block", width: "100%", padding: 8 }} />
|
|
554
|
+
</div>
|
|
555
|
+
<div style={{ marginBottom: 12 }}>
|
|
556
|
+
<label htmlFor="password">Password</label>
|
|
557
|
+
<input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required style={{ display: "block", width: "100%", padding: 8 }} />
|
|
558
|
+
</div>
|
|
559
|
+
{error && <p style={{ color: "red" }}>{error}</p>}
|
|
560
|
+
<button type="submit" disabled={loading} style={{ padding: "8px 24px" }}>
|
|
561
|
+
{loading ? "Logging in..." : "Log In"}
|
|
562
|
+
</button>
|
|
563
|
+
</form>
|
|
564
|
+
</div>
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
`},{path:"src/pages/Signup.tsx",content:`import { useState } from "react";
|
|
568
|
+
import { useNavigate } from "react-router-dom";
|
|
569
|
+
import { vaif } from "../lib/vaif";
|
|
570
|
+
|
|
571
|
+
export default function Signup() {
|
|
572
|
+
const navigate = useNavigate();
|
|
573
|
+
const [email, setEmail] = useState("");
|
|
574
|
+
const [password, setPassword] = useState("");
|
|
575
|
+
const [error, setError] = useState<string | null>(null);
|
|
576
|
+
const [loading, setLoading] = useState(false);
|
|
577
|
+
|
|
578
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
579
|
+
e.preventDefault();
|
|
580
|
+
setLoading(true);
|
|
581
|
+
setError(null);
|
|
582
|
+
|
|
583
|
+
const { error } = await vaif.auth.signUp({ email, password });
|
|
584
|
+
if (error) {
|
|
585
|
+
setError(error.message);
|
|
586
|
+
setLoading(false);
|
|
587
|
+
} else {
|
|
588
|
+
navigate("/");
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return (
|
|
593
|
+
<div style={{ maxWidth: 400, margin: "80px auto" }}>
|
|
594
|
+
<h1>Sign Up</h1>
|
|
595
|
+
<form onSubmit={handleSubmit}>
|
|
596
|
+
<div style={{ marginBottom: 12 }}>
|
|
597
|
+
<label htmlFor="email">Email</label>
|
|
598
|
+
<input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required style={{ display: "block", width: "100%", padding: 8 }} />
|
|
599
|
+
</div>
|
|
600
|
+
<div style={{ marginBottom: 12 }}>
|
|
601
|
+
<label htmlFor="password">Password</label>
|
|
602
|
+
<input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required minLength={8} style={{ display: "block", width: "100%", padding: 8 }} />
|
|
603
|
+
</div>
|
|
604
|
+
{error && <p style={{ color: "red" }}>{error}</p>}
|
|
605
|
+
<button type="submit" disabled={loading} style={{ padding: "8px 24px" }}>
|
|
606
|
+
{loading ? "Creating account..." : "Sign Up"}
|
|
607
|
+
</button>
|
|
608
|
+
</form>
|
|
609
|
+
</div>
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
`},{path:"src/components/AuthGuard.tsx",content:`import { useEffect, useState, type ReactNode } from "react";
|
|
613
|
+
import { useNavigate } from "react-router-dom";
|
|
614
|
+
import { vaif } from "../lib/vaif";
|
|
615
|
+
|
|
616
|
+
interface AuthGuardProps {
|
|
617
|
+
children: ReactNode;
|
|
618
|
+
fallback?: ReactNode;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export function AuthGuard({ children, fallback }: AuthGuardProps) {
|
|
622
|
+
const navigate = useNavigate();
|
|
623
|
+
const [loading, setLoading] = useState(true);
|
|
624
|
+
const [authenticated, setAuthenticated] = useState(false);
|
|
625
|
+
|
|
626
|
+
useEffect(() => {
|
|
627
|
+
vaif.auth.getSession().then(({ data }) => {
|
|
628
|
+
if (data.session) {
|
|
629
|
+
setAuthenticated(true);
|
|
630
|
+
} else {
|
|
631
|
+
navigate("/login");
|
|
632
|
+
}
|
|
633
|
+
setLoading(false);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
const { data: { subscription } } = vaif.auth.onAuthStateChange((_event, session) => {
|
|
637
|
+
setAuthenticated(!!session);
|
|
638
|
+
if (!session) navigate("/login");
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
return () => subscription.unsubscribe();
|
|
642
|
+
}, [navigate]);
|
|
643
|
+
|
|
644
|
+
if (loading) return fallback ?? <div>Loading...</div>;
|
|
645
|
+
if (!authenticated) return null;
|
|
646
|
+
return <>{children}</>;
|
|
647
|
+
}
|
|
648
|
+
`}],storage:[{path:"src/hooks/useFileUpload.ts",content:`import { useState, useCallback } from "react";
|
|
649
|
+
import { vaif } from "../lib/vaif";
|
|
650
|
+
|
|
651
|
+
interface UploadResult {
|
|
652
|
+
path: string;
|
|
653
|
+
publicUrl: string;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
interface UseFileUploadOptions {
|
|
657
|
+
bucket?: string;
|
|
658
|
+
folder?: string;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export function useFileUpload({ bucket = "uploads", folder = "" }: UseFileUploadOptions = {}) {
|
|
662
|
+
const [uploading, setUploading] = useState(false);
|
|
663
|
+
const [error, setError] = useState<string | null>(null);
|
|
664
|
+
|
|
665
|
+
const upload = useCallback(
|
|
666
|
+
async (file: File): Promise<UploadResult | null> => {
|
|
667
|
+
setUploading(true);
|
|
668
|
+
setError(null);
|
|
669
|
+
|
|
670
|
+
const filePath = folder
|
|
671
|
+
? \`\${folder}/\${Date.now()}-\${file.name}\`
|
|
672
|
+
: \`\${Date.now()}-\${file.name}\`;
|
|
673
|
+
|
|
674
|
+
const { data, error: uploadError } = await vaif.storage
|
|
675
|
+
.from(bucket)
|
|
676
|
+
.upload(filePath, file);
|
|
677
|
+
|
|
678
|
+
if (uploadError) {
|
|
679
|
+
setError(uploadError.message);
|
|
680
|
+
setUploading(false);
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const { data: urlData } = vaif.storage.from(bucket).getPublicUrl(data.path);
|
|
685
|
+
|
|
686
|
+
setUploading(false);
|
|
687
|
+
return { path: data.path, publicUrl: urlData.publicUrl };
|
|
688
|
+
},
|
|
689
|
+
[bucket, folder],
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
return { upload, uploading, error };
|
|
693
|
+
}
|
|
694
|
+
`}],realtime:[{path:"src/hooks/useRealtimeSubscription.ts",content:`import { useEffect, useState, useCallback } from "react";
|
|
695
|
+
import { vaif } from "../lib/vaif";
|
|
696
|
+
|
|
697
|
+
interface UseRealtimeOptions<T> {
|
|
698
|
+
table: string;
|
|
699
|
+
schema?: string;
|
|
700
|
+
filter?: string;
|
|
701
|
+
initialData?: T[];
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
export function useRealtimeSubscription<T extends { id: string }>({
|
|
705
|
+
table,
|
|
706
|
+
schema = "public",
|
|
707
|
+
filter,
|
|
708
|
+
initialData = [],
|
|
709
|
+
}: UseRealtimeOptions<T>) {
|
|
710
|
+
const [data, setData] = useState<T[]>(initialData);
|
|
711
|
+
|
|
712
|
+
useEffect(() => {
|
|
713
|
+
// Load initial data
|
|
714
|
+
let query = vaif.from(table).select("*");
|
|
715
|
+
query.then(({ data: rows }) => {
|
|
716
|
+
if (rows) setData(rows as T[]);
|
|
717
|
+
});
|
|
718
|
+
}, [table]);
|
|
719
|
+
|
|
720
|
+
useEffect(() => {
|
|
721
|
+
const channelConfig: Record<string, string> = {
|
|
722
|
+
event: "*",
|
|
723
|
+
schema,
|
|
724
|
+
table,
|
|
725
|
+
};
|
|
726
|
+
if (filter) channelConfig.filter = filter;
|
|
727
|
+
|
|
728
|
+
const channel = vaif
|
|
729
|
+
.channel(\`\${table}-changes\`)
|
|
730
|
+
.on("postgres_changes", channelConfig, (payload) => {
|
|
731
|
+
if (payload.eventType === "INSERT") {
|
|
732
|
+
setData((prev) => [...prev, payload.new as T]);
|
|
733
|
+
} else if (payload.eventType === "UPDATE") {
|
|
734
|
+
setData((prev) =>
|
|
735
|
+
prev.map((item) => (item.id === (payload.new as T).id ? (payload.new as T) : item)),
|
|
736
|
+
);
|
|
737
|
+
} else if (payload.eventType === "DELETE") {
|
|
738
|
+
setData((prev) => prev.filter((item) => item.id !== (payload.old as T).id));
|
|
739
|
+
}
|
|
740
|
+
})
|
|
741
|
+
.subscribe();
|
|
742
|
+
|
|
743
|
+
return () => {
|
|
744
|
+
channel.unsubscribe();
|
|
745
|
+
};
|
|
746
|
+
}, [table, schema, filter]);
|
|
747
|
+
|
|
748
|
+
const refresh = useCallback(async () => {
|
|
749
|
+
const { data: rows } = await vaif.from(table).select("*");
|
|
750
|
+
if (rows) setData(rows as T[]);
|
|
751
|
+
}, [table]);
|
|
752
|
+
|
|
753
|
+
return { data, refresh };
|
|
754
|
+
}
|
|
755
|
+
`}]},dependencies:["@vaiftech/client","@vaiftech/react","react","react-dom","react-router-dom"],devDependencies:["@types/react","@types/react-dom","@vitejs/plugin-react","typescript","vite"],postInstructions:["cd my-vaif-app","npm install","# Copy .env.example to .env and add your VAIF credentials","npm run dev"]},"ios-swift-app":{name:"iOS Swift App",description:"Swift client manager for iOS/macOS apps using Swift Package Manager",tag:"Swift / iOS",defaultFeatures:["database","auth"],files:[{path:"VaifManager.swift",content:`import Foundation
|
|
141
756
|
import VaifClient
|
|
142
757
|
|
|
143
758
|
/// Singleton manager for the VAIF client.
|
|
@@ -243,42 +858,287 @@ Run the VAIF CLI to generate Swift models from your schema:
|
|
|
243
858
|
\`\`\`bash
|
|
244
859
|
npx @vaiftech/cli generate --output ./Models/Database.swift --lang swift
|
|
245
860
|
\`\`\`
|
|
246
|
-
`}],postInstructions:["Add the VaifClient Swift package from https://github.com/vaif-technologies/vaif-swift","Add VAIF_PROJECT_ID and VAIF_API_KEY to your Xcode scheme environment","See README-VAIF.md for full setup instructions"]},"expo-mobile-app":{name:"Expo Mobile App",description:"React Native / Expo app with VAIF client configured for mobile",tag:"Expo / React Native",files:[{path:"
|
|
861
|
+
`}],postInstructions:["Add the VaifClient Swift package from https://github.com/vaif-technologies/vaif-swift","Add VAIF_PROJECT_ID and VAIF_API_KEY to your Xcode scheme environment","See README-VAIF.md for full setup instructions"]},"expo-mobile-app":{name:"Expo Mobile App",description:"React Native / Expo app with VAIF client configured for mobile",tag:"Expo / React Native",defaultFeatures:["database","auth"],files:[{path:"package.json",content:`{
|
|
862
|
+
"name": "my-vaif-app",
|
|
863
|
+
"version": "0.1.0",
|
|
864
|
+
"main": "expo-router/entry",
|
|
865
|
+
"scripts": {
|
|
866
|
+
"start": "expo start",
|
|
867
|
+
"android": "expo start --android",
|
|
868
|
+
"ios": "expo start --ios",
|
|
869
|
+
"web": "expo start --web"
|
|
870
|
+
},
|
|
871
|
+
"dependencies": {
|
|
872
|
+
"@react-native-async-storage/async-storage": "^2.1.0",
|
|
873
|
+
"@vaiftech/sdk-expo": "^1.0.0",
|
|
874
|
+
"expo": "~52.0.0",
|
|
875
|
+
"expo-router": "~4.0.0",
|
|
876
|
+
"react": "^19.0.0",
|
|
877
|
+
"react-native": "~0.76.0"
|
|
878
|
+
},
|
|
879
|
+
"devDependencies": {
|
|
880
|
+
"@types/react": "^19.0.0",
|
|
881
|
+
"typescript": "^5.7.0"
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
`},{path:"app.json",content:`{
|
|
885
|
+
"expo": {
|
|
886
|
+
"name": "my-vaif-app",
|
|
887
|
+
"slug": "my-vaif-app",
|
|
888
|
+
"version": "1.0.0",
|
|
889
|
+
"scheme": "myvaifapp",
|
|
890
|
+
"platforms": ["ios", "android", "web"],
|
|
891
|
+
"newArchEnabled": true
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
`},{path:"app/_layout.tsx",content:`import { Stack } from "expo-router";
|
|
895
|
+
import { VaifProvider } from "@vaiftech/sdk-expo";
|
|
896
|
+
import { vaif } from "../lib/vaif";
|
|
897
|
+
|
|
898
|
+
export default function RootLayout() {
|
|
899
|
+
return (
|
|
900
|
+
<VaifProvider client={vaif}>
|
|
901
|
+
<Stack>
|
|
902
|
+
<Stack.Screen name="index" options={{ title: "Home" }} />
|
|
903
|
+
</Stack>
|
|
904
|
+
</VaifProvider>
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
`},{path:"app/index.tsx",content:`import { View, Text, StyleSheet } from "react-native";
|
|
908
|
+
|
|
909
|
+
export default function HomeScreen() {
|
|
910
|
+
return (
|
|
911
|
+
<View style={styles.container}>
|
|
912
|
+
<Text style={styles.title}>Welcome to VAIF</Text>
|
|
913
|
+
<Text style={styles.subtitle}>Your mobile app is ready. Start building!</Text>
|
|
914
|
+
</View>
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const styles = StyleSheet.create({
|
|
919
|
+
container: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24 },
|
|
920
|
+
title: { fontSize: 28, fontWeight: "bold", marginBottom: 8 },
|
|
921
|
+
subtitle: { fontSize: 16, color: "#666", textAlign: "center" },
|
|
922
|
+
});
|
|
923
|
+
`},{path:"lib/vaif.ts",content:`import { createExpoClient } from "@vaiftech/sdk-expo";
|
|
247
924
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
248
925
|
|
|
249
926
|
export const vaif = createExpoClient({
|
|
250
927
|
projectId: process.env.EXPO_PUBLIC_VAIF_PROJECT_ID!,
|
|
251
928
|
apiKey: process.env.EXPO_PUBLIC_VAIF_API_KEY!,
|
|
252
929
|
storage: AsyncStorage,
|
|
253
|
-
|
|
254
|
-
realtime: {
|
|
255
|
-
enabled: true,
|
|
256
|
-
},
|
|
257
|
-
});
|
|
258
|
-
`},{path:"app.config.ts",content:`import { ExpoConfig, ConfigContext } from "expo/config";
|
|
259
|
-
|
|
260
|
-
export default ({ config }: ConfigContext): ExpoConfig => ({
|
|
261
|
-
...config,
|
|
262
|
-
name: "my-vaif-app",
|
|
263
|
-
slug: "my-vaif-app",
|
|
264
|
-
extra: {
|
|
265
|
-
// These are available via process.env.EXPO_PUBLIC_* in SDK 49+
|
|
266
|
-
// Set them in .env and they will be embedded at build time
|
|
267
|
-
vaifProjectId: process.env.EXPO_PUBLIC_VAIF_PROJECT_ID,
|
|
268
|
-
vaifApiKey: process.env.EXPO_PUBLIC_VAIF_API_KEY,
|
|
269
|
-
},
|
|
930
|
+
realtime: { enabled: true },
|
|
270
931
|
});
|
|
271
932
|
`},{path:".env.example",content:`# VAIF Configuration
|
|
272
933
|
# Get these values from https://vaif.studio/dashboard \u2192 Project Settings \u2192 API Keys
|
|
273
934
|
|
|
274
935
|
EXPO_PUBLIC_VAIF_PROJECT_ID=your-project-id
|
|
275
936
|
EXPO_PUBLIC_VAIF_API_KEY=your-anon-key
|
|
276
|
-
`}
|
|
937
|
+
`},{path:".gitignore",content:`node_modules
|
|
938
|
+
.expo
|
|
939
|
+
dist
|
|
940
|
+
.env
|
|
941
|
+
`}],featureFiles:{auth:[{path:"app/(auth)/login.tsx",content:`import { useState } from "react";
|
|
942
|
+
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert } from "react-native";
|
|
943
|
+
import { useRouter } from "expo-router";
|
|
944
|
+
import { vaif } from "../../lib/vaif";
|
|
945
|
+
|
|
946
|
+
export default function LoginScreen() {
|
|
947
|
+
const router = useRouter();
|
|
948
|
+
const [email, setEmail] = useState("");
|
|
949
|
+
const [password, setPassword] = useState("");
|
|
950
|
+
const [loading, setLoading] = useState(false);
|
|
951
|
+
|
|
952
|
+
async function handleLogin() {
|
|
953
|
+
setLoading(true);
|
|
954
|
+
const { error } = await vaif.auth.signInWithPassword({ email, password });
|
|
955
|
+
setLoading(false);
|
|
956
|
+
if (error) Alert.alert("Error", error.message);
|
|
957
|
+
else router.replace("/");
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
return (
|
|
961
|
+
<View style={styles.container}>
|
|
962
|
+
<Text style={styles.title}>Log In</Text>
|
|
963
|
+
<TextInput style={styles.input} placeholder="Email" value={email} onChangeText={setEmail} autoCapitalize="none" keyboardType="email-address" />
|
|
964
|
+
<TextInput style={styles.input} placeholder="Password" value={password} onChangeText={setPassword} secureTextEntry />
|
|
965
|
+
<TouchableOpacity style={styles.button} onPress={handleLogin} disabled={loading}>
|
|
966
|
+
<Text style={styles.buttonText}>{loading ? "Logging in..." : "Log In"}</Text>
|
|
967
|
+
</TouchableOpacity>
|
|
968
|
+
</View>
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const styles = StyleSheet.create({
|
|
973
|
+
container: { flex: 1, padding: 24, justifyContent: "center" },
|
|
974
|
+
title: { fontSize: 28, fontWeight: "bold", marginBottom: 24, textAlign: "center" },
|
|
975
|
+
input: { borderWidth: 1, borderColor: "#ccc", borderRadius: 8, padding: 12, marginBottom: 12, fontSize: 16 },
|
|
976
|
+
button: { backgroundColor: "#0070f3", borderRadius: 8, padding: 14, alignItems: "center" },
|
|
977
|
+
buttonText: { color: "#fff", fontSize: 16, fontWeight: "600" },
|
|
978
|
+
});
|
|
979
|
+
`},{path:"app/(auth)/signup.tsx",content:`import { useState } from "react";
|
|
980
|
+
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert } from "react-native";
|
|
981
|
+
import { useRouter } from "expo-router";
|
|
982
|
+
import { vaif } from "../../lib/vaif";
|
|
983
|
+
|
|
984
|
+
export default function SignupScreen() {
|
|
985
|
+
const router = useRouter();
|
|
986
|
+
const [email, setEmail] = useState("");
|
|
987
|
+
const [password, setPassword] = useState("");
|
|
988
|
+
const [loading, setLoading] = useState(false);
|
|
989
|
+
|
|
990
|
+
async function handleSignup() {
|
|
991
|
+
setLoading(true);
|
|
992
|
+
const { error } = await vaif.auth.signUp({ email, password });
|
|
993
|
+
setLoading(false);
|
|
994
|
+
if (error) Alert.alert("Error", error.message);
|
|
995
|
+
else router.replace("/");
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return (
|
|
999
|
+
<View style={styles.container}>
|
|
1000
|
+
<Text style={styles.title}>Sign Up</Text>
|
|
1001
|
+
<TextInput style={styles.input} placeholder="Email" value={email} onChangeText={setEmail} autoCapitalize="none" keyboardType="email-address" />
|
|
1002
|
+
<TextInput style={styles.input} placeholder="Password" value={password} onChangeText={setPassword} secureTextEntry />
|
|
1003
|
+
<TouchableOpacity style={styles.button} onPress={handleSignup} disabled={loading}>
|
|
1004
|
+
<Text style={styles.buttonText}>{loading ? "Creating account..." : "Sign Up"}</Text>
|
|
1005
|
+
</TouchableOpacity>
|
|
1006
|
+
</View>
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const styles = StyleSheet.create({
|
|
1011
|
+
container: { flex: 1, padding: 24, justifyContent: "center" },
|
|
1012
|
+
title: { fontSize: 28, fontWeight: "bold", marginBottom: 24, textAlign: "center" },
|
|
1013
|
+
input: { borderWidth: 1, borderColor: "#ccc", borderRadius: 8, padding: 12, marginBottom: 12, fontSize: 16 },
|
|
1014
|
+
button: { backgroundColor: "#0070f3", borderRadius: 8, padding: 14, alignItems: "center" },
|
|
1015
|
+
buttonText: { color: "#fff", fontSize: 16, fontWeight: "600" },
|
|
1016
|
+
});
|
|
1017
|
+
`}],storage:[{path:"hooks/useImagePicker.ts",content:`import { useState, useCallback } from "react";
|
|
1018
|
+
import * as ImagePicker from "expo-image-picker";
|
|
1019
|
+
import { vaif } from "../lib/vaif";
|
|
1020
|
+
|
|
1021
|
+
interface UploadResult {
|
|
1022
|
+
path: string;
|
|
1023
|
+
publicUrl: string;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
export function useImagePicker(bucket = "uploads") {
|
|
1027
|
+
const [uploading, setUploading] = useState(false);
|
|
1028
|
+
const [error, setError] = useState<string | null>(null);
|
|
1029
|
+
|
|
1030
|
+
const pickAndUpload = useCallback(async (): Promise<UploadResult | null> => {
|
|
1031
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
1032
|
+
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
|
1033
|
+
quality: 0.8,
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
if (result.canceled || !result.assets[0]) return null;
|
|
1037
|
+
|
|
1038
|
+
setUploading(true);
|
|
1039
|
+
setError(null);
|
|
1040
|
+
|
|
1041
|
+
const asset = result.assets[0];
|
|
1042
|
+
const ext = asset.uri.split(".").pop() || "jpg";
|
|
1043
|
+
const filePath = \`\${Date.now()}.\${ext}\`;
|
|
1044
|
+
|
|
1045
|
+
const response = await fetch(asset.uri);
|
|
1046
|
+
const blob = await response.blob();
|
|
1047
|
+
|
|
1048
|
+
const { data, error: uploadError } = await vaif.storage
|
|
1049
|
+
.from(bucket)
|
|
1050
|
+
.upload(filePath, blob);
|
|
1051
|
+
|
|
1052
|
+
if (uploadError) {
|
|
1053
|
+
setError(uploadError.message);
|
|
1054
|
+
setUploading(false);
|
|
1055
|
+
return null;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const { data: urlData } = vaif.storage.from(bucket).getPublicUrl(data.path);
|
|
1059
|
+
setUploading(false);
|
|
1060
|
+
return { path: data.path, publicUrl: urlData.publicUrl };
|
|
1061
|
+
}, [bucket]);
|
|
1062
|
+
|
|
1063
|
+
return { pickAndUpload, uploading, error };
|
|
1064
|
+
}
|
|
1065
|
+
`}],realtime:[{path:"hooks/useRealtimeMessages.ts",content:`import { useEffect, useState, useCallback } from "react";
|
|
1066
|
+
import { vaif } from "../lib/vaif";
|
|
1067
|
+
|
|
1068
|
+
interface Message {
|
|
1069
|
+
id: string;
|
|
1070
|
+
content: string;
|
|
1071
|
+
user_id: string;
|
|
1072
|
+
created_at: string;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
export function useRealtimeMessages(channelId: string) {
|
|
1076
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
1077
|
+
const [loading, setLoading] = useState(true);
|
|
1078
|
+
|
|
1079
|
+
useEffect(() => {
|
|
1080
|
+
vaif.from("messages").select("*").eq("channel_id", channelId)
|
|
1081
|
+
.order("created_at", { ascending: true }).limit(50)
|
|
1082
|
+
.then(({ data }) => {
|
|
1083
|
+
if (data) setMessages(data as Message[]);
|
|
1084
|
+
setLoading(false);
|
|
1085
|
+
});
|
|
1086
|
+
}, [channelId]);
|
|
1087
|
+
|
|
1088
|
+
useEffect(() => {
|
|
1089
|
+
const channel = vaif
|
|
1090
|
+
.channel(\`messages:\${channelId}\`)
|
|
1091
|
+
.on("postgres_changes", { event: "INSERT", schema: "public", table: "messages", filter: \`channel_id=eq.\${channelId}\` }, (payload) => {
|
|
1092
|
+
setMessages((prev) => [...prev, payload.new as Message]);
|
|
1093
|
+
})
|
|
1094
|
+
.subscribe();
|
|
1095
|
+
|
|
1096
|
+
return () => { channel.unsubscribe(); };
|
|
1097
|
+
}, [channelId]);
|
|
1098
|
+
|
|
1099
|
+
const refresh = useCallback(async () => {
|
|
1100
|
+
const { data } = await vaif.from("messages").select("*").eq("channel_id", channelId).order("created_at", { ascending: true }).limit(50);
|
|
1101
|
+
if (data) setMessages(data as Message[]);
|
|
1102
|
+
}, [channelId]);
|
|
1103
|
+
|
|
1104
|
+
return { messages, loading, refresh };
|
|
1105
|
+
}
|
|
1106
|
+
`}]},dependencies:["@vaiftech/sdk-expo","@react-native-async-storage/async-storage","expo","expo-router","react","react-native"],postInstructions:["cd my-vaif-app","npm install","# Copy .env.example to .env and add your VAIF credentials","npx expo start"]},"flutter-app":{name:"Flutter App",description:"Dart/Flutter client setup with environment configuration",tag:"Flutter / Dart",defaultFeatures:["database","auth"],files:[{path:"lib/main.dart",content:`import 'package:flutter/material.dart';
|
|
1107
|
+
import 'vaif_client.dart';
|
|
1108
|
+
|
|
1109
|
+
void main() async {
|
|
1110
|
+
WidgetsFlutterBinding.ensureInitialized();
|
|
1111
|
+
await initVaif();
|
|
1112
|
+
runApp(const MyApp());
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
class MyApp extends StatelessWidget {
|
|
1116
|
+
const MyApp({super.key});
|
|
1117
|
+
|
|
1118
|
+
@override
|
|
1119
|
+
Widget build(BuildContext context) {
|
|
1120
|
+
return MaterialApp(
|
|
1121
|
+
title: 'My VAIF App',
|
|
1122
|
+
theme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true),
|
|
1123
|
+
home: const HomeScreen(),
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
class HomeScreen extends StatelessWidget {
|
|
1129
|
+
const HomeScreen({super.key});
|
|
1130
|
+
|
|
1131
|
+
@override
|
|
1132
|
+
Widget build(BuildContext context) {
|
|
1133
|
+
return Scaffold(
|
|
1134
|
+
appBar: AppBar(title: const Text('My VAIF App')),
|
|
1135
|
+
body: const Center(child: Text('Welcome to VAIF! Start building.')),
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
`},{path:"lib/vaif_client.dart",content:`import 'package:vaif_client/vaif_client.dart';
|
|
277
1140
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
278
1141
|
|
|
279
|
-
/// Global VAIF client instance.
|
|
280
|
-
///
|
|
281
|
-
/// Initialise by calling [initVaif] in your main() before runApp().
|
|
282
1142
|
late final VaifClient vaif;
|
|
283
1143
|
|
|
284
1144
|
Future<void> initVaif() async {
|
|
@@ -288,149 +1148,186 @@ Future<void> initVaif() async {
|
|
|
288
1148
|
final apiKey = dotenv.env['VAIF_API_KEY'];
|
|
289
1149
|
|
|
290
1150
|
if (projectId == null || apiKey == null) {
|
|
291
|
-
throw Exception(
|
|
292
|
-
'VAIF_PROJECT_ID and VAIF_API_KEY must be set in .env',
|
|
293
|
-
);
|
|
1151
|
+
throw Exception('VAIF_PROJECT_ID and VAIF_API_KEY must be set in .env');
|
|
294
1152
|
}
|
|
295
1153
|
|
|
296
1154
|
vaif = VaifClient(
|
|
297
1155
|
projectId: projectId,
|
|
298
1156
|
apiKey: apiKey,
|
|
299
|
-
// Enable realtime subscriptions
|
|
300
1157
|
realtime: const RealtimeConfig(enabled: true),
|
|
301
1158
|
);
|
|
302
1159
|
}
|
|
1160
|
+
`},{path:"pubspec.yaml",content:`name: my_vaif_app
|
|
1161
|
+
description: A Flutter app powered by VAIF.
|
|
1162
|
+
version: 0.1.0
|
|
1163
|
+
publish_to: 'none'
|
|
303
1164
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
final response = await vaif.from(table).select().execute();
|
|
307
|
-
return List<Map<String, dynamic>>.from(response.data);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/// Example: insert a row.
|
|
311
|
-
Future<void> insertRow(String table, Map<String, dynamic> values) async {
|
|
312
|
-
await vaif.from(table).insert(values).execute();
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/// Example: subscribe to realtime changes.
|
|
316
|
-
void subscribeToTable(
|
|
317
|
-
String table,
|
|
318
|
-
void Function(Map<String, dynamic> payload) onEvent,
|
|
319
|
-
) {
|
|
320
|
-
vaif.realtime.channel(table).on(
|
|
321
|
-
RealtimeListenTypes.postgresChanges,
|
|
322
|
-
ChannelFilter(event: '*', schema: 'public', table: table),
|
|
323
|
-
(payload, [ref]) => onEvent(payload),
|
|
324
|
-
).subscribe();
|
|
325
|
-
}
|
|
326
|
-
`},{path:".env.example",content:`# VAIF Configuration
|
|
327
|
-
# Get these values from https://vaif.studio/dashboard \u2192 Project Settings \u2192 API Keys
|
|
328
|
-
|
|
329
|
-
VAIF_PROJECT_ID=your-project-id
|
|
330
|
-
VAIF_API_KEY=your-anon-key
|
|
331
|
-
`},{path:"README-VAIF.md",content:`# VAIF Flutter Setup
|
|
332
|
-
|
|
333
|
-
## 1. Add Dependencies
|
|
1165
|
+
environment:
|
|
1166
|
+
sdk: ^3.5.0
|
|
334
1167
|
|
|
335
|
-
Add the following to your \`pubspec.yaml\`:
|
|
336
|
-
|
|
337
|
-
\`\`\`yaml
|
|
338
1168
|
dependencies:
|
|
1169
|
+
flutter:
|
|
1170
|
+
sdk: flutter
|
|
339
1171
|
vaif_client: ^1.0.0
|
|
340
1172
|
flutter_dotenv: ^5.1.0
|
|
341
|
-
\`\`\`
|
|
342
1173
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
flutter
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
## 2. Configure Environment
|
|
1174
|
+
dev_dependencies:
|
|
1175
|
+
flutter_test:
|
|
1176
|
+
sdk: flutter
|
|
1177
|
+
flutter_lints: ^5.0.0
|
|
349
1178
|
|
|
350
|
-
Copy \`.env.example\` to \`.env\` and fill in your credentials.
|
|
351
|
-
|
|
352
|
-
Add the \`.env\` file to your \`pubspec.yaml\` assets:
|
|
353
|
-
|
|
354
|
-
\`\`\`yaml
|
|
355
1179
|
flutter:
|
|
1180
|
+
uses-material-design: true
|
|
356
1181
|
assets:
|
|
357
1182
|
- .env
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
1183
|
+
`},{path:".env.example",content:`VAIF_PROJECT_ID=your-project-id
|
|
1184
|
+
VAIF_API_KEY=your-anon-key
|
|
1185
|
+
`},{path:".gitignore",content:`build/
|
|
1186
|
+
.dart_tool/
|
|
1187
|
+
.packages
|
|
1188
|
+
.env
|
|
1189
|
+
*.iml
|
|
1190
|
+
`}],featureFiles:{auth:[{path:"lib/screens/login_screen.dart",content:`import 'package:flutter/material.dart';
|
|
1191
|
+
import '../vaif_client.dart';
|
|
1192
|
+
|
|
1193
|
+
class LoginScreen extends StatefulWidget {
|
|
1194
|
+
const LoginScreen({super.key});
|
|
1195
|
+
|
|
1196
|
+
@override
|
|
1197
|
+
State<LoginScreen> createState() => _LoginScreenState();
|
|
1198
|
+
}
|
|
361
1199
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
1200
|
+
class _LoginScreenState extends State<LoginScreen> {
|
|
1201
|
+
final _emailController = TextEditingController();
|
|
1202
|
+
final _passwordController = TextEditingController();
|
|
1203
|
+
bool _loading = false;
|
|
1204
|
+
String? _error;
|
|
1205
|
+
|
|
1206
|
+
Future<void> _handleLogin() async {
|
|
1207
|
+
setState(() { _loading = true; _error = null; });
|
|
1208
|
+
|
|
1209
|
+
try {
|
|
1210
|
+
await vaif.auth.signInWithPassword(
|
|
1211
|
+
email: _emailController.text,
|
|
1212
|
+
password: _passwordController.text,
|
|
1213
|
+
);
|
|
1214
|
+
if (mounted) Navigator.of(context).pushReplacementNamed('/');
|
|
1215
|
+
} catch (e) {
|
|
1216
|
+
setState(() { _error = e.toString(); });
|
|
1217
|
+
} finally {
|
|
1218
|
+
if (mounted) setState(() { _loading = false; });
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
365
1221
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
1222
|
+
@override
|
|
1223
|
+
Widget build(BuildContext context) {
|
|
1224
|
+
return Scaffold(
|
|
1225
|
+
appBar: AppBar(title: const Text('Log In')),
|
|
1226
|
+
body: Padding(
|
|
1227
|
+
padding: const EdgeInsets.all(24),
|
|
1228
|
+
child: Column(
|
|
1229
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
1230
|
+
children: [
|
|
1231
|
+
TextField(controller: _emailController, decoration: const InputDecoration(labelText: 'Email'), keyboardType: TextInputType.emailAddress),
|
|
1232
|
+
const SizedBox(height: 12),
|
|
1233
|
+
TextField(controller: _passwordController, decoration: const InputDecoration(labelText: 'Password'), obscureText: true),
|
|
1234
|
+
const SizedBox(height: 24),
|
|
1235
|
+
if (_error != null) Text(_error!, style: const TextStyle(color: Colors.red)),
|
|
1236
|
+
const SizedBox(height: 12),
|
|
1237
|
+
ElevatedButton(
|
|
1238
|
+
onPressed: _loading ? null : _handleLogin,
|
|
1239
|
+
child: Text(_loading ? 'Logging in...' : 'Log In'),
|
|
1240
|
+
),
|
|
1241
|
+
],
|
|
1242
|
+
),
|
|
1243
|
+
),
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
370
1246
|
}
|
|
371
|
-
|
|
1247
|
+
`}]},postInstructions:["flutter pub get","# Copy .env.example to .env and add your VAIF credentials","flutter run"]},"python-fastapi-backend":{name:"Python FastAPI Backend",description:"FastAPI backend with VAIF client, auth middleware, and type-safe queries",tag:"Python / FastAPI",defaultFeatures:["database","auth"],files:[{path:"main.py",content:`"""VAIF FastAPI application."""
|
|
372
1248
|
|
|
373
|
-
|
|
1249
|
+
from fastapi import FastAPI
|
|
1250
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
1251
|
+
from vaif_client import get_vaif_client
|
|
374
1252
|
|
|
375
|
-
|
|
376
|
-
// Query data
|
|
377
|
-
final todos = await vaif.from('todos').select().execute();
|
|
1253
|
+
app = FastAPI(title="My VAIF API", version="0.1.0")
|
|
378
1254
|
|
|
379
|
-
|
|
380
|
-
|
|
1255
|
+
app.add_middleware(
|
|
1256
|
+
CORSMiddleware,
|
|
1257
|
+
allow_origins=["*"],
|
|
1258
|
+
allow_credentials=True,
|
|
1259
|
+
allow_methods=["*"],
|
|
1260
|
+
allow_headers=["*"],
|
|
1261
|
+
)
|
|
381
1262
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
1263
|
+
|
|
1264
|
+
@app.get("/")
|
|
1265
|
+
async def root():
|
|
1266
|
+
return {"status": "ok", "message": "VAIF API running"}
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
@app.get("/health")
|
|
1270
|
+
async def health():
|
|
1271
|
+
return {"status": "healthy"}
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
@app.get("/items")
|
|
1275
|
+
async def list_items():
|
|
1276
|
+
client = get_vaif_client()
|
|
1277
|
+
result = await client.from_("items").select("*").execute()
|
|
1278
|
+
return {"data": result.data}
|
|
1279
|
+
`},{path:"vaif_client.py",content:`"""VAIF client setup for FastAPI."""
|
|
392
1280
|
|
|
393
1281
|
import os
|
|
394
1282
|
from functools import lru_cache
|
|
395
|
-
from typing import Optional
|
|
396
1283
|
|
|
397
1284
|
from dotenv import load_dotenv
|
|
398
|
-
from fastapi import Depends, HTTPException, Request, status
|
|
399
|
-
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
400
1285
|
from vaif import Client, ServiceClient
|
|
401
1286
|
|
|
402
1287
|
load_dotenv()
|
|
403
1288
|
|
|
404
1289
|
VAIF_PROJECT_ID = os.environ["VAIF_PROJECT_ID"]
|
|
405
1290
|
VAIF_API_KEY = os.environ["VAIF_API_KEY"]
|
|
406
|
-
VAIF_SECRET_KEY = os.environ
|
|
1291
|
+
VAIF_SECRET_KEY = os.environ.get("VAIF_SECRET_KEY", "")
|
|
407
1292
|
|
|
408
1293
|
|
|
409
1294
|
@lru_cache()
|
|
410
1295
|
def get_vaif_client() -> Client:
|
|
411
1296
|
"""Public client using the anon key (respects Row Level Security)."""
|
|
412
|
-
return Client(
|
|
413
|
-
project_id=VAIF_PROJECT_ID,
|
|
414
|
-
api_key=VAIF_API_KEY,
|
|
415
|
-
)
|
|
1297
|
+
return Client(project_id=VAIF_PROJECT_ID, api_key=VAIF_API_KEY)
|
|
416
1298
|
|
|
417
1299
|
|
|
418
1300
|
@lru_cache()
|
|
419
1301
|
def get_vaif_admin() -> ServiceClient:
|
|
420
1302
|
"""Admin/service client that bypasses RLS. Use with caution."""
|
|
421
|
-
return ServiceClient(
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
1303
|
+
return ServiceClient(project_id=VAIF_PROJECT_ID, secret_key=VAIF_SECRET_KEY)
|
|
1304
|
+
`},{path:"requirements.txt",content:`vaif-client>=1.0.0
|
|
1305
|
+
fastapi>=0.110.0
|
|
1306
|
+
uvicorn[standard]>=0.27.0
|
|
1307
|
+
python-dotenv>=1.0.0
|
|
1308
|
+
`},{path:".env.example",content:`# VAIF Configuration
|
|
1309
|
+
# Get these values from https://vaif.studio/dashboard \u2192 Project Settings \u2192 API Keys
|
|
1310
|
+
|
|
1311
|
+
VAIF_PROJECT_ID=your-project-id
|
|
1312
|
+
VAIF_API_KEY=your-anon-key
|
|
1313
|
+
VAIF_SECRET_KEY=your-secret-key
|
|
1314
|
+
`},{path:".gitignore",content:`__pycache__
|
|
1315
|
+
*.pyc
|
|
1316
|
+
.env
|
|
1317
|
+
.venv
|
|
1318
|
+
venv
|
|
1319
|
+
`}],featureFiles:{auth:[{path:"middleware/auth.py",content:`"""VAIF auth middleware for FastAPI."""
|
|
425
1320
|
|
|
1321
|
+
from typing import Optional
|
|
426
1322
|
|
|
427
|
-
|
|
1323
|
+
from fastapi import Depends, HTTPException, status
|
|
1324
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
1325
|
+
from vaif_client import get_vaif_admin
|
|
428
1326
|
|
|
429
1327
|
security = HTTPBearer(auto_error=False)
|
|
430
1328
|
|
|
431
1329
|
|
|
432
1330
|
async def get_current_user(
|
|
433
|
-
request: Request,
|
|
434
1331
|
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
|
435
1332
|
):
|
|
436
1333
|
"""FastAPI dependency that validates VAIF auth tokens.
|
|
@@ -462,52 +1359,77 @@ async def get_current_user(
|
|
|
462
1359
|
)
|
|
463
1360
|
|
|
464
1361
|
return user
|
|
1362
|
+
`},{path:"middleware/__init__.py",content:""}],storage:[{path:"routes/storage.py",content:`"""File upload routes using VAIF Storage."""
|
|
465
1363
|
|
|
1364
|
+
from fastapi import APIRouter, UploadFile, File
|
|
1365
|
+
from vaif_client import get_vaif_admin
|
|
466
1366
|
|
|
467
|
-
|
|
1367
|
+
router = APIRouter(prefix="/storage", tags=["storage"])
|
|
468
1368
|
|
|
1369
|
+
BUCKET = "uploads"
|
|
469
1370
|
|
|
470
|
-
async def query_table(table: str, filters: Optional[dict] = None):
|
|
471
|
-
"""Quick helper to query a table with optional filters."""
|
|
472
|
-
client = get_vaif_client()
|
|
473
|
-
q = client.from_(table).select("*")
|
|
474
|
-
if filters:
|
|
475
|
-
for key, value in filters.items():
|
|
476
|
-
q = q.eq(key, value)
|
|
477
|
-
return await q.execute()
|
|
478
1371
|
|
|
1372
|
+
@router.post("/upload")
|
|
1373
|
+
async def upload_file(file: UploadFile = File(...)):
|
|
1374
|
+
client = get_vaif_admin()
|
|
1375
|
+
file_path = f"{file.filename}"
|
|
1376
|
+
contents = await file.read()
|
|
1377
|
+
|
|
1378
|
+
result = await client.storage.from_(BUCKET).upload(file_path, contents)
|
|
1379
|
+
url_data = client.storage.from_(BUCKET).get_public_url(result.path)
|
|
1380
|
+
|
|
1381
|
+
return {"path": result.path, "public_url": url_data}
|
|
1382
|
+
`},{path:"routes/__init__.py",content:""}],functions:[{path:"routes/functions.py",content:`"""Invoke VAIF serverless functions."""
|
|
1383
|
+
|
|
1384
|
+
from fastapi import APIRouter
|
|
1385
|
+
from vaif_client import get_vaif_client
|
|
479
1386
|
|
|
480
|
-
|
|
481
|
-
|
|
1387
|
+
router = APIRouter(prefix="/functions", tags=["functions"])
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
@router.post("/invoke/{function_name}")
|
|
1391
|
+
async def invoke_function(function_name: str, payload: dict = {}):
|
|
482
1392
|
client = get_vaif_client()
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
1393
|
+
result = await client.functions.invoke(function_name, body=payload)
|
|
1394
|
+
return {"data": result}
|
|
1395
|
+
`}]},postInstructions:["pip install -r requirements.txt","# Copy .env.example to .env and add your VAIF credentials","uvicorn main:app --reload"]},"go-backend-api":{name:"Go Backend API",description:"Go backend with VAIF client initialisation and HTTP middleware",tag:"Go",defaultFeatures:["database","auth"],files:[{path:"main.go",content:`package main
|
|
486
1396
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
1397
|
+
import (
|
|
1398
|
+
"encoding/json"
|
|
1399
|
+
"log"
|
|
1400
|
+
"net/http"
|
|
1401
|
+
|
|
1402
|
+
"github.com/joho/godotenv"
|
|
1403
|
+
"myapp/vaif"
|
|
1404
|
+
)
|
|
1405
|
+
|
|
1406
|
+
func main() {
|
|
1407
|
+
_ = godotenv.Load()
|
|
1408
|
+
|
|
1409
|
+
if err := vaif.Init(); err != nil {
|
|
1410
|
+
log.Fatalf("Failed to initialise VAIF: %v", err)
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
mux := http.NewServeMux()
|
|
1414
|
+
|
|
1415
|
+
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
|
|
1416
|
+
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
|
1417
|
+
})
|
|
1418
|
+
|
|
1419
|
+
log.Println("Server running on :8080")
|
|
1420
|
+
log.Fatal(http.ListenAndServe(":8080", mux))
|
|
1421
|
+
}
|
|
1422
|
+
`},{path:"vaif/client.go",content:`package vaif
|
|
495
1423
|
|
|
496
1424
|
import (
|
|
497
1425
|
"fmt"
|
|
498
|
-
"net/http"
|
|
499
1426
|
"os"
|
|
500
|
-
"strings"
|
|
501
1427
|
|
|
502
1428
|
vaifclient "github.com/vaif-technologies/vaif-go"
|
|
503
1429
|
)
|
|
504
1430
|
|
|
505
|
-
// Client is the global VAIF client instance.
|
|
506
|
-
// Initialise by calling Init() at application start.
|
|
507
1431
|
var Client *vaifclient.Client
|
|
508
1432
|
|
|
509
|
-
// Init creates the VAIF client from environment variables.
|
|
510
|
-
// Call this in main() before starting your HTTP server.
|
|
511
1433
|
func Init() error {
|
|
512
1434
|
projectID := os.Getenv("VAIF_PROJECT_ID")
|
|
513
1435
|
apiKey := os.Getenv("VAIF_API_KEY")
|
|
@@ -529,9 +1451,34 @@ func Init() error {
|
|
|
529
1451
|
|
|
530
1452
|
return nil
|
|
531
1453
|
}
|
|
1454
|
+
`},{path:"go.mod",content:`module myapp
|
|
1455
|
+
|
|
1456
|
+
go 1.22
|
|
1457
|
+
|
|
1458
|
+
require (
|
|
1459
|
+
github.com/joho/godotenv v1.5.1
|
|
1460
|
+
github.com/vaif-technologies/vaif-go v1.0.0
|
|
1461
|
+
)
|
|
1462
|
+
`},{path:".env.example",content:`# VAIF Configuration
|
|
1463
|
+
VAIF_PROJECT_ID=your-project-id
|
|
1464
|
+
VAIF_API_KEY=your-anon-key
|
|
1465
|
+
VAIF_SECRET_KEY=your-secret-key
|
|
1466
|
+
`},{path:".gitignore",content:`*.exe
|
|
1467
|
+
*.exe~
|
|
1468
|
+
*.dll
|
|
1469
|
+
*.so
|
|
1470
|
+
*.dylib
|
|
1471
|
+
.env
|
|
1472
|
+
`}],featureFiles:{auth:[{path:"middleware/auth.go",content:`package middleware
|
|
1473
|
+
|
|
1474
|
+
import (
|
|
1475
|
+
"net/http"
|
|
1476
|
+
"strings"
|
|
1477
|
+
|
|
1478
|
+
"myapp/vaif"
|
|
1479
|
+
vaifclient "github.com/vaif-technologies/vaif-go"
|
|
1480
|
+
)
|
|
532
1481
|
|
|
533
|
-
// AuthMiddleware validates the VAIF auth token from the Authorization header.
|
|
534
|
-
// It injects the authenticated user into the request context.
|
|
535
1482
|
func AuthMiddleware(next http.Handler) http.Handler {
|
|
536
1483
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
537
1484
|
auth := r.Header.Get("Authorization")
|
|
@@ -546,7 +1493,7 @@ func AuthMiddleware(next http.Handler) http.Handler {
|
|
|
546
1493
|
return
|
|
547
1494
|
}
|
|
548
1495
|
|
|
549
|
-
user, err := Client.Auth.GetUser(r.Context(), token)
|
|
1496
|
+
user, err := vaif.Client.Auth.GetUser(r.Context(), token)
|
|
550
1497
|
if err != nil {
|
|
551
1498
|
http.Error(w, "invalid or expired token", http.StatusUnauthorized)
|
|
552
1499
|
return
|
|
@@ -556,85 +1503,48 @@ func AuthMiddleware(next http.Handler) http.Handler {
|
|
|
556
1503
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
557
1504
|
})
|
|
558
1505
|
}
|
|
559
|
-
`},{path:".
|
|
560
|
-
# Get these values from https://vaif.studio/dashboard \u2192 Project Settings \u2192 API Keys
|
|
561
|
-
|
|
562
|
-
VAIF_PROJECT_ID=your-project-id
|
|
563
|
-
VAIF_API_KEY=your-anon-key
|
|
564
|
-
VAIF_SECRET_KEY=your-secret-key
|
|
565
|
-
`},{path:"README-VAIF.md",content:`# VAIF Go Setup
|
|
566
|
-
|
|
567
|
-
## 1. Install the Go Client
|
|
568
|
-
|
|
569
|
-
\`\`\`bash
|
|
570
|
-
go get github.com/vaif-technologies/vaif-go
|
|
571
|
-
\`\`\`
|
|
572
|
-
|
|
573
|
-
## 2. Configure Environment
|
|
574
|
-
|
|
575
|
-
Copy \`.env.example\` to \`.env\` and fill in your credentials.
|
|
576
|
-
|
|
577
|
-
Use a library like [godotenv](https://github.com/joho/godotenv) to load the file:
|
|
578
|
-
|
|
579
|
-
\`\`\`bash
|
|
580
|
-
go get github.com/joho/godotenv
|
|
581
|
-
\`\`\`
|
|
582
|
-
|
|
583
|
-
## 3. Initialise in main.go
|
|
584
|
-
|
|
585
|
-
\`\`\`go
|
|
586
|
-
package main
|
|
1506
|
+
`}],storage:[{path:"handlers/storage.go",content:`package handlers
|
|
587
1507
|
|
|
588
1508
|
import (
|
|
589
|
-
"
|
|
1509
|
+
"encoding/json"
|
|
1510
|
+
"fmt"
|
|
1511
|
+
"io"
|
|
590
1512
|
"net/http"
|
|
1513
|
+
"time"
|
|
591
1514
|
|
|
592
|
-
"
|
|
593
|
-
"yourmodule/vaif"
|
|
1515
|
+
"myapp/vaif"
|
|
594
1516
|
)
|
|
595
1517
|
|
|
596
|
-
func
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
log.Fatalf("Failed to initialise VAIF: %v", err)
|
|
1518
|
+
func UploadHandler(w http.ResponseWriter, r *http.Request) {
|
|
1519
|
+
if r.Method != http.MethodPost {
|
|
1520
|
+
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
1521
|
+
return
|
|
601
1522
|
}
|
|
602
1523
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
protected := vaif.AuthMiddleware(http.HandlerFunc(protectedHandler))
|
|
610
|
-
mux.Handle("/api/data", protected)
|
|
1524
|
+
file, header, err := r.FormFile("file")
|
|
1525
|
+
if err != nil {
|
|
1526
|
+
http.Error(w, "invalid file", http.StatusBadRequest)
|
|
1527
|
+
return
|
|
1528
|
+
}
|
|
1529
|
+
defer file.Close()
|
|
611
1530
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
1531
|
+
data, err := io.ReadAll(file)
|
|
1532
|
+
if err != nil {
|
|
1533
|
+
http.Error(w, "failed to read file", http.StatusInternalServerError)
|
|
1534
|
+
return
|
|
1535
|
+
}
|
|
615
1536
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
1537
|
+
path := fmt.Sprintf("%d-%s", time.Now().Unix(), header.Filename)
|
|
1538
|
+
result, err := vaif.Client.Storage.From("uploads").Upload(r.Context(), path, data)
|
|
1539
|
+
if err != nil {
|
|
1540
|
+
http.Error(w, "upload failed", http.StatusInternalServerError)
|
|
1541
|
+
return
|
|
1542
|
+
}
|
|
619
1543
|
|
|
620
|
-
|
|
621
|
-
w.
|
|
1544
|
+
w.Header().Set("Content-Type", "application/json")
|
|
1545
|
+
json.NewEncoder(w).Encode(result)
|
|
622
1546
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
## 4. Query Data
|
|
626
|
-
|
|
627
|
-
\`\`\`go
|
|
628
|
-
// Fetch rows
|
|
629
|
-
rows, err := vaif.Client.From("todos").Select("*").Execute(ctx)
|
|
630
|
-
|
|
631
|
-
// Insert a row
|
|
632
|
-
_, err := vaif.Client.From("todos").Insert(map[string]interface{}{
|
|
633
|
-
"title": "Buy milk",
|
|
634
|
-
"done": false,
|
|
635
|
-
}).Execute(ctx)
|
|
636
|
-
\`\`\`
|
|
637
|
-
`}],postInstructions:["Run: go get github.com/vaif-technologies/vaif-go","Copy .env.example to .env and fill in your project credentials","Call vaif.Init() in main() before starting your server","See README-VAIF.md for full setup instructions"]},"todo-app":{name:"Todo App",description:"Simple React todo app \u2013 great for learning VAIF basics",tag:"React Starter",files:[{path:"src/lib/vaif.ts",content:`import { createClient } from "@vaiftech/client";
|
|
1547
|
+
`}]},postInstructions:["go mod tidy","# Copy .env.example to .env and add your VAIF credentials","go run main.go"]},"todo-app":{name:"Todo App",description:"Simple React todo app \u2013 great for learning VAIF basics",tag:"React Starter",defaultFeatures:["database"],files:[{path:"src/lib/vaif.ts",content:`import { createClient } from "@vaiftech/client";
|
|
638
1548
|
|
|
639
1549
|
export const vaif = createClient({
|
|
640
1550
|
projectId: import.meta.env.VITE_VAIF_PROJECT_ID,
|
|
@@ -693,7 +1603,7 @@ export async function deleteTodo(id: string): Promise<void> {
|
|
|
693
1603
|
|
|
694
1604
|
VITE_VAIF_PROJECT_ID=your-project-id
|
|
695
1605
|
VITE_VAIF_API_KEY=your-anon-key
|
|
696
|
-
`}],dependencies:["@vaiftech/client","@vaiftech/react"],postInstructions:["Copy .env.example to .env and fill in your project credentials","Create a 'todos' table in your VAIF dashboard with columns: id (uuid), title (text), done (boolean), created_at (timestamptz)","Import helpers from './lib/vaif' in your components","Run: npx vaif generate to generate TypeScript types"]},"realtime-chat":{name:"Realtime Chat",description:"React chat app with VAIF realtime subscriptions for live messaging",tag:"React + Realtime",files:[{path:"src/lib/vaif.ts",content:`import { createClient } from "@vaiftech/client";
|
|
1606
|
+
`}],dependencies:["@vaiftech/client","@vaiftech/react"],postInstructions:["Copy .env.example to .env and fill in your project credentials","Create a 'todos' table in your VAIF dashboard with columns: id (uuid), title (text), done (boolean), created_at (timestamptz)","Import helpers from './lib/vaif' in your components","Run: npx vaif generate to generate TypeScript types"]},"realtime-chat":{name:"Realtime Chat",description:"React chat app with VAIF realtime subscriptions for live messaging",tag:"React + Realtime",defaultFeatures:["database","realtime","auth"],files:[{path:"src/lib/vaif.ts",content:`import { createClient } from "@vaiftech/client";
|
|
697
1607
|
|
|
698
1608
|
export const vaif = createClient({
|
|
699
1609
|
projectId: import.meta.env.VITE_VAIF_PROJECT_ID,
|
|
@@ -846,7 +1756,7 @@ export function useRealtimeMessages({
|
|
|
846
1756
|
|
|
847
1757
|
VITE_VAIF_PROJECT_ID=your-project-id
|
|
848
1758
|
VITE_VAIF_API_KEY=your-anon-key
|
|
849
|
-
`}],dependencies:["@vaiftech/client","@vaiftech/react"],postInstructions:["Copy .env.example to .env and fill in your project credentials","Create a 'messages' table with columns: id (uuid), content (text), user_id (text), username (text), channel_id (text), created_at (timestamptz)","Enable Realtime on the messages table in your VAIF dashboard","Use the useRealtimeMessages hook in your components","Run: npx vaif generate to generate TypeScript types"]},"saas-starter":{name:"SaaS Starter",description:"Full SaaS starter with VAIF auth, team/org support, and server-side helpers",tag:"Next.js SaaS",files:[{path:"lib/vaif.ts",content:`import { createClient } from "@vaiftech/client";
|
|
1759
|
+
`}],dependencies:["@vaiftech/client","@vaiftech/react"],postInstructions:["Copy .env.example to .env and fill in your project credentials","Create a 'messages' table with columns: id (uuid), content (text), user_id (text), username (text), channel_id (text), created_at (timestamptz)","Enable Realtime on the messages table in your VAIF dashboard","Use the useRealtimeMessages hook in your components","Run: npx vaif generate to generate TypeScript types"]},"saas-starter":{name:"SaaS Starter",description:"Full SaaS starter with VAIF auth, team/org support, and server-side helpers",tag:"Next.js SaaS",defaultFeatures:["database","auth","functions"],files:[{path:"lib/vaif.ts",content:`import { createClient } from "@vaiftech/client";
|
|
850
1760
|
import { createServerClient } from "@vaiftech/client/server";
|
|
851
1761
|
|
|
852
1762
|
// Browser client \u2013 use in Client Components
|
|
@@ -1015,7 +1925,7 @@ export async function requireTeamRole(
|
|
|
1015
1925
|
NEXT_PUBLIC_VAIF_PROJECT_ID=your-project-id
|
|
1016
1926
|
NEXT_PUBLIC_VAIF_API_KEY=your-anon-key
|
|
1017
1927
|
VAIF_SECRET_KEY=your-secret-key
|
|
1018
|
-
`}],dependencies:["@vaiftech/client","@vaiftech/auth","@vaiftech/react"],postInstructions:["Copy .env.example to .env.local and fill in your project credentials","Create 'teams' and 'team_members' tables in your VAIF dashboard","Import auth helpers from '@/lib/auth' in your Server Components/Actions","Use requireUser() for authenticated routes and requireTeamRole() for role checks","Run: npx vaif generate to generate TypeScript types"]},"ecommerce-api":{name:"E-commerce API",description:"API-first e-commerce setup with VAIF storage for product images",tag:"Next.js E-commerce",files:[{path:"lib/vaif.ts",content:`import { createClient } from "@vaiftech/client";
|
|
1928
|
+
`}],dependencies:["@vaiftech/client","@vaiftech/auth","@vaiftech/react"],postInstructions:["Copy .env.example to .env.local and fill in your project credentials","Create 'teams' and 'team_members' tables in your VAIF dashboard","Import auth helpers from '@/lib/auth' in your Server Components/Actions","Use requireUser() for authenticated routes and requireTeamRole() for role checks","Run: npx vaif generate to generate TypeScript types"]},"ecommerce-api":{name:"E-commerce API",description:"API-first e-commerce setup with VAIF storage for product images",tag:"Next.js E-commerce",defaultFeatures:["database","auth","storage"],files:[{path:"lib/vaif.ts",content:`import { createClient } from "@vaiftech/client";
|
|
1019
1929
|
import { createServerClient } from "@vaiftech/client/server";
|
|
1020
1930
|
|
|
1021
1931
|
// Browser client
|
|
@@ -1146,20 +2056,21 @@ function getContentType(fileName: string): string {
|
|
|
1146
2056
|
NEXT_PUBLIC_VAIF_PROJECT_ID=your-project-id
|
|
1147
2057
|
NEXT_PUBLIC_VAIF_API_KEY=your-anon-key
|
|
1148
2058
|
VAIF_SECRET_KEY=your-secret-key
|
|
1149
|
-
`}],dependencies:["@vaiftech/client","@vaiftech/auth"],postInstructions:["Copy .env.example to .env.local and fill in your project credentials","Create a 'product-images' storage bucket in your VAIF dashboard","Import storage helpers from '@/lib/storage' in your API routes","Use uploadProductImage() in your product creation flow","Run: npx vaif generate to generate TypeScript types"]}};async function
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
2059
|
+
`}],dependencies:["@vaiftech/client","@vaiftech/auth"],postInstructions:["Copy .env.example to .env.local and fill in your project credentials","Create a 'product-images' storage bucket in your VAIF dashboard","Import storage helpers from '@/lib/storage' in your API routes","Use uploadProductImage() in your product creation flow","Run: npx vaif generate to generate TypeScript types"]}};async function J(r){if(!process.stdin.isTTY||!process.stdout.isTTY)return r;let a=new Set(r.map(e=>h.findIndex(o=>o.name===e)).filter(e=>e>=0)),t=0;return new Promise(e=>{let o=k__default.default.createInterface({input:process.stdin,output:process.stdout});k__default.default.emitKeypressEvents(process.stdin,o),process.stdin.setRawMode&&process.stdin.setRawMode(true);function i(){let s=h.length+2;process.stdout.write(`\x1B[${s}A`),l();}function l(){console.log(u__default.default.bold(`
|
|
2060
|
+
? Which VAIF features do you want to include?`)),h.forEach((s,n)=>{let p=a.has(n)?u__default.default.green("[x]"):"[ ]",c=n===t?u__default.default.cyan("> "):" ";console.log(`${c}${p} ${s.label} ${u__default.default.gray(`(${s.description})`)}`);}),console.log(u__default.default.gray(" (up/down to move, space to toggle, enter to confirm)"));}l(),process.stdin.on("keypress",(s,n)=>{if(n.name==="up"&&t>0)t--,i();else if(n.name==="down"&&t<h.length-1)t++,i();else if(n.name==="space")a.has(t)?a.delete(t):a.add(t),i();else if(n.name==="return"){process.stdin.setRawMode&&process.stdin.setRawMode(false),o.close();let p=[...a].sort().map(c=>h[c].name);e(p.length>0?p:r);}else n.name==="c"&&n.ctrl&&(process.stdin.setRawMode&&process.stdin.setRawMode(false),o.close(),process.exit(0));});})}async function D(r,a={}){let t=z[r];t||(console.log(u__default.default.red(`
|
|
2061
|
+
Unknown template: ${r}`)),console.log(u__default.default.yellow(`Run 'vaif templates' to see available templates.
|
|
2062
|
+
`)),process.exit(1));let e;a.features&&a.features.length>0?e=a.features.filter(s=>h.some(n=>n.name===s)):t.featureFiles&&Object.keys(t.featureFiles).length>0?e=await J(t.defaultFeatures??["database","auth"]):e=t.defaultFeatures??[],console.log(""),console.log(u__default.default.bold(`Scaffolding ${u__default.default.cyan(t.name)} template...`)),e.length>0&&console.log(u__default.default.gray(` Features: ${e.join(", ")}`)),console.log("");let o=[...t.files];if(t.featureFiles)for(let s of e){let n=t.featureFiles[s];n&&o.push(...n);}let i=0,l=0;for(let s of o){let n=S__default.default.resolve(s.path),p=S__default.default.dirname(n);if(v__default.default.existsSync(p)||v__default.default.mkdirSync(p,{recursive:true}),v__default.default.existsSync(n)&&!a.force){console.log(u__default.default.yellow(` skip ${s.path} (already exists)`)),l++;continue}v__default.default.writeFileSync(n,s.content,"utf-8"),console.log(u__default.default.green(` create ${s.path}`)),i++;}console.log(""),i>0&&console.log(u__default.default.green(`Created ${i} file${i!==1?"s":""}.`)),l>0&&console.log(u__default.default.yellow(`Skipped ${l} file${l!==1?"s":""} (use --force to overwrite).`)),(t.dependencies?.length||t.devDependencies?.length)&&(console.log(""),console.log(u__default.default.bold("Install dependencies:")),t.dependencies?.length&&console.log(u__default.default.cyan(` npm install ${t.dependencies.join(" ")}`)),t.devDependencies?.length&&console.log(u__default.default.cyan(` npm install -D ${t.devDependencies.join(" ")}`))),console.log(""),console.log(u__default.default.bold.green("Project scaffolded successfully!")),console.log(""),console.log(u__default.default.bold(" Next steps:")),t.postInstructions.forEach(s=>{console.log(u__default.default.gray(` ${s}`));}),console.log(""),console.log(u__default.default.gray(" Get your project credentials at https://vaif.studio")),console.log("");}var H={$schema:"https://vaif.studio/schemas/config.json",projectId:"",database:{url:"${DATABASE_URL}",schema:"public"},types:{output:"./src/types/database.ts"},api:{baseUrl:"https://api.vaif.studio"}};async function G(r){let a=L__default.default("Initializing VAIF configuration...").start(),t=S__default.default.resolve("vaif.config.json");v__default.default.existsSync(t)&&!r.force&&(a.fail("vaif.config.json already exists"),console.log(u__default.default.yellow(`
|
|
2063
|
+
Use --force to overwrite existing configuration.`)),process.exit(1));try{if(v__default.default.writeFileSync(t,JSON.stringify(H,null,2),"utf-8"),a.succeed("Created vaif.config.json"),r.template){let e=r.features?r.features.split(",").map(o=>o.trim()):void 0;await D(r.template,{force:r.force,features:e});}else {let e=S__default.default.resolve(".env.example");if(v__default.default.existsSync(e)||(v__default.default.writeFileSync(e,`# VAIF Configuration
|
|
1153
2064
|
DATABASE_URL=postgresql://user:password@localhost:5432/database
|
|
1154
2065
|
VAIF_API_KEY=your-api-key
|
|
1155
|
-
`,"utf-8"),console.log(
|
|
1156
|
-
Error: ${e.message}`)),process.exit(1);}}async function
|
|
2066
|
+
`,"utf-8"),console.log(u__default.default.gray("Created .env.example"))),r.typescript){let o=S__default.default.resolve("src/types");v__default.default.existsSync(o)||(v__default.default.mkdirSync(o,{recursive:!0}),console.log(u__default.default.gray("Created src/types directory")));}console.log(""),console.log(u__default.default.green("VAIF initialized successfully!")),console.log(""),console.log(u__default.default.gray("Next steps:")),console.log(u__default.default.gray(" 1. Update vaif.config.json with your project ID")),console.log(u__default.default.gray(" 2. Set DATABASE_URL in your environment")),console.log(u__default.default.gray(" 3. Run: npx vaif generate")),console.log("");}}catch(e){a.fail("Failed to initialize"),e instanceof Error&&console.error(u__default.default.red(`
|
|
2067
|
+
Error: ${e.message}`)),process.exit(1);}}async function Fe(r){let{connectionString:a,schema:t="public"}=r,e=new N__default.default.Client({connectionString:a});await e.connect();try{let o=await e.query(`
|
|
1157
2068
|
SELECT table_name, table_type
|
|
1158
2069
|
FROM information_schema.tables
|
|
1159
2070
|
WHERE table_schema = $1
|
|
1160
2071
|
AND table_type = 'BASE TABLE'
|
|
1161
2072
|
ORDER BY table_name
|
|
1162
|
-
`,[t]),
|
|
2073
|
+
`,[t]),i=await e.query(`
|
|
1163
2074
|
SELECT
|
|
1164
2075
|
table_name,
|
|
1165
2076
|
column_name,
|
|
@@ -1174,7 +2085,7 @@ Error: ${e.message}`)),process.exit(1);}}async function we(r){let{connectionStri
|
|
|
1174
2085
|
FROM information_schema.columns
|
|
1175
2086
|
WHERE table_schema = $1
|
|
1176
2087
|
ORDER BY table_name, ordinal_position
|
|
1177
|
-
`,[t]),
|
|
2088
|
+
`,[t]),l=await e.query(`
|
|
1178
2089
|
SELECT
|
|
1179
2090
|
t.typname as enum_name,
|
|
1180
2091
|
e.enumlabel as enum_value
|
|
@@ -1183,16 +2094,16 @@ Error: ${e.message}`)),process.exit(1);}}async function we(r){let{connectionStri
|
|
|
1183
2094
|
JOIN pg_namespace n ON n.oid = t.typnamespace
|
|
1184
2095
|
WHERE n.nspname = $1
|
|
1185
2096
|
ORDER BY t.typname, e.enumsortorder
|
|
1186
|
-
`,[t]),s=new Map;for(let
|
|
1187
|
-
`);t.push(`export type ${
|
|
1188
|
-
${s};`),t.push("");}}t.push("// ============ TABLES ============"),t.push("");let e=[];for(let[i
|
|
2097
|
+
`,[t]),s=new Map;for(let c of o.rows)s.set(c.table_name,[]);for(let c of i.rows){let d=s.get(c.table_name);d&&d.push(c);}let n=new Map;for(let c of l.rows){let d=n.get(c.enum_name)||[];d.push(c.enum_value),n.set(c.enum_name,d);}let p=ee(s,n);return O__default.default.format(p,{parser:"typescript",semi:!0,singleQuote:!1,trailingComma:"es5",printWidth:100})}finally{await e.end();}}var A={smallint:"number",integer:"number",bigint:"string",int2:"number",int4:"number",int8:"string",decimal:"string",numeric:"string",real:"number",float4:"number",float8:"number","double precision":"number",money:"string",boolean:"boolean",bool:"boolean",text:"string",varchar:"string",char:"string",character:"string","character varying":"string",name:"string",citext:"string",date:"string",time:"string",timetz:"string",timestamp:"string",timestamptz:"string","timestamp without time zone":"string","timestamp with time zone":"string",interval:"string",bytea:"Buffer",uuid:"string",json:"unknown",jsonb:"unknown",inet:"string",cidr:"string",macaddr:"string",point:"{ x: number; y: number }",ARRAY:"unknown[]"};function Q(r,a){let{data_type:t,udt_name:e,is_nullable:o}=r;if(a.has(e)){let s=a.get(e).map(n=>`"${n}"`).join(" | ");return o==="YES"?`(${s}) | null`:s}if(t==="ARRAY"){let l=e.replace(/^_/,"");if(a.has(l)){let p=a.get(l).map(c=>`"${c}"`).join(" | ");return o==="YES"?`(${p})[] | null`:`(${p})[]`}let s=A[l]||"unknown";return o==="YES"?`${s}[] | null`:`${s}[]`}let i=A[t]||A[e]||"unknown";return o==="YES"&&(i=`${i} | null`),i}function C(r){return r.split(/[_\-\s]+/).map(a=>a.charAt(0).toUpperCase()+a.slice(1).toLowerCase()).join("")}function ee(r,a){let t=["/**"," * Auto-generated TypeScript types from database schema"," * Generated by @vaiftech/cli",` * Generated at: ${new Date().toISOString()}`," * "," * DO NOT EDIT MANUALLY - changes will be overwritten"," */",""];if(a.size>0){t.push("// ============ ENUMS ============"),t.push("");for(let[o,i]of a){let l=C(o),s=i.map(n=>` | "${n}"`).join(`
|
|
2098
|
+
`);t.push(`export type ${l} =
|
|
2099
|
+
${s};`),t.push("");}}t.push("// ============ TABLES ============"),t.push("");let e=[];for(let[o,i]of r){e.push(o);let l=C(o),s=[],n=[],p=[];for(let c of i){let d=Q(c,a),f=c.column_name,_=c.column_default!==null||c.is_identity==="YES",I=c.is_nullable==="YES";s.push(` ${f}: ${d};`),_||c.column_name==="id"?n.push(` ${f}?: ${d.replace(" | null","")} | null;`):I?n.push(` ${f}?: ${d};`):n.push(` ${f}: ${d.replace(" | null","")};`),p.push(` ${f}?: ${d.replace(" | null","")} | null;`);}t.push(`export interface ${l} {
|
|
1189
2100
|
${s.join(`
|
|
1190
2101
|
`)}
|
|
1191
|
-
}`),t.push(""),t.push(`export interface ${
|
|
1192
|
-
${
|
|
2102
|
+
}`),t.push(""),t.push(`export interface ${l}Insert {
|
|
2103
|
+
${n.join(`
|
|
1193
2104
|
`)}
|
|
1194
|
-
}`),t.push(""),t.push(`export interface ${
|
|
2105
|
+
}`),t.push(""),t.push(`export interface ${l}Update {
|
|
1195
2106
|
${p.join(`
|
|
1196
2107
|
`)}
|
|
1197
|
-
}`),t.push("");}t.push("// ============ DATABASE SCHEMA ============"),t.push(""),t.push("export interface Database {");for(let
|
|
1198
|
-
`)}exports.generateTypes=
|
|
2108
|
+
}`),t.push("");}t.push("// ============ DATABASE SCHEMA ============"),t.push(""),t.push("export interface Database {");for(let o of e){let i=C(o);t.push(` ${o}: {`),t.push(` Row: ${i};`),t.push(` Insert: ${i}Insert;`),t.push(` Update: ${i}Update;`),t.push(" };");}return t.push("}"),t.push(""),t.push("export type TableName = keyof Database;"),t.push(""),t.push("// ============ HELPER TYPES ============"),t.push(""),t.push('export type Row<T extends TableName> = Database[T]["Row"];'),t.push('export type Insert<T extends TableName> = Database[T]["Insert"];'),t.push('export type Update<T extends TableName> = Database[T]["Update"];'),t.push(""),t.join(`
|
|
2109
|
+
`)}exports.generateTypes=q;exports.generateTypesFromConnection=Fe;exports.initConfig=G;exports.loadConfig=x;
|