@vaiftech/cli 1.9.8 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -1
- package/dist/{chunk-T7GOLIY4.js → chunk-6Y62OF5M.js} +1397 -57
- package/dist/cli.cjs +888 -145
- package/dist/cli.js +80 -677
- package/dist/index.cjs +1399 -59
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/package.json +11 -11
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import
|
|
2
|
-
Could not connect to VAIF API. Please try again later.`)),process.exit(1));let
|
|
3
|
-
Could not connect to VAIF API.`)),console.log(
|
|
4
|
-
The authentication session expired. Please try again.`)),process.exit(1));continue}let
|
|
5
|
-
Timed out waiting for browser authentication.`)),console.log(
|
|
6
|
-
No email provided. Login cancelled.`)),process.exit(1));let
|
|
7
|
-
No password provided. Login cancelled.`)),process.exit(1));let
|
|
8
|
-
${i.message||"Invalid email or password."}`)),process.exit(1));let
|
|
9
|
-
Could not connect to VAIF API. Please try again later.`)),process.exit(1);}}async function
|
|
10
|
-
Your session has expired. Please login again.`)),process.exit(1)),
|
|
1
|
+
import v from'fs';import I from'path';import se from'dotenv';import le from'os';import {exec}from'child_process';import j from'ora';import u from'chalk';import H from'readline';import ve from'pg';import Ie from'prettier';se.config();async function N(n){let e=I.resolve(n);if(!v.existsSync(e))return null;try{let a=v.readFileSync(e,"utf-8"),t=JSON.parse(a);return t.database?.url&&(t.database.url=G(t.database.url)),t.api?.apiKey&&(t.api.apiKey=G(t.api.apiKey)),t}catch{throw new Error(`Failed to parse config file: ${n}`)}}function G(n){return n.replace(/\$\{([^}]+)\}/g,(e,a)=>process.env[a]||e)}var V=I.join(le.homedir(),".vaif"),S=I.join(V,"auth.json"),D=process.env.VAIF_API_URL||"https://api.vaif.studio";function de(){v.existsSync(V)||v.mkdirSync(V,{recursive:true});}function W(n){de(),v.writeFileSync(S,JSON.stringify(n,null,2),"utf-8"),v.chmodSync(S,384);}function R(){if(!v.existsSync(S))return null;try{let n=v.readFileSync(S,"utf-8");return JSON.parse(n)}catch{return null}}function ue(n){let e=H.createInterface({input:process.stdin,output:process.stdout});return new Promise(a=>{e.question(n,t=>{e.close(),a(t);});})}function pe(n){return new Promise(e=>{let a=H.createInterface({input:process.stdin,output:process.stdout});process.stdin;let o=process.stdout.write.bind(process.stdout),i=false;process.stdout.write=((...s)=>i?true:o(...s)),a.question(n,s=>{i=false,process.stdout.write=o,console.log(""),a.close(),e(s);}),i=true;})}function me(n){let e=process.platform,a;e==="darwin"?a=`open "${n}"`:e==="win32"?a=`start "" "${n}"`:a=`xdg-open "${n}"`,exec(a,t=>{});}function fe(n){return new Promise(e=>setTimeout(e,n))}async function ge(n){try{let e=await fetch(`${D}/auth/me`,{headers:{Authorization:`Bearer ${n}`}});if(e.ok){let a=await e.json();return {valid:!0,email:a.user?.email||a.email}}return {valid:!1}}catch{return {valid:false}}}async function he(n){let e=j();e.start("Setting up authentication...");let a,t;try{let l=await fetch(`${D}/auth/cli/authorize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({})});l.ok||(e.fail("Failed to initiate authentication"),console.log(u.red(`
|
|
2
|
+
Could not connect to VAIF API. Please try again later.`)),process.exit(1));let r=await l.json();a=r.code,t=r.url;}catch{e.fail("Failed to connect to VAIF API"),console.log(u.red(`
|
|
3
|
+
Could not connect to VAIF API.`)),console.log(u.gray("Check your internet connection or try: vaif login --email")),process.exit(1);}e.stop(),console.log(u.cyan(" Opening browser for authentication...")),console.log(""),console.log(u.gray(" If the browser doesn't open, visit this URL:")),console.log(u.white(` ${t}`)),console.log(""),me(t),e.start("Waiting for browser authentication...");let o=12e4,i=2e3,s=Date.now();for(;Date.now()-s<o;){await fe(i);try{let l=await fetch(`${D}/auth/cli/token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:a})});if(!l.ok){let c=await l.json();(c.error==="ExpiredCode"||c.error==="InvalidCode")&&(e.fail("Authentication expired"),console.log(u.red(`
|
|
4
|
+
The authentication session expired. Please try again.`)),process.exit(1));continue}let r=await l.json();if(r.ok&&r.accessToken){let c={token:r.accessToken,email:r.user?.email,projectId:n,expiresAt:new Date(Date.now()+r.expiresIn*1e3).toISOString()};W(c),e.succeed("Logged in successfully"),console.log(""),r.user?.email&&console.log(u.green(` Authenticated as: ${r.user.email}`)),console.log(u.gray(` Config saved to: ${S}`)),console.log("");return}}catch{}}e.fail("Authentication timed out"),console.log(u.red(`
|
|
5
|
+
Timed out waiting for browser authentication.`)),console.log(u.gray("Try again or use: vaif login --email")),process.exit(1);}async function ye(n){console.log(""),console.log(u.bold("VAIF CLI Login")),console.log(u.gray("Enter your VAIF account credentials")),console.log("");let e=await ue(u.cyan(" Email: "));(!e||e.trim()==="")&&(console.log(u.red(`
|
|
6
|
+
No email provided. Login cancelled.`)),process.exit(1));let a=await pe(u.cyan(" Password: "));(!a||a.trim()==="")&&(console.log(u.red(`
|
|
7
|
+
No password provided. Login cancelled.`)),process.exit(1));let t=j("Authenticating...").start();try{let o=await fetch(`${D}/auth/cli/login`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:e.trim(),password:a})}),i=await o.json();(!o.ok||!i.ok)&&(t.fail("Login failed"),console.log(u.red(`
|
|
8
|
+
${i.message||"Invalid email or password."}`)),process.exit(1));let s={token:i.accessToken,email:i.user?.email,projectId:n,expiresAt:new Date(Date.now()+i.expiresIn*1e3).toISOString()};W(s),t.succeed("Logged in successfully"),console.log(""),i.user?.email&&console.log(u.green(` Authenticated as: ${i.user.email}`)),console.log(u.gray(` Config saved to: ${S}`)),console.log("");}catch{t.fail("Failed to connect to VAIF API"),console.log(u.red(`
|
|
9
|
+
Could not connect to VAIF API. Please try again later.`)),process.exit(1);}}async function Qe(n){console.log(""),console.log(u.bold("Welcome to VAIF CLI")),console.log(u.gray("Authenticate to access your VAIF projects")),console.log(""),n.email?await ye(n.projectId):await he(n.projectId),console.log(u.gray("You can now use VAIF CLI commands like:")),console.log(u.gray(" vaif pull - Pull remote schema")),console.log(u.gray(" vaif push - Push schema changes")),console.log(u.gray(" vaif generate - Generate TypeScript types")),console.log("");}async function Ze(){v.existsSync(S)?(v.unlinkSync(S),console.log(u.green("Logged out successfully"))):console.log(u.yellow("Not currently logged in"));}async function et(){let n=R();(!n||!n.token)&&(console.log(u.yellow("Not logged in")),console.log(u.gray("Run `vaif login` to authenticate")),process.exit(1));let e=j("Checking authentication...").start(),{valid:a,email:t}=await ge(n.token);a||(e.fail("Session expired"),console.log(u.yellow(`
|
|
10
|
+
Your session has expired. Please login again.`)),process.exit(1)),e.succeed("Authenticated"),console.log(""),console.log(u.green(` Email: ${t||n.email||"Unknown"}`)),n.projectId&&console.log(u.green(` Project: ${n.projectId}`)),console.log("");}var _e=process.env.VAIF_API_URL||"https://api.vaif.studio";async function Ee(n,e){let a=await fetch(`${_e}/schema-engine/introspect/${e}`,{headers:{Authorization:`Bearer ${n}`,"Content-Type":"application/json"}});if(!a.ok){let l=await a.text();throw new Error(`API introspection failed: ${l}`)}let t=await a.json();if(!t.ok||!t.schemaExists)throw new Error("Project schema does not exist yet. Push a migration first with `vaif db push`.");let o=new Map,i=[];for(let l of t.tables){let r=l.columns.map(c=>({column_name:c.name,data_type:c.type,is_nullable:c.nullable?"YES":"NO",column_default:c.default,udt_name:c.type,is_identity:c.primaryKey&&c.default?.includes("gen_random_uuid")?"YES":"NO",character_maximum_length:null,numeric_precision:null,numeric_scale:null}));o.set(l.name,r);for(let c of l.foreignKeys)i.push({constraint_name:c.constraintName,table_name:l.name,column_name:c.columnName,foreign_table_name:c.refTable,foreign_column_name:c.refColumn});}return {tables:o,enums:new Map,foreignKeys:i}}async function Ae(n,e){let a=await n.query(`
|
|
11
11
|
SELECT table_name, table_type
|
|
12
12
|
FROM information_schema.tables
|
|
13
13
|
WHERE table_schema = $1
|
|
14
14
|
AND table_type = 'BASE TABLE'
|
|
15
15
|
ORDER BY table_name
|
|
16
|
-
`,[
|
|
16
|
+
`,[e]),t=await n.query(`
|
|
17
17
|
SELECT
|
|
18
18
|
table_name,
|
|
19
19
|
column_name,
|
|
@@ -28,7 +28,7 @@ Your session has expired. Please login again.`)),process.exit(1)),t.succeed("Aut
|
|
|
28
28
|
FROM information_schema.columns
|
|
29
29
|
WHERE table_schema = $1
|
|
30
30
|
ORDER BY table_name, ordinal_position
|
|
31
|
-
`,[
|
|
31
|
+
`,[e]),o=await n.query(`
|
|
32
32
|
SELECT
|
|
33
33
|
tc.constraint_name,
|
|
34
34
|
tc.table_name,
|
|
@@ -44,7 +44,7 @@ Your session has expired. Please login again.`)),process.exit(1)),t.succeed("Aut
|
|
|
44
44
|
AND ccu.table_schema = tc.table_schema
|
|
45
45
|
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
46
46
|
AND tc.table_schema = $1
|
|
47
|
-
`,[
|
|
47
|
+
`,[e]),i=await n.query(`
|
|
48
48
|
SELECT
|
|
49
49
|
t.typname as enum_name,
|
|
50
50
|
e.enumlabel as enum_value
|
|
@@ -53,24 +53,24 @@ Your session has expired. Please login again.`)),process.exit(1)),t.succeed("Aut
|
|
|
53
53
|
JOIN pg_namespace n ON n.oid = t.typnamespace
|
|
54
54
|
WHERE n.nspname = $1
|
|
55
55
|
ORDER BY t.typname, e.enumsortorder
|
|
56
|
-
`,[
|
|
57
|
-
`);return `export type ${
|
|
58
|
-
${
|
|
59
|
-
${
|
|
56
|
+
`,[e]),s=new Map;for(let r of a.rows)s.set(r.table_name,[]);for(let r of t.rows){let c=s.get(r.table_name);c&&c.push(r);}let l=new Map;for(let r of i.rows){let c=l.get(r.enum_name)||[];c.push(r.enum_value),l.set(r.enum_name,c);}return {tables:s,enums:l,foreignKeys:o.rows}}var M={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 Te(n,e){let{data_type:a,udt_name:t,is_nullable:o}=n;if(e.has(t)){let l=e.get(t).map(r=>`"${r}"`).join(" | ");return o==="YES"?`(${l}) | null`:l}if(a==="ARRAY"){let s=t.replace(/^_/,"");if(e.has(s)){let c=e.get(s).map(h=>`"${h}"`).join(" | ");return o==="YES"?`(${c})[] | null`:`(${c})[]`}let l=M[s]||"unknown";return o==="YES"?`${l}[] | null`:`${l}[]`}let i=M[a]||M[t]||"unknown";return o==="YES"&&(i=`${i} | null`),i}function K(n){return n.split(/[_\-\s]+/).map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join("")}function Se(n,e){let a=K(n),t=e.map(o=>` | "${o}"`).join(`
|
|
57
|
+
`);return `export type ${a} =
|
|
58
|
+
${t};`}function we(n,e,a){let t=K(n),o=[],i=[],s=[];for(let h of e){let m=Te(h,a),d=h.column_name,f=h.column_default!==null||h.is_identity==="YES",A=h.is_nullable==="YES";o.push(` ${d}: ${m};`),f||h.column_name==="id"?i.push(` ${d}?: ${m.replace(" | null","")} | null;`):A?i.push(` ${d}?: ${m};`):i.push(` ${d}: ${m.replace(" | null","")};`),s.push(` ${d}?: ${m.replace(" | null","")} | null;`);}let l=`export interface ${t} {
|
|
59
|
+
${o.join(`
|
|
60
60
|
`)}
|
|
61
|
-
}`,
|
|
61
|
+
}`,r=`export interface ${t}Insert {
|
|
62
62
|
${i.join(`
|
|
63
63
|
`)}
|
|
64
|
-
}`,
|
|
65
|
-
${
|
|
64
|
+
}`,c=`export interface ${t}Update {
|
|
65
|
+
${s.join(`
|
|
66
66
|
`)}
|
|
67
|
-
}`;return {base:
|
|
68
|
-
`)}async function
|
|
69
|
-
Either:`)),console.log(
|
|
70
|
-
Set projectId in vaif.config.json or use VAIF_PROJECT_ID env var.`)),process.exit(1)),
|
|
71
|
-
Push a migration first: vaif db push`));return}
|
|
72
|
-
Error: ${
|
|
73
|
-
Make sure your database is running and accessible.`))),process.exit(1);}}var
|
|
67
|
+
}`;return {base:l,insert:r,update:c}}function xe(n,e,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(e.size>0){t.push("// ============ ENUMS ============"),t.push("");for(let[i,s]of e)t.push(Se(i,s)),t.push("");}t.push("// ============ TABLES ============"),t.push("");let o=[];for(let[i,s]of n){let{base:l,insert:r,update:c}=we(i,s,e);o.push(i),t.push(l),t.push(""),t.push(r),t.push(""),t.push(c),t.push("");}t.push("// ============ DATABASE SCHEMA ============"),t.push(""),t.push("export interface Database {");for(let i of o){let s=K(i);t.push(` ${i}: {`),t.push(` Row: ${s};`),t.push(` Insert: ${s}Insert;`),t.push(` Update: ${s}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(`
|
|
68
|
+
`)}async function ut(n){let e=j("Loading configuration...").start();try{let a=await N(n.config),t=n.connection||a?.database?.url||process.env.DATABASE_URL,o=t&&!t.includes("${"),i,s,l;if(o){e.text="Connecting to database...";let d=new ve.Client({connectionString:t});await d.connect(),e.text="Introspecting schema...",{tables:i,enums:s,foreignKeys:l}=await Ae(d,n.schema),await d.end();}else {let d=R();(!d||!d.token)&&(e.fail("No database connection and not logged in"),console.log(u.yellow(`
|
|
69
|
+
Either:`)),console.log(u.gray(" 1. Run `vaif login` to authenticate (no DATABASE_URL needed)")),console.log(u.gray(" 2. Set DATABASE_URL in your .env file")),console.log(u.gray(" 3. Pass --connection postgresql://user:pass@host:5432/db")),process.exit(1));let f=a?.projectId||process.env.VAIF_PROJECT_ID||d.projectId;f||(e.fail("No project ID specified"),console.log(u.yellow(`
|
|
70
|
+
Set projectId in vaif.config.json or use VAIF_PROJECT_ID env var.`)),process.exit(1)),e.text="Introspecting schema via API...",{tables:i,enums:s,foreignKeys:l}=await Ee(d.token,f);}if(i.size===0){e.warn("No tables found"),console.log(u.yellow(`
|
|
71
|
+
Push a migration first: vaif db push`));return}e.text=`Generating types for ${i.size} tables...`;let r=xe(i,s,l),c=await Ie.format(r,{parser:"typescript",semi:!0,singleQuote:!1,trailingComma:"es5",printWidth:100});if(n.dryRun){e.succeed("Generated types (dry run):"),console.log(""),console.log(u.gray("\u2500".repeat(60))),console.log(c),console.log(u.gray("\u2500".repeat(60)));return}let h=I.resolve(n.output),m=I.dirname(h);v.existsSync(m)||v.mkdirSync(m,{recursive:!0}),v.writeFileSync(h,c,"utf-8"),e.succeed(`Generated types for ${i.size} tables \u2192 ${u.cyan(n.output)}`),console.log(""),console.log(u.green("Generated:")),console.log(u.gray(` Tables: ${i.size}`)),console.log(u.gray(` Enums: ${s.size}`)),console.log(""),console.log(u.gray("Import in your code:")),console.log(u.cyan(` import type { Database, Row, Insert, Update } from "${n.output.replace(/\.ts$/,"")}";`));}catch(a){e.fail("Failed to generate types"),a instanceof Error&&(console.error(u.red(`
|
|
72
|
+
Error: ${a.message}`)),a.message.includes("ECONNREFUSED")&&console.log(u.yellow(`
|
|
73
|
+
Make sure your database is running and accessible.`))),process.exit(1);}}var C=[{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"}],ee={"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:`{
|
|
74
74
|
"name": "my-vaif-app",
|
|
75
75
|
"private": true,
|
|
76
76
|
"version": "0.1.0",
|
|
@@ -180,7 +180,7 @@ export function createVaifServer() {
|
|
|
180
180
|
});
|
|
181
181
|
}
|
|
182
182
|
`},{path:".env.local.example",content:`# VAIF Configuration
|
|
183
|
-
# Get these values from https://vaif.studio/
|
|
183
|
+
# Get these values from https://console.vaif.studio/security/api-keys \u2192 Project Settings \u2192 API Keys
|
|
184
184
|
|
|
185
185
|
NEXT_PUBLIC_VAIF_API_URL=https://api.vaif.studio
|
|
186
186
|
NEXT_PUBLIC_VAIF_PROJECT_ID=your-project-id
|
|
@@ -221,7 +221,7 @@ A full-stack Next.js application powered by [VAIF Studio](https://vaif.studio),
|
|
|
221
221
|
cp .env.local.example .env.local
|
|
222
222
|
\\\`\\\`\\\`
|
|
223
223
|
|
|
224
|
-
Get your Project ID, API Key, and Secret Key from <https://vaif.studio/
|
|
224
|
+
Get your Project ID, API Key, and Secret Key from <https://console.vaif.studio/security/api-keys> under **Project Settings > API Keys**.
|
|
225
225
|
|
|
226
226
|
3. **Install and log in to the VAIF CLI**
|
|
227
227
|
|
|
@@ -695,7 +695,7 @@ interface ImportMeta {
|
|
|
695
695
|
readonly env: ImportMetaEnv;
|
|
696
696
|
}
|
|
697
697
|
`},{path:".env.example",content:`# VAIF Configuration
|
|
698
|
-
# Get these values from https://vaif.studio/
|
|
698
|
+
# Get these values from https://console.vaif.studio/security/api-keys
|
|
699
699
|
|
|
700
700
|
VITE_VAIF_API_URL=https://api.vaif.studio
|
|
701
701
|
VITE_VAIF_PROJECT_ID=your-project-id
|
|
@@ -729,7 +729,7 @@ A single-page React application built with [Vite](https://vite.dev/) and powered
|
|
|
729
729
|
cp .env.example .env
|
|
730
730
|
\\\`\\\`\\\`
|
|
731
731
|
|
|
732
|
-
Get your Project ID and API Key from <https://vaif.studio/
|
|
732
|
+
Get your Project ID and API Key from <https://console.vaif.studio/security/api-keys> under **Project Settings > API Keys**.
|
|
733
733
|
|
|
734
734
|
3. **Install and log in to the VAIF CLI**
|
|
735
735
|
|
|
@@ -1252,7 +1252,7 @@ An iOS/macOS application powered by [VAIF Studio](https://vaif.studio), using th
|
|
|
1252
1252
|
<string>your-anon-key</string>
|
|
1253
1253
|
\\\`\\\`\\\`
|
|
1254
1254
|
|
|
1255
|
-
Get your credentials from <https://vaif.studio/
|
|
1255
|
+
Get your credentials from <https://console.vaif.studio/security/api-keys> under **Project Settings > API Keys**.
|
|
1256
1256
|
|
|
1257
1257
|
3. **Install and log in to the VAIF CLI** (for type generation)
|
|
1258
1258
|
|
|
@@ -1381,7 +1381,7 @@ export const vaif = createExpoClient({
|
|
|
1381
1381
|
realtime: { enabled: true },
|
|
1382
1382
|
});
|
|
1383
1383
|
`},{path:".env.example",content:`# VAIF Configuration
|
|
1384
|
-
# Get these values from https://vaif.studio/
|
|
1384
|
+
# Get these values from https://console.vaif.studio/security/api-keys \u2192 Project Settings \u2192 API Keys
|
|
1385
1385
|
|
|
1386
1386
|
EXPO_PUBLIC_VAIF_PROJECT_ID=your-project-id
|
|
1387
1387
|
EXPO_PUBLIC_VAIF_API_KEY=your-anon-key
|
|
@@ -1418,7 +1418,7 @@ A React Native / Expo mobile application powered by [VAIF Studio](https://vaif.s
|
|
|
1418
1418
|
cp .env.example .env
|
|
1419
1419
|
\\\`\\\`\\\`
|
|
1420
1420
|
|
|
1421
|
-
Get your Project ID and API Key from <https://vaif.studio/
|
|
1421
|
+
Get your Project ID and API Key from <https://console.vaif.studio/security/api-keys> under **Project Settings > API Keys**.
|
|
1422
1422
|
|
|
1423
1423
|
3. **Install and log in to the VAIF CLI**
|
|
1424
1424
|
|
|
@@ -1853,7 +1853,7 @@ A Flutter application powered by [VAIF Studio](https://vaif.studio), with Dart c
|
|
|
1853
1853
|
cp .env.example .env
|
|
1854
1854
|
\\\`\\\`\\\`
|
|
1855
1855
|
|
|
1856
|
-
Get your Project ID and API Key from <https://vaif.studio/
|
|
1856
|
+
Get your Project ID and API Key from <https://console.vaif.studio/security/api-keys> under **Project Settings > API Keys**.
|
|
1857
1857
|
|
|
1858
1858
|
3. **Install and log in to the VAIF CLI** (for schema and type generation)
|
|
1859
1859
|
|
|
@@ -2111,7 +2111,7 @@ fastapi>=0.110.0
|
|
|
2111
2111
|
uvicorn[standard]>=0.27.0
|
|
2112
2112
|
python-dotenv>=1.0.0
|
|
2113
2113
|
`},{path:".env.example",content:`# VAIF Configuration
|
|
2114
|
-
# Get these values from https://vaif.studio/
|
|
2114
|
+
# Get these values from https://console.vaif.studio/security/api-keys \u2192 Project Settings \u2192 API Keys
|
|
2115
2115
|
|
|
2116
2116
|
VAIF_PROJECT_ID=your-project-id
|
|
2117
2117
|
VAIF_API_KEY=your-anon-key
|
|
@@ -2153,7 +2153,7 @@ A FastAPI backend application powered by [VAIF Studio](https://vaif.studio), wit
|
|
|
2153
2153
|
cp .env.example .env
|
|
2154
2154
|
\\\`\\\`\\\`
|
|
2155
2155
|
|
|
2156
|
-
Get your Project ID, API Key, and Secret Key from <https://vaif.studio/
|
|
2156
|
+
Get your Project ID, API Key, and Secret Key from <https://console.vaif.studio/security/api-keys> under **Project Settings > API Keys**.
|
|
2157
2157
|
|
|
2158
2158
|
4. **Install and log in to the VAIF CLI**
|
|
2159
2159
|
|
|
@@ -2476,7 +2476,7 @@ A Go backend API powered by [VAIF Studio](https://vaif.studio), with HTTP handle
|
|
|
2476
2476
|
cp .env.example .env
|
|
2477
2477
|
\\\`\\\`\\\`
|
|
2478
2478
|
|
|
2479
|
-
Get your Project ID, API Key, and Secret Key from <https://vaif.studio/
|
|
2479
|
+
Get your Project ID, API Key, and Secret Key from <https://console.vaif.studio/security/api-keys> under **Project Settings > API Keys**.
|
|
2480
2480
|
|
|
2481
2481
|
3. **Install and log in to the VAIF CLI**
|
|
2482
2482
|
|
|
@@ -2751,7 +2751,7 @@ export async function deleteTodo(id: string): Promise<void> {
|
|
|
2751
2751
|
if (error) throw error;
|
|
2752
2752
|
}
|
|
2753
2753
|
`},{path:".env.example",content:`# VAIF Configuration
|
|
2754
|
-
# Get these values from https://vaif.studio/
|
|
2754
|
+
# Get these values from https://console.vaif.studio/security/api-keys
|
|
2755
2755
|
|
|
2756
2756
|
VITE_VAIF_API_URL=https://api.vaif.studio
|
|
2757
2757
|
VITE_VAIF_PROJECT_ID=your-project-id
|
|
@@ -2780,7 +2780,7 @@ A simple React todo application for learning [VAIF Studio](https://vaif.studio)
|
|
|
2780
2780
|
cp .env.example .env
|
|
2781
2781
|
\\\`\\\`\\\`
|
|
2782
2782
|
|
|
2783
|
-
Get your Project ID and API Key from <https://vaif.studio/
|
|
2783
|
+
Get your Project ID and API Key from <https://console.vaif.studio/security/api-keys> under **Project Settings > API Keys**.
|
|
2784
2784
|
|
|
2785
2785
|
3. **Install and log in to the VAIF CLI**
|
|
2786
2786
|
|
|
@@ -3031,7 +3031,7 @@ export function useRealtimeMessages({
|
|
|
3031
3031
|
return { messages, isLoading, error, refresh };
|
|
3032
3032
|
}
|
|
3033
3033
|
`},{path:".env.example",content:`# VAIF Configuration
|
|
3034
|
-
# Get these values from https://vaif.studio/
|
|
3034
|
+
# Get these values from https://console.vaif.studio/security/api-keys
|
|
3035
3035
|
|
|
3036
3036
|
VITE_VAIF_API_URL=https://api.vaif.studio
|
|
3037
3037
|
VITE_VAIF_PROJECT_ID=your-project-id
|
|
@@ -3060,7 +3060,7 @@ A React chat application with live messaging powered by [VAIF Studio](https://va
|
|
|
3060
3060
|
cp .env.example .env
|
|
3061
3061
|
\\\`\\\`\\\`
|
|
3062
3062
|
|
|
3063
|
-
Get your Project ID and API Key from <https://vaif.studio/
|
|
3063
|
+
Get your Project ID and API Key from <https://console.vaif.studio/security/api-keys> under **Project Settings > API Keys**.
|
|
3064
3064
|
|
|
3065
3065
|
3. **Install and log in to the VAIF CLI**
|
|
3066
3066
|
|
|
@@ -3347,7 +3347,7 @@ export async function requireTeamRole(
|
|
|
3347
3347
|
return member as TeamMember;
|
|
3348
3348
|
}
|
|
3349
3349
|
`},{path:".env.example",content:`# VAIF Configuration
|
|
3350
|
-
# Get these values from https://vaif.studio/
|
|
3350
|
+
# Get these values from https://console.vaif.studio/security/api-keys \u2192 Project Settings \u2192 API Keys
|
|
3351
3351
|
|
|
3352
3352
|
NEXT_PUBLIC_VAIF_API_URL=https://api.vaif.studio
|
|
3353
3353
|
NEXT_PUBLIC_VAIF_PROJECT_ID=your-project-id
|
|
@@ -3380,7 +3380,7 @@ A full SaaS starter kit powered by [VAIF Studio](https://vaif.studio) with authe
|
|
|
3380
3380
|
cp .env.example .env.local
|
|
3381
3381
|
\\\`\\\`\\\`
|
|
3382
3382
|
|
|
3383
|
-
Get your Project ID, API Key, and Secret Key from <https://vaif.studio/
|
|
3383
|
+
Get your Project ID, API Key, and Secret Key from <https://console.vaif.studio/security/api-keys> under **Project Settings > API Keys**.
|
|
3384
3384
|
|
|
3385
3385
|
3. **Install and log in to the VAIF CLI**
|
|
3386
3386
|
|
|
@@ -3625,7 +3625,7 @@ function getContentType(fileName: string): string {
|
|
|
3625
3625
|
return mimeTypes[ext ?? ""] ?? "application/octet-stream";
|
|
3626
3626
|
}
|
|
3627
3627
|
`},{path:".env.example",content:`# VAIF Configuration
|
|
3628
|
-
# Get these values from https://vaif.studio/
|
|
3628
|
+
# Get these values from https://console.vaif.studio/security/api-keys \u2192 Project Settings \u2192 API Keys
|
|
3629
3629
|
|
|
3630
3630
|
NEXT_PUBLIC_VAIF_API_URL=https://api.vaif.studio
|
|
3631
3631
|
NEXT_PUBLIC_VAIF_PROJECT_ID=your-project-id
|
|
@@ -3658,7 +3658,7 @@ An API-first e-commerce setup powered by [VAIF Studio](https://vaif.studio) with
|
|
|
3658
3658
|
cp .env.example .env.local
|
|
3659
3659
|
\\\`\\\`\\\`
|
|
3660
3660
|
|
|
3661
|
-
Get your Project ID, API Key, and Secret Key from <https://vaif.studio/
|
|
3661
|
+
Get your Project ID, API Key, and Secret Key from <https://console.vaif.studio/security/api-keys> under **Project Settings > API Keys**.
|
|
3662
3662
|
|
|
3663
3663
|
3. **Install and log in to the VAIF CLI**
|
|
3664
3664
|
|
|
@@ -3774,16 +3774,1356 @@ export const posts = pgTable("posts", {
|
|
|
3774
3774
|
|
|
3775
3775
|
return Response.json({ message: \`Hello, \${name}!\` });
|
|
3776
3776
|
}
|
|
3777
|
-
`}]},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"]}};function
|
|
3778
|
-
? Which VAIF features do you want to include?`)),
|
|
3779
|
-
Unknown template: ${
|
|
3780
|
-
`)),process.exit(1));let e
|
|
3781
|
-
No features specified.`)),console.log(
|
|
3782
|
-
`,"utf-8"),console.log(
|
|
3783
|
-
`,"utf-8");}catch{}(
|
|
3784
|
-
|
|
3785
|
-
|
|
3777
|
+
`}]},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"]}};function vt(){console.log(""),console.log(u.bold("Available project templates")),console.log("");let n=26,e=22;console.log(` ${u.gray("Template".padEnd(n))}${u.gray("Stack".padEnd(e))}${u.gray("Description")}`),console.log(u.gray(" "+"-".repeat(n+e+40)));for(let[a,t]of Object.entries(ee))console.log(` ${u.cyan(a.padEnd(n))}${u.yellow(t.tag.padEnd(e))}${u.white(t.description)}`);console.log(""),console.log(u.gray("Usage:")),console.log(u.gray(" npx @vaiftech/cli init --template <name>")),console.log(u.gray(" npx @vaiftech/cli init -t nextjs-fullstack")),console.log(u.gray(" npx @vaiftech/cli init -t react-spa --features auth,database,realtime")),console.log(""),console.log(u.gray("Available features: auth, database, realtime, storage, functions")),console.log("");}async function Pe(n){if(!process.stdin.isTTY||!process.stdout.isTTY)return n;let e=new Set(n.map(t=>C.findIndex(o=>o.name===t)).filter(t=>t>=0)),a=0;return new Promise(t=>{let o=H.createInterface({input:process.stdin,output:process.stdout});H.emitKeypressEvents(process.stdin,o),process.stdin.setRawMode&&process.stdin.setRawMode(true);function i(){let l=C.length+2;process.stdout.write(`\x1B[${l}A`),s();}function s(){console.log(u.bold(`
|
|
3778
|
+
? Which VAIF features do you want to include?`)),C.forEach((l,r)=>{let c=e.has(r)?u.green("[x]"):"[ ]",h=r===a?u.cyan("> "):" ";console.log(`${h}${c} ${l.label} ${u.gray(`(${l.description})`)}`);}),console.log(u.gray(" (up/down to move, space to toggle, enter to confirm)"));}s(),process.stdin.on("keypress",(l,r)=>{if(r.name==="up"&&a>0)a--,i();else if(r.name==="down"&&a<C.length-1)a++,i();else if(r.name==="space")e.has(a)?e.delete(a):e.add(a),i();else if(r.name==="return"){process.stdin.setRawMode&&process.stdin.setRawMode(false),o.close();let c=[...e].sort().map(h=>C[h].name);t(c.length>0?c:n);}else r.name==="c"&&r.ctrl&&(process.stdin.setRawMode&&process.stdin.setRawMode(false),o.close(),process.exit(0));});})}async function q(n,e={}){let a=ee[n];a||(console.log(u.red(`
|
|
3779
|
+
Unknown template: ${n}`)),console.log(u.yellow(`Run 'vaif templates' to see available templates.
|
|
3780
|
+
`)),process.exit(1));let t;e.features&&e.features.length>0?t=e.features.filter(d=>C.some(f=>f.name===d)):e.addOnly?(console.log(u.red(`
|
|
3781
|
+
No features specified.`)),console.log(u.yellow("Usage: vaif init --template <name> --add-features <features>")),console.log(u.gray("Available features: auth, database, realtime, storage, functions")),process.exit(1)):a.featureFiles&&Object.keys(a.featureFiles).length>0?t=await Pe(a.defaultFeatures??["database","auth"]):t=a.defaultFeatures??[],e.addOnly?(console.log(""),console.log(u.bold(`Adding features to ${u.cyan(a.name)} project...`)),console.log(u.gray(` Features: ${t.join(", ")}`)),console.log("")):(console.log(""),console.log(u.bold(`Scaffolding ${u.cyan(a.name)} template...`)),t.length>0&&console.log(u.gray(` Features: ${t.join(", ")}`)),console.log(""));let o=e.addOnly?[]:[...a.files],i=new Set,s=[];if(a.featureFiles)for(let d of t){let f=a.featureFiles[d];if(f)for(let A of f)i.add(A.path),s.push(A);}let l=o.filter(d=>!i.has(d.path)).concat(s),r=0,c=0;for(let d of l){let f=I.resolve(d.path),A=I.dirname(f);if(v.existsSync(A)||v.mkdirSync(A,{recursive:true}),d.path==="package.json"&&v.existsSync(f)&&!e.force)try{let _=JSON.parse(v.readFileSync(f,"utf-8")),w=JSON.parse(d.content),U=z=>{if(!z)return {};let Y={};for(let[oe,L]of Object.entries(z))!L.startsWith("workspace:")&&!L.startsWith("link:")&&!L.startsWith("file:")&&(Y[oe]=L);return Y};_.dependencies={...U(_.dependencies),...w.dependencies||{}},_.devDependencies={...U(_.devDependencies),...w.devDependencies||{}},w.scripts&&(_.scripts={..._.scripts||{},...w.scripts}),v.writeFileSync(f,JSON.stringify(_,null,2)+`
|
|
3782
|
+
`,"utf-8"),console.log(u.green(` merge ${d.path} (added dependencies)`)),r++;continue}catch{}if(v.existsSync(f)&&!e.force){console.log(u.yellow(` skip ${d.path} (already exists)`)),c++;continue}v.writeFileSync(f,d.content,"utf-8"),console.log(u.green(` create ${d.path}`)),r++;}console.log(""),r>0&&console.log(u.green(`Created ${r} file${r!==1?"s":""}.`)),c>0&&console.log(u.yellow(`Skipped ${c} file${c!==1?"s":""} (use --force to overwrite).`));let h={auth:{"@vaiftech/auth":"^1.0.0"},database:{},realtime:{},storage:{},functions:{}},m=I.resolve("package.json");if(v.existsSync(m)&&t.length>0)try{let d=JSON.parse(v.readFileSync(m,"utf-8")),f=!1;for(let A of t){let _=h[A];if(_)for(let[w,U]of Object.entries(_))d.dependencies?.[w]||(d.dependencies=d.dependencies||{},d.dependencies[w]=U,f=!0);}f&&v.writeFileSync(m,JSON.stringify(d,null,2)+`
|
|
3783
|
+
`,"utf-8");}catch{}(a.dependencies?.length||a.devDependencies?.length)&&(console.log(""),console.log(u.bold("Install dependencies:")),a.dependencies?.length&&console.log(u.cyan(` npm install ${a.dependencies.join(" ")}`)),a.devDependencies?.length&&console.log(u.cyan(` npm install -D ${a.devDependencies.join(" ")}`))),console.log(""),console.log(u.bold.green("Project scaffolded successfully!")),console.log(""),console.log(u.bold(" Next steps:")),a.postInstructions.forEach(d=>{console.log(u.gray(` ${d}`));}),console.log(""),console.log(u.gray(" Get your project credentials at https://console.vaif.studio/security/api-keys")),console.log("");}var F=process.env.VAIF_API_URL||"https://api.vaif.studio";function Fe(n){let e=H.createInterface({input:process.stdin,output:process.stdout});return new Promise(a=>{e.question(n,t=>{e.close(),a(t.trim());});})}function Re(n,e,a){return n.projectId||e?.projectId||process.env.VAIF_PROJECT_ID||a.projectId||null}async function Ue(n){let e=j("Fetching your projects...").start();try{let a=await fetch(`${F}/projects`,{headers:{Authorization:`Bearer ${n}`}});if(!a.ok)return e.fail("Could not fetch projects"),null;let t=await a.json();if(!t||t.length===0)return e.fail("No projects found. Create a project at https://vaif.studio first."),null;if(t.length===1)return e.succeed(`Found project: ${u.cyan(t[0].name)} (${t[0].id})`),t[0].id;e.succeed(`Found ${t.length} projects
|
|
3784
|
+
`);for(let s=0;s<t.length;s++)console.log(u.gray(` ${s+1}.`)+` ${u.white(t[s].name)} ${u.gray(`(${t[s].id})`)}`);console.log("");let o=await Fe(u.cyan(` Select a project [1-${t.length}]: `)),i=parseInt(o,10)-1;return isNaN(i)||i<0||i>=t.length?(console.log(u.red(`
|
|
3785
|
+
Invalid selection.`)),null):t[i].id}catch{return e.fail("Could not fetch projects"),null}}function Le(n){if(!n||n.length===0)return "*No tables found. Create tables in the VAIF Studio dashboard.*";let e="";for(let a of n){e+=`### \`${a.name}\`
|
|
3786
|
+
|
|
3787
|
+
`,e+=`| Column | Type | Nullable | Default | Constraints |
|
|
3788
|
+
`,e+=`|--------|------|----------|---------|-------------|
|
|
3789
|
+
`;for(let t of a.columns){let o=[];t.isPrimaryKey&&o.push("PK"),t.nullable||o.push("NOT NULL");let i=a.foreignKeys?.find(s=>s.column===t.name);i&&o.push(`FK \u2192 ${i.foreignTable}.${i.foreignColumn}`),e+=`| ${t.name} | ${t.type} | ${t.nullable?"yes":"no"} | ${t.defaultValue||"-"} | ${o.join(", ")||"-"} |
|
|
3790
|
+
`;}e+=`
|
|
3791
|
+
`;}return e}function Ne(n,e,a){let t=n.slice(0,3);if(t.length===0)return "";let o="";for(let i of t){let s=i.columns.filter(r=>!r.isPrimaryKey&&r.name!=="created_at"&&r.name!=="updated_at"),l=s.slice(0,3).map(r=>` ${r.name}: ${De(r)}`).join(`,
|
|
3792
|
+
`);o+=`### \`${i.name}\`
|
|
3793
|
+
|
|
3794
|
+
`,o+="```typescript\n",o+=`// Select all
|
|
3795
|
+
const ${i.name} = await vaif.from("${i.name}").select();
|
|
3796
|
+
|
|
3797
|
+
`,o+=`// Select with filter
|
|
3798
|
+
const filtered = await vaif.from("${i.name}").select().eq("${s[0]?.name||"id"}", value);
|
|
3799
|
+
|
|
3800
|
+
`,o+=`// Insert
|
|
3801
|
+
const created = await vaif.from("${i.name}").insert({
|
|
3802
|
+
${l}
|
|
3803
|
+
});
|
|
3804
|
+
|
|
3805
|
+
`,o+=`// Update
|
|
3806
|
+
await vaif.from("${i.name}").update(recordId, {
|
|
3807
|
+
${s[0]?.name||"name"}: newValue
|
|
3808
|
+
});
|
|
3809
|
+
|
|
3810
|
+
`,o+=`// Delete
|
|
3811
|
+
await vaif.from("${i.name}").delete(recordId);
|
|
3812
|
+
`,o+="```\n\n",i===t[0]&&(o+=`#### Direct REST API (fetch)
|
|
3813
|
+
|
|
3814
|
+
`,o+="```typescript\n",o+=`// GET all rows (returns { data: [...], count: N })
|
|
3815
|
+
`,o+=`const res = await fetch("${e}/generated/${i.name}", {
|
|
3816
|
+
`,o+=` headers: { "x-vaif-key": "${a}" },
|
|
3817
|
+
`,o+=`});
|
|
3818
|
+
`,o+=`const { data } = await res.json();
|
|
3819
|
+
|
|
3820
|
+
`,o+=`// GET single row (returns { data: {...} })
|
|
3821
|
+
`,o+=`const res2 = await fetch("${e}/generated/${i.name}/\${id}", {
|
|
3822
|
+
`,o+=` headers: { "x-vaif-key": "${a}" },
|
|
3823
|
+
`,o+=`});
|
|
3824
|
+
`,o+=`const { data: record } = await res2.json();
|
|
3825
|
+
`,o+="```\n\n");}return o}function De(n){let e=n.type.toLowerCase();return e.includes("uuid")?'"crypto.randomUUID()"':e.includes("int")||e.includes("serial")?"42":e.includes("bool")?"true":e.includes("timestamp")||e.includes("date")?'"new Date().toISOString()"':e.includes("json")?"{}":e.includes("float")||e.includes("numeric")||e.includes("decimal")||e.includes("double")?"3.14":`"example_${n.name}"`}function ke(n){let{projectId:e,apiKey:a,apiUrl:t,projectName:o,schema:i}=n,s=Le(i.tables),l=Ne(i.tables,t,a);return `# VAIF Studio Backend
|
|
3826
|
+
|
|
3827
|
+
This project uses **VAIF Studio** as its backend. Project: **${o}** (\`${e}\`).
|
|
3828
|
+
|
|
3829
|
+
## SDK Setup
|
|
3830
|
+
|
|
3831
|
+
\`\`\`bash
|
|
3832
|
+
npm install @vaiftech/client
|
|
3833
|
+
\`\`\`
|
|
3834
|
+
|
|
3835
|
+
\`\`\`typescript
|
|
3836
|
+
import { createVaifClient } from "@vaiftech/client";
|
|
3837
|
+
|
|
3838
|
+
const vaif = createVaifClient({
|
|
3839
|
+
baseUrl: "${t}",
|
|
3840
|
+
projectId: "${e}",
|
|
3841
|
+
apiKey: "${a}",
|
|
3842
|
+
});
|
|
3843
|
+
\`\`\`
|
|
3844
|
+
|
|
3845
|
+
## Database Schema
|
|
3846
|
+
|
|
3847
|
+
${s}
|
|
3848
|
+
|
|
3849
|
+
## CRUD Examples
|
|
3850
|
+
|
|
3851
|
+
${l}
|
|
3852
|
+
|
|
3853
|
+
## Authentication (End-User Auth)
|
|
3854
|
+
|
|
3855
|
+
VAIF provides **project-scoped** authentication for your app's end-users. All auth routes are scoped to your project ID.
|
|
3856
|
+
|
|
3857
|
+
### API Routes
|
|
3858
|
+
|
|
3859
|
+
| Route | Method | Auth | Description |
|
|
3860
|
+
|-------|--------|------|-------------|
|
|
3861
|
+
| \`/projects/${e}/auth/signup\` | POST | None (public) | Register a new user |
|
|
3862
|
+
| \`/projects/${e}/auth/login\` | POST | None (public) | Login with email/password |
|
|
3863
|
+
| \`/projects/${e}/auth/refresh\` | POST | Cookie | Refresh access token |
|
|
3864
|
+
|
|
3865
|
+
### Signup
|
|
3866
|
+
|
|
3867
|
+
\`\`\`typescript
|
|
3868
|
+
const res = await fetch("${t}/projects/${e}/auth/signup", {
|
|
3869
|
+
method: "POST",
|
|
3870
|
+
headers: { "Content-Type": "application/json" },
|
|
3871
|
+
body: JSON.stringify({
|
|
3872
|
+
email: "user@example.com",
|
|
3873
|
+
password: "securePassword123",
|
|
3874
|
+
metadata: { displayName: "Jane Doe" }, // optional
|
|
3875
|
+
}),
|
|
3876
|
+
});
|
|
3877
|
+
const { accessToken, expiresIn, user } = await res.json();
|
|
3878
|
+
// accessToken: JWT with { sub: userId, email, projectId, type: "project_user" }
|
|
3879
|
+
\`\`\`
|
|
3880
|
+
|
|
3881
|
+
### Login
|
|
3882
|
+
|
|
3883
|
+
\`\`\`typescript
|
|
3884
|
+
const res = await fetch("${t}/projects/${e}/auth/login", {
|
|
3885
|
+
method: "POST",
|
|
3886
|
+
headers: { "Content-Type": "application/json" },
|
|
3887
|
+
body: JSON.stringify({
|
|
3888
|
+
email: "user@example.com",
|
|
3889
|
+
password: "securePassword123",
|
|
3890
|
+
}),
|
|
3891
|
+
});
|
|
3892
|
+
const { accessToken, expiresIn, user } = await res.json();
|
|
3893
|
+
\`\`\`
|
|
3894
|
+
|
|
3895
|
+
### Token Refresh
|
|
3896
|
+
|
|
3897
|
+
\`\`\`typescript
|
|
3898
|
+
// Refresh token is stored as httpOnly cookie (project_refresh_token)
|
|
3899
|
+
// and sent automatically with same-origin requests
|
|
3900
|
+
const res = await fetch("${t}/projects/${e}/auth/refresh", {
|
|
3901
|
+
method: "POST",
|
|
3902
|
+
credentials: "include", // sends the httpOnly cookie
|
|
3903
|
+
});
|
|
3904
|
+
const { accessToken, expiresIn, user } = await res.json();
|
|
3905
|
+
\`\`\`
|
|
3906
|
+
|
|
3907
|
+
### Using Auth in Your App
|
|
3908
|
+
|
|
3909
|
+
\`\`\`typescript
|
|
3910
|
+
// Store the access token and use it for authenticated requests
|
|
3911
|
+
const headers = {
|
|
3912
|
+
Authorization: \\\`Bearer \\\${accessToken}\\\`,
|
|
3913
|
+
"x-vaif-key": "${a}",
|
|
3914
|
+
};
|
|
3915
|
+
|
|
3916
|
+
// The JWT contains: { sub: userId, email, projectId, type: "project_user" }
|
|
3917
|
+
// Use this with RLS to scope data to the current user
|
|
3918
|
+
\`\`\`
|
|
3919
|
+
|
|
3920
|
+
> **Important**: Auth routes are at \`/projects/{projectId}/auth/*\`, NOT \`/auth/*\`. The \`/auth/*\` routes are for VAIF Studio platform accounts, not your app's end-users.
|
|
3921
|
+
|
|
3922
|
+
## Storage
|
|
3923
|
+
|
|
3924
|
+
\`\`\`typescript
|
|
3925
|
+
// Upload a file
|
|
3926
|
+
const { url } = await vaif.storage.upload("avatars", file, {
|
|
3927
|
+
contentType: "image/png",
|
|
3928
|
+
});
|
|
3929
|
+
|
|
3930
|
+
// Download a file
|
|
3931
|
+
const blob = await vaif.storage.download("avatars", "photo.png");
|
|
3932
|
+
|
|
3933
|
+
// Create a signed URL (expiring)
|
|
3934
|
+
const { signedUrl } = await vaif.storage.createSignedUrl("avatars", "photo.png", {
|
|
3935
|
+
expiresIn: 3600,
|
|
3936
|
+
});
|
|
3937
|
+
|
|
3938
|
+
// List files in a bucket
|
|
3939
|
+
const files = await vaif.storage.list("avatars", { limit: 100 });
|
|
3940
|
+
\`\`\`
|
|
3941
|
+
|
|
3942
|
+
## Functions
|
|
3943
|
+
|
|
3944
|
+
\`\`\`typescript
|
|
3945
|
+
// Invoke a serverless function
|
|
3946
|
+
const result = await vaif.functions.invoke("send_welcome_email", {
|
|
3947
|
+
body: { userId: "user_123", template: "welcome" },
|
|
3948
|
+
});
|
|
3949
|
+
\`\`\`
|
|
3950
|
+
|
|
3951
|
+
> **Function naming**: Names must be alphanumeric and underscores only (\`^[a-zA-Z0-9_]+$\`). Use \`send_email\` not \`send-email\`.
|
|
3952
|
+
|
|
3953
|
+
## Realtime
|
|
3954
|
+
|
|
3955
|
+
\`\`\`typescript
|
|
3956
|
+
// Subscribe to a channel
|
|
3957
|
+
const channel = vaif.realtime.channel("my-channel");
|
|
3958
|
+
|
|
3959
|
+
// Listen for postgres changes
|
|
3960
|
+
channel.on("postgres_changes", {
|
|
3961
|
+
event: "INSERT",
|
|
3962
|
+
schema: "public",
|
|
3963
|
+
table: "messages",
|
|
3964
|
+
}, (payload) => {
|
|
3965
|
+
console.log("New message:", payload.new);
|
|
3966
|
+
});
|
|
3967
|
+
|
|
3968
|
+
// Listen for UPDATE events
|
|
3969
|
+
channel.on("postgres_changes", {
|
|
3970
|
+
event: "UPDATE",
|
|
3971
|
+
schema: "public",
|
|
3972
|
+
table: "messages",
|
|
3973
|
+
}, (payload) => {
|
|
3974
|
+
console.log("Updated:", payload.new, "was:", payload.old);
|
|
3975
|
+
});
|
|
3976
|
+
|
|
3977
|
+
// Listen for DELETE events
|
|
3978
|
+
channel.on("postgres_changes", {
|
|
3979
|
+
event: "DELETE",
|
|
3980
|
+
schema: "public",
|
|
3981
|
+
table: "messages",
|
|
3982
|
+
}, (payload) => {
|
|
3983
|
+
console.log("Deleted:", payload.old);
|
|
3984
|
+
});
|
|
3985
|
+
|
|
3986
|
+
// Subscribe to start receiving events
|
|
3987
|
+
channel.subscribe();
|
|
3988
|
+
|
|
3989
|
+
// Unsubscribe when done
|
|
3990
|
+
channel.unsubscribe();
|
|
3991
|
+
\`\`\`
|
|
3992
|
+
|
|
3993
|
+
## Row-Level Security (RLS)
|
|
3994
|
+
|
|
3995
|
+
Filter data per-user by sending the \`X-VAIF-RLS\` header with your requests. The header format is \`field:value\`, comma-separated for multiple fields.
|
|
3996
|
+
|
|
3997
|
+
\`\`\`typescript
|
|
3998
|
+
// SDK: pass RLS context to scope queries to the current user
|
|
3999
|
+
const posts = await vaif.from("posts").select({
|
|
4000
|
+
headers: { "x-vaif-rls": \`user_id:\${currentUser.id}\` },
|
|
4001
|
+
});
|
|
4002
|
+
|
|
4003
|
+
// Multiple RLS fields (e.g., multi-tenant + user scoping)
|
|
4004
|
+
const data = await vaif.from("documents").select({
|
|
4005
|
+
headers: { "x-vaif-rls": \`org_id:\${orgId},user_id:\${userId}\` },
|
|
4006
|
+
});
|
|
4007
|
+
\`\`\`
|
|
4008
|
+
|
|
4009
|
+
**How it works:**
|
|
4010
|
+
- On **SELECT / UPDATE / DELETE**: RLS fields are added as WHERE conditions (e.g., \`WHERE user_id = $1\`)
|
|
4011
|
+
- On **INSERT**: RLS fields are auto-populated into the record if not already provided
|
|
4012
|
+
- This enables multi-tenant data isolation without database-level RLS policies
|
|
4013
|
+
|
|
4014
|
+
## Realtime Setup
|
|
4015
|
+
|
|
4016
|
+
Enable realtime on your tables, then subscribe to live changes:
|
|
4017
|
+
|
|
4018
|
+
\`\`\`typescript
|
|
4019
|
+
// 1. Enable realtime on tables via API
|
|
4020
|
+
await fetch("${t}/realtime/install", {
|
|
4021
|
+
method: "POST",
|
|
4022
|
+
headers: {
|
|
4023
|
+
Authorization: \`Bearer \${token}\`,
|
|
4024
|
+
"Content-Type": "application/json",
|
|
4025
|
+
},
|
|
4026
|
+
body: JSON.stringify({
|
|
4027
|
+
projectId: "${e}",
|
|
4028
|
+
tables: ["messages", "notifications"],
|
|
4029
|
+
}),
|
|
4030
|
+
});
|
|
4031
|
+
|
|
4032
|
+
// 2. Subscribe to changes via SDK
|
|
4033
|
+
const channel = vaif.realtime.channel("chat-room");
|
|
4034
|
+
|
|
4035
|
+
channel.on("postgres_changes", {
|
|
4036
|
+
event: "*", // INSERT, UPDATE, DELETE, or * for all
|
|
4037
|
+
schema: "public",
|
|
4038
|
+
table: "messages",
|
|
4039
|
+
}, (payload) => {
|
|
4040
|
+
console.log("Change:", payload.eventType, payload.new);
|
|
4041
|
+
});
|
|
4042
|
+
|
|
4043
|
+
channel.subscribe();
|
|
4044
|
+
|
|
4045
|
+
// 3. Presence: track who's online
|
|
4046
|
+
channel.on("presence", { event: "sync" }, () => {
|
|
4047
|
+
const state = channel.presenceState();
|
|
4048
|
+
console.log("Online users:", Object.keys(state));
|
|
4049
|
+
});
|
|
4050
|
+
channel.track({ user_id: currentUser.id, status: "online" });
|
|
4051
|
+
|
|
4052
|
+
// 4. Broadcast: send ephemeral messages (typing indicators, cursors)
|
|
4053
|
+
channel.send({
|
|
4054
|
+
type: "broadcast",
|
|
4055
|
+
event: "typing",
|
|
4056
|
+
payload: { userId: currentUser.id },
|
|
4057
|
+
});
|
|
4058
|
+
|
|
4059
|
+
// Cleanup
|
|
4060
|
+
channel.unsubscribe();
|
|
4061
|
+
\`\`\`
|
|
4062
|
+
|
|
4063
|
+
## Storage Policies
|
|
4064
|
+
|
|
4065
|
+
Control who can access storage buckets with RLS-style policies:
|
|
4066
|
+
|
|
4067
|
+
\`\`\`typescript
|
|
4068
|
+
// Create a policy: only the uploader can read their own files
|
|
4069
|
+
await fetch("${t}/storage/buckets/\${bucketId}/policies", {
|
|
4070
|
+
method: "POST",
|
|
4071
|
+
headers: {
|
|
4072
|
+
Authorization: \`Bearer \${token}\`,
|
|
4073
|
+
"Content-Type": "application/json",
|
|
4074
|
+
},
|
|
4075
|
+
body: JSON.stringify({
|
|
4076
|
+
name: "owner_read",
|
|
4077
|
+
operation: "SELECT", // SELECT | INSERT | UPDATE | DELETE | ALL
|
|
4078
|
+
definition: "auth.uid() = owner_id",
|
|
4079
|
+
}),
|
|
4080
|
+
});
|
|
4081
|
+
|
|
4082
|
+
// Create a policy: authenticated users can upload
|
|
4083
|
+
await fetch("${t}/storage/buckets/\${bucketId}/policies", {
|
|
4084
|
+
method: "POST",
|
|
4085
|
+
headers: {
|
|
4086
|
+
Authorization: \`Bearer \${token}\`,
|
|
4087
|
+
"Content-Type": "application/json",
|
|
4088
|
+
},
|
|
4089
|
+
body: JSON.stringify({
|
|
4090
|
+
name: "auth_insert",
|
|
4091
|
+
operation: "INSERT",
|
|
4092
|
+
definition: "auth.uid() IS NOT NULL",
|
|
4093
|
+
}),
|
|
4094
|
+
});
|
|
4095
|
+
\`\`\`
|
|
4096
|
+
|
|
4097
|
+
## Edge Function Deployment
|
|
4098
|
+
|
|
4099
|
+
Create and deploy serverless functions:
|
|
4100
|
+
|
|
4101
|
+
\`\`\`typescript
|
|
4102
|
+
// 1. Create a function
|
|
4103
|
+
const fn = await fetch("${t}/functions", {
|
|
4104
|
+
method: "POST",
|
|
4105
|
+
headers: {
|
|
4106
|
+
Authorization: \`Bearer \${token}\`,
|
|
4107
|
+
"Content-Type": "application/json",
|
|
4108
|
+
},
|
|
4109
|
+
body: JSON.stringify({
|
|
4110
|
+
projectId: "${e}",
|
|
4111
|
+
name: "send_welcome_email",
|
|
4112
|
+
runtime: "nodejs20", // nodejs20 (default)
|
|
4113
|
+
entrypoint: "index.ts", // default
|
|
4114
|
+
timeoutMs: 10000, // 1000\u201330000ms, default 10000
|
|
4115
|
+
}),
|
|
4116
|
+
});
|
|
4117
|
+
|
|
4118
|
+
// 2. Deploy source code
|
|
4119
|
+
await fetch(\`${t}/functions/\${fn.id}/source\`, {
|
|
4120
|
+
method: "PUT",
|
|
4121
|
+
headers: {
|
|
4122
|
+
Authorization: \`Bearer \${token}\`,
|
|
4123
|
+
"Content-Type": "application/json",
|
|
4124
|
+
},
|
|
4125
|
+
body: JSON.stringify({
|
|
4126
|
+
sourceCode: \`
|
|
4127
|
+
export default async function handler(req, ctx) {
|
|
4128
|
+
const { userId } = req.body;
|
|
4129
|
+
// ctx.secrets contains your encrypted secrets
|
|
4130
|
+
const apiKey = ctx.secrets.SENDGRID_KEY;
|
|
4131
|
+
return { status: "sent", userId };
|
|
4132
|
+
}
|
|
4133
|
+
\`,
|
|
4134
|
+
}),
|
|
4135
|
+
});
|
|
4136
|
+
|
|
4137
|
+
// 3. Invoke the function
|
|
4138
|
+
const result = await vaif.functions.invoke("send_welcome_email", {
|
|
4139
|
+
body: { userId: "user_123" },
|
|
4140
|
+
});
|
|
4141
|
+
\`\`\`
|
|
4142
|
+
|
|
4143
|
+
### Authenticated Context in Functions
|
|
4144
|
+
|
|
4145
|
+
Access the caller's verified identity via \\\`vaif.auth\\\`:
|
|
4146
|
+
|
|
4147
|
+
\`\`\`typescript
|
|
4148
|
+
export default async function handler(req) {
|
|
4149
|
+
const auth = vaif.auth;
|
|
4150
|
+
// auth.type = 'user' | 'api_key' | 'function'
|
|
4151
|
+
// auth.userId \u2014 User ID (for user/function types)
|
|
4152
|
+
// auth.email \u2014 User email (for user type)
|
|
4153
|
+
// auth.projectId \u2014 Always present
|
|
4154
|
+
// auth.scopes \u2014 API key scopes (for api_key type)
|
|
4155
|
+
|
|
4156
|
+
if (!auth || auth.type !== 'user') {
|
|
4157
|
+
return { statusCode: 401, body: { error: 'Unauthorized' } };
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
return { body: { message: "Hello " + auth.email } };
|
|
4161
|
+
}
|
|
4162
|
+
\`\`\`
|
|
4163
|
+
|
|
4164
|
+
### Function-to-Function Invocation
|
|
4165
|
+
|
|
4166
|
+
Call other functions from within a handler:
|
|
4167
|
+
|
|
4168
|
+
\`\`\`typescript
|
|
4169
|
+
export default async function handler(req) {
|
|
4170
|
+
const result = await vaif.invoke("send_email", {
|
|
4171
|
+
to: "user@example.com",
|
|
4172
|
+
subject: "Hello",
|
|
4173
|
+
});
|
|
4174
|
+
return { statusCode: 200, body: result };
|
|
4175
|
+
}
|
|
4176
|
+
\`\`\`
|
|
4177
|
+
|
|
4178
|
+
### Database Triggers
|
|
4179
|
+
|
|
4180
|
+
Fire functions automatically on insert/update/delete events. Configure triggers via the API:
|
|
4181
|
+
|
|
4182
|
+
\`\`\`
|
|
4183
|
+
POST /functions/\\\${functionId}/triggers
|
|
4184
|
+
{ "event": "db.insert", "tableName": "orders", "enabled": true }
|
|
4185
|
+
\`\`\`
|
|
4186
|
+
|
|
4187
|
+
## API Key Management
|
|
4188
|
+
|
|
4189
|
+
API keys are project-scoped and used for data-plane authentication (CRUD, storage, functions).
|
|
4190
|
+
|
|
4191
|
+
\`\`\`typescript
|
|
4192
|
+
// Create a new API key
|
|
4193
|
+
const { key } = await fetch("${t}/projects/${e}/api-keys", {
|
|
4194
|
+
method: "POST",
|
|
4195
|
+
headers: {
|
|
4196
|
+
Authorization: \`Bearer \${token}\`,
|
|
4197
|
+
"Content-Type": "application/json",
|
|
4198
|
+
},
|
|
4199
|
+
body: JSON.stringify({ name: "production-frontend" }),
|
|
4200
|
+
}).then(r => r.json());
|
|
4201
|
+
|
|
4202
|
+
// List keys
|
|
4203
|
+
const keys = await fetch("${t}/projects/${e}/api-keys", {
|
|
4204
|
+
headers: { Authorization: \`Bearer \${token}\` },
|
|
4205
|
+
}).then(r => r.json());
|
|
4206
|
+
|
|
4207
|
+
// Rotate a key (generates new secret, old key stops working)
|
|
4208
|
+
await fetch(\`${t}/projects/${e}/api-keys/\${keyId}/rotate\`, {
|
|
4209
|
+
method: "POST",
|
|
4210
|
+
headers: { Authorization: \`Bearer \${token}\` },
|
|
4211
|
+
});
|
|
4212
|
+
|
|
4213
|
+
// Revoke a key
|
|
4214
|
+
await fetch(\`${t}/projects/${e}/api-keys/\${keyId}/revoke\`, {
|
|
4215
|
+
method: "POST",
|
|
4216
|
+
headers: { Authorization: \`Bearer \${token}\` },
|
|
4217
|
+
});
|
|
4218
|
+
\`\`\`
|
|
4219
|
+
|
|
4220
|
+
## Secrets & Environment Variables
|
|
4221
|
+
|
|
4222
|
+
Secrets are encrypted at rest and injected into function invocations at runtime.
|
|
4223
|
+
|
|
4224
|
+
\`\`\`typescript
|
|
4225
|
+
// Set a secret (via API)
|
|
4226
|
+
await fetch("${t}/functions/secrets", {
|
|
4227
|
+
method: "POST",
|
|
4228
|
+
headers: {
|
|
4229
|
+
Authorization: \`Bearer \${token}\`,
|
|
4230
|
+
"Content-Type": "application/json",
|
|
4231
|
+
},
|
|
4232
|
+
body: JSON.stringify({
|
|
4233
|
+
projectId: "${e}",
|
|
4234
|
+
key: "STRIPE_SECRET_KEY",
|
|
4235
|
+
value: "sk_live_...",
|
|
4236
|
+
}),
|
|
4237
|
+
});
|
|
4238
|
+
|
|
4239
|
+
// Or use the CLI
|
|
4240
|
+
// vaif secrets set STRIPE_SECRET_KEY sk_live_...
|
|
4241
|
+
// vaif secrets list
|
|
4242
|
+
// vaif secrets delete STRIPE_SECRET_KEY
|
|
4243
|
+
\`\`\`
|
|
4244
|
+
|
|
4245
|
+
**Accessing secrets in functions:**
|
|
4246
|
+
|
|
4247
|
+
\`\`\`typescript
|
|
4248
|
+
export default async function handler(req, ctx) {
|
|
4249
|
+
const stripe = new Stripe(ctx.secrets.STRIPE_SECRET_KEY);
|
|
4250
|
+
// ...
|
|
4251
|
+
}
|
|
4252
|
+
\`\`\`
|
|
4253
|
+
|
|
4254
|
+
## API Reference Notes
|
|
4255
|
+
|
|
4256
|
+
### Filter Syntax
|
|
4257
|
+
|
|
4258
|
+
The SDK supports these filter operators:
|
|
4259
|
+
|
|
4260
|
+
| Operator | Description | Example |
|
|
4261
|
+
|----------|-------------|---------|
|
|
4262
|
+
| \`eq\` | Equal | \`.eq("status", "active")\` |
|
|
4263
|
+
| \`neq\` | Not equal | \`.neq("status", "deleted")\` |
|
|
4264
|
+
| \`gt\` | Greater than | \`.gt("age", 18)\` |
|
|
4265
|
+
| \`lt\` | Less than | \`.lt("price", 100)\` |
|
|
4266
|
+
| \`gte\` | Greater than or equal | \`.gte("score", 90)\` |
|
|
4267
|
+
| \`lte\` | Less than or equal | \`.lte("count", 10)\` |
|
|
4268
|
+
| \`in\` | In array | \`.in("role", ["admin", "editor"])\` |
|
|
4269
|
+
| \`like\` | Pattern match (case-sensitive) | \`.like("name", "%john%")\` |
|
|
4270
|
+
| \`ilike\` | Pattern match (case-insensitive) | \`.ilike("name", "%john%")\` |
|
|
4271
|
+
| \`is\` | IS comparison (null, true, false) | \`.is("deleted_at", null)\` |
|
|
4272
|
+
|
|
4273
|
+
### JSONB Subkey Filters
|
|
4274
|
+
|
|
4275
|
+
Filter on nested JSONB fields using arrow notation:
|
|
4276
|
+
|
|
4277
|
+
| Filter | SQL Generated |
|
|
4278
|
+
|--------|--------------|
|
|
4279
|
+
| \\\`filter[metadata->status]=active\\\` | \\\`metadata->>'status' = 'active'\\\` |
|
|
4280
|
+
| \\\`filter[config->theme.ilike]=%dark%\\\` | \\\`config->>'theme' ILIKE '%dark%'\\\` |
|
|
4281
|
+
| \\\`filter[data->user->role]=admin\\\` | \\\`data->'user'->>'role' = 'admin'\\\` |
|
|
4282
|
+
|
|
4283
|
+
All standard operators work with JSONB paths. The last segment uses \\\`->>\\\` (text extraction).
|
|
4284
|
+
|
|
4285
|
+
### Compound Filters (AND + OR)
|
|
4286
|
+
|
|
4287
|
+
Combine AND and OR conditions:
|
|
4288
|
+
|
|
4289
|
+
\`\`\`
|
|
4290
|
+
?filter[status]=active&or_filter[role]=admin&or_filter[role]=moderator
|
|
4291
|
+
\`\`\`
|
|
4292
|
+
|
|
4293
|
+
This generates: \\\`WHERE status = 'active' AND (role = 'admin' OR role = 'moderator')\\\`
|
|
4294
|
+
|
|
4295
|
+
### Full-Text Search
|
|
4296
|
+
|
|
4297
|
+
\`\`\`typescript
|
|
4298
|
+
const results = await fetch("${t}/generated/posts/search", {
|
|
4299
|
+
method: "POST",
|
|
4300
|
+
headers: { "x-vaif-key": "${a}", "Content-Type": "application/json" },
|
|
4301
|
+
body: JSON.stringify({
|
|
4302
|
+
query: "search term",
|
|
4303
|
+
columns: ["title", "body"],
|
|
4304
|
+
limit: 20,
|
|
4305
|
+
}),
|
|
4306
|
+
});
|
|
4307
|
+
// Results ranked by ts_rank score
|
|
4308
|
+
\`\`\`
|
|
4309
|
+
|
|
4310
|
+
### Aggregation
|
|
4311
|
+
|
|
4312
|
+
\`\`\`typescript
|
|
4313
|
+
const stats = await fetch("${t}/generated/orders/aggregate", {
|
|
4314
|
+
method: "POST",
|
|
4315
|
+
headers: { "x-vaif-key": "${a}", "Content-Type": "application/json" },
|
|
4316
|
+
body: JSON.stringify({
|
|
4317
|
+
aggregates: [
|
|
4318
|
+
{ fn: "count", column: "*" },
|
|
4319
|
+
{ fn: "sum", column: "total" },
|
|
4320
|
+
{ fn: "avg", column: "total" },
|
|
4321
|
+
],
|
|
4322
|
+
groupBy: ["status"],
|
|
4323
|
+
}),
|
|
4324
|
+
});
|
|
4325
|
+
\`\`\`
|
|
4326
|
+
|
|
4327
|
+
### Joins (Foreign Key Includes)
|
|
4328
|
+
|
|
4329
|
+
Include related rows by specifying foreign key columns:
|
|
4330
|
+
|
|
4331
|
+
\`\`\`
|
|
4332
|
+
GET /generated/posts?include=author_id&include=category_id
|
|
4333
|
+
\`\`\`
|
|
4334
|
+
|
|
4335
|
+
Returns posts with \\\`author_id_included\\\` and \\\`category_id_included\\\` objects containing the related rows.
|
|
4336
|
+
|
|
4337
|
+
### Upsert
|
|
4338
|
+
|
|
4339
|
+
Insert or update on conflict:
|
|
4340
|
+
|
|
4341
|
+
\`\`\`typescript
|
|
4342
|
+
const result = await fetch("${t}/generated/users", {
|
|
4343
|
+
method: "POST",
|
|
4344
|
+
headers: { "x-vaif-key": "${a}", "Content-Type": "application/json" },
|
|
4345
|
+
body: JSON.stringify({
|
|
4346
|
+
email: "alice@example.com",
|
|
4347
|
+
name: "Alice",
|
|
4348
|
+
_upsert: true,
|
|
4349
|
+
_conflictColumns: ["email"],
|
|
4350
|
+
}),
|
|
4351
|
+
});
|
|
4352
|
+
\`\`\`
|
|
4353
|
+
|
|
4354
|
+
### Pagination
|
|
4355
|
+
|
|
4356
|
+
\`\`\`typescript
|
|
4357
|
+
// Default: limit 20, offset 0
|
|
4358
|
+
const page1 = await vaif.from("posts").select().limit(20).offset(0);
|
|
4359
|
+
const page2 = await vaif.from("posts").select().limit(20).offset(20);
|
|
4360
|
+
\`\`\`
|
|
4361
|
+
|
|
4362
|
+
### REST API Response Format
|
|
4363
|
+
|
|
4364
|
+
When calling the REST API directly (without the SDK), all data-plane responses are wrapped:
|
|
4365
|
+
|
|
4366
|
+
\`\`\`typescript
|
|
4367
|
+
// GET /generated/{table} \u2192 list
|
|
4368
|
+
{ data: [...], count: 5 }
|
|
4369
|
+
|
|
4370
|
+
// GET /generated/{table}/{id} \u2192 single record
|
|
4371
|
+
{ data: { id: "...", ... } }
|
|
4372
|
+
|
|
4373
|
+
// POST /generated/{table} \u2192 created record
|
|
4374
|
+
{ data: { id: "...", ... } }
|
|
4375
|
+
|
|
4376
|
+
// PATCH /generated/{table}/{id} \u2192 updated record
|
|
4377
|
+
{ data: { id: "...", ... } }
|
|
4378
|
+
|
|
4379
|
+
// DELETE /generated/{table}/{id}
|
|
4380
|
+
{ ok: true }
|
|
4381
|
+
|
|
4382
|
+
// Error responses
|
|
4383
|
+
{ error: "NotFound", message: "...", requestId: "..." }
|
|
4384
|
+
\`\`\`
|
|
4385
|
+
|
|
4386
|
+
The SDK unwraps these automatically, but if you use \`fetch()\` directly, access the data via \`response.data\`.
|
|
4387
|
+
|
|
4388
|
+
### Numeric/Decimal Column Serialization
|
|
4389
|
+
|
|
4390
|
+
PostgreSQL \`numeric\` and \`decimal\` columns serialize to **JSON strings** (to preserve arbitrary precision). This is standard behavior. Any column typed as \`numeric\` or \`decimal\` will arrive as \`"3.14"\` not \`3.14\`.
|
|
4391
|
+
|
|
4392
|
+
\`\`\`typescript
|
|
4393
|
+
// Wrong \u2014 value is a string, comparison may fail
|
|
4394
|
+
if (item.price > 10.0) { ... }
|
|
4395
|
+
|
|
4396
|
+
// Correct \u2014 parse before arithmetic
|
|
4397
|
+
if (parseFloat(item.price) > 10.0) { ... }
|
|
4398
|
+
\`\`\`
|
|
4399
|
+
|
|
4400
|
+
### Auth Headers
|
|
4401
|
+
|
|
4402
|
+
VAIF uses **two auth modes** \u2014 choose the right one for each operation:
|
|
4403
|
+
|
|
4404
|
+
| Auth Mode | Header | Used For |
|
|
4405
|
+
|-----------|--------|----------|
|
|
4406
|
+
| **API Key** | \`x-vaif-key: vaif_xxx\` | Data-plane: CRUD (\`/generated/*\`), storage uploads/downloads, function invocation |
|
|
4407
|
+
| **JWT Token** | \`Authorization: Bearer <jwt>\` | Control-plane: schema introspection, project management, function CRUD, bucket creation |
|
|
4408
|
+
|
|
4409
|
+
> **Important**: API keys do NOT work for control-plane endpoints (creating functions, managing buckets, schema changes). Those require a JWT session token. The MCP server handles this automatically by using both auth modes.
|
|
4410
|
+
|
|
4411
|
+
### MCP Tools (via .mcp.json)
|
|
4412
|
+
|
|
4413
|
+
The \`.mcp.json\` file configures an MCP server that gives Claude Code direct access to your VAIF project. Available tools:
|
|
4414
|
+
|
|
4415
|
+
| Tool | What it does |
|
|
4416
|
+
|------|-------------|
|
|
4417
|
+
| \`list_tables\`, \`describe_table\` | Inspect database schema |
|
|
4418
|
+
| \`get_schema\` | Full schema as JSON |
|
|
4419
|
+
| \`create_tables\` | Create or update tables declaratively |
|
|
4420
|
+
| \`query_rows\` | Query with filters, JSONB paths, pagination |
|
|
4421
|
+
| \`insert_row\`, \`update_row\`, \`delete_row\` | CRUD operations on any table |
|
|
4422
|
+
| \`list_functions\`, \`deploy_function\`, \`invoke_function\` | Function management |
|
|
4423
|
+
| \`get_function_logs\` | Execution history with status filters |
|
|
4424
|
+
| \`set_secret\`, \`list_secrets\`, \`delete_secret\` | Function secrets |
|
|
4425
|
+
| \`list_buckets\`, \`list_files\`, \`get_signed_url\` | Storage operations |
|
|
4426
|
+
| \`enable_realtime\`, \`realtime_status\` | Realtime subscriptions |
|
|
4427
|
+
|
|
4428
|
+
> **Note**: MCP tools are only available to the main Claude Code session, not to spawned sub-agents (Task tool). The main session should handle all VAIF backend operations directly.
|
|
4429
|
+
`}async function ne(n){let e=j(),a=R();(!a||!a.token)&&(console.log(u.red("Not logged in")),console.log(u.gray("Run `vaif login` first to authenticate")),process.exit(1));let t=n.config||"vaif.config.json",o=null;try{o=await N(t);}catch{}let i=Re(n,o,a);i||(console.log(u.yellow(`No project ID specified \u2014 fetching your projects...
|
|
4430
|
+
`)),i=await Ue(a.token),i||(console.log(u.gray(`
|
|
4431
|
+
Tip: pass --project-id <id> or set projectId in vaif.config.json`)),process.exit(1)));let s=I.resolve(n.outputDir||".");console.log(""),console.log(u.bold("VAIF Claude Code Setup")),console.log(u.gray(` Project: ${i}`)),console.log(""),e.start("Fetching database schema...");let l={tables:[]};try{let m=await fetch(`${F}/schema-engine/introspect/${i}`,{headers:{Authorization:`Bearer ${a.token}`}});m.ok?(l=await m.json(),e.succeed(`Fetched schema (${l.tables?.length||0} tables)`)):e.warn("Could not fetch schema \u2014 continuing without it");}catch{e.warn("Could not fetch schema \u2014 continuing without it");}e.start("Fetching project info...");let r=i,c=F;try{let m=await fetch(`${F}/projects/${i}`,{headers:{Authorization:`Bearer ${a.token}`}});if(m.ok){let d=await m.json(),f=d.project||d;r=f.name||i,c=f.apiUrl||F,e.succeed(`Project: ${r}`);}else e.warn("Could not fetch project info \u2014 using defaults");}catch{e.warn("Could not fetch project info \u2014 using defaults");}let h=n.apiKey||o?.api?.apiKey||"";if(!h){e.start("Generating API key for Claude Code...");try{let m=`claude-code-${Date.now()}`,d=await fetch(`${F}/projects/${i}/api-keys`,{method:"POST",headers:{Authorization:`Bearer ${a.token}`,"Content-Type":"application/json"},body:JSON.stringify({name:m,scopes:["crud","realtime","functions","storage"]})});if(d.ok){let f=await d.json();h=f.apiKey||f.key,e.succeed(`Generated API key: ${m}`);}else e.fail("Could not auto-generate API key"),console.log(u.yellow(" Pass one with --api-key or generate via `vaif keys generate`")),process.exit(1);}catch{e.fail("Could not auto-generate API key"),console.log(u.yellow(" Pass one with --api-key or generate via `vaif keys generate`")),process.exit(1);}}if(!n.skipMcp){e.start("Writing .mcp.json...");let m={mcpServers:{"vaif-studio":{command:"npx",args:["@vaiftech/mcp"],env:{VAIF_API_KEY:h,VAIF_PROJECT_ID:i,VAIF_API_URL:c,VAIF_AUTH_TOKEN:a.token}}}},d=I.join(s,".mcp.json");v.writeFileSync(d,JSON.stringify(m,null,2)+`
|
|
4432
|
+
`,"utf-8"),e.succeed(`Written ${u.cyan(".mcp.json")}`);}if(!n.skipClaudeMd){e.start("Writing CLAUDE.md...");let m=ke({projectId:i,apiKey:h,apiUrl:c,projectName:r,schema:l}),d=I.join(s,"CLAUDE.md");v.writeFileSync(d,m,"utf-8"),e.succeed(`Written ${u.cyan("CLAUDE.md")}`);}console.log(""),console.log(u.green.bold(" Claude Code integration configured!")),console.log(""),n.skipMcp||console.log(u.gray(" MCP Server: ")+u.white(".mcp.json")),n.skipClaudeMd||console.log(u.gray(" Context: ")+u.white("CLAUDE.md")),console.log(""),console.log(u.bold(" Next steps:")),console.log(u.gray(" 1. Open this project in Claude Code")),console.log(u.gray(" 2. The MCP server auto-connects to your VAIF project")),console.log(u.gray(" 3. Ask Claude to query, modify, or build against your schema")),console.log("");}var k={base:`# VAIF Studio Backend
|
|
4433
|
+
|
|
4434
|
+
This project uses **VAIF Studio** as its backend.
|
|
4435
|
+
|
|
4436
|
+
## SDK Setup
|
|
4437
|
+
|
|
4438
|
+
\`\`\`bash
|
|
4439
|
+
npm install @vaiftech/client
|
|
4440
|
+
\`\`\`
|
|
4441
|
+
|
|
4442
|
+
\`\`\`typescript
|
|
4443
|
+
import { createVaifClient } from "@vaiftech/client";
|
|
4444
|
+
|
|
4445
|
+
const vaif = createVaifClient({
|
|
4446
|
+
baseUrl: "https://api.vaif.studio",
|
|
4447
|
+
projectId: process.env.VAIF_PROJECT_ID!,
|
|
4448
|
+
apiKey: process.env.VAIF_API_KEY!,
|
|
4449
|
+
});
|
|
4450
|
+
\`\`\`
|
|
4451
|
+
|
|
4452
|
+
## MCP Server
|
|
4453
|
+
|
|
4454
|
+
This project includes an MCP server for Claude Code integration. Tools available:
|
|
4455
|
+
|
|
4456
|
+
- **Database**: list_tables, describe_table, query_rows, insert_row, update_row, delete_row
|
|
4457
|
+
- **Schema**: get_schema, create_tables
|
|
4458
|
+
- **Storage**: list_buckets, create_bucket, list_files, get_signed_url
|
|
4459
|
+
- **Functions**: list_functions, invoke_function, create_function, deploy_function
|
|
4460
|
+
- **Auth**: list_api_keys, create_api_key
|
|
4461
|
+
- **Realtime**: realtime_status, enable_realtime
|
|
4462
|
+
|
|
4463
|
+
## Database
|
|
4464
|
+
|
|
4465
|
+
\`\`\`typescript
|
|
4466
|
+
// Query
|
|
4467
|
+
const { data } = await vaif.from("table_name").select().eq("column", value);
|
|
4468
|
+
|
|
4469
|
+
// Insert
|
|
4470
|
+
const { data } = await vaif.from("table_name").insert({ column: value });
|
|
4471
|
+
|
|
4472
|
+
// Update
|
|
4473
|
+
await vaif.from("table_name").update({ column: newValue }).eq("id", recordId);
|
|
4474
|
+
|
|
4475
|
+
// Delete
|
|
4476
|
+
await vaif.from("table_name").delete().eq("id", recordId);
|
|
4477
|
+
\`\`\`
|
|
4478
|
+
|
|
4479
|
+
## Authentication
|
|
4480
|
+
|
|
4481
|
+
\`\`\`typescript
|
|
4482
|
+
// Sign up
|
|
4483
|
+
await vaif.auth.signUp({ email, password });
|
|
4484
|
+
|
|
4485
|
+
// Sign in
|
|
4486
|
+
const { data } = await vaif.auth.signIn({ email, password });
|
|
4487
|
+
|
|
4488
|
+
// OAuth
|
|
4489
|
+
await vaif.auth.signInWithOAuth({ provider: "google", redirectTo: callbackUrl });
|
|
4490
|
+
|
|
4491
|
+
// Get current user
|
|
4492
|
+
const user = await vaif.auth.getUser();
|
|
4493
|
+
\`\`\`
|
|
4494
|
+
|
|
4495
|
+
## Storage
|
|
4496
|
+
|
|
4497
|
+
\`\`\`typescript
|
|
4498
|
+
// Upload
|
|
4499
|
+
await vaif.storage.from("bucket").upload("path/file.png", file);
|
|
4500
|
+
|
|
4501
|
+
// Signed URL
|
|
4502
|
+
const { url } = await vaif.storage.from("bucket").createSignedUrl("path/file.png", 3600);
|
|
4503
|
+
|
|
4504
|
+
// List files
|
|
4505
|
+
const { data } = await vaif.storage.from("bucket").list("path/");
|
|
4506
|
+
\`\`\`
|
|
4507
|
+
|
|
4508
|
+
## Functions
|
|
4509
|
+
|
|
4510
|
+
\`\`\`typescript
|
|
4511
|
+
// Invoke
|
|
4512
|
+
const result = await vaif.functions.invoke("function_name", { body: { key: "value" } });
|
|
4513
|
+
\`\`\`
|
|
4514
|
+
|
|
4515
|
+
> Function names must be alphanumeric + underscores only (\`^[a-zA-Z0-9_]+$\`).
|
|
4516
|
+
|
|
4517
|
+
## Realtime
|
|
4518
|
+
|
|
4519
|
+
\`\`\`typescript
|
|
4520
|
+
const subscription = vaif.realtime
|
|
4521
|
+
.channel("table_name")
|
|
4522
|
+
.on("INSERT", (payload) => console.log("New:", payload.new))
|
|
4523
|
+
.on("UPDATE", (payload) => console.log("Updated:", payload.new))
|
|
4524
|
+
.on("DELETE", (payload) => console.log("Removed:", payload.old))
|
|
4525
|
+
.subscribe();
|
|
4526
|
+
\`\`\`
|
|
4527
|
+
|
|
4528
|
+
## Filter Operators
|
|
4529
|
+
|
|
4530
|
+
| Operator | Description | Example |
|
|
4531
|
+
|----------|-------------|---------|
|
|
4532
|
+
| eq | Equal | \`.eq("status", "active")\` |
|
|
4533
|
+
| neq | Not equal | \`.neq("status", "deleted")\` |
|
|
4534
|
+
| gt / gte | Greater than | \`.gt("age", 18)\` |
|
|
4535
|
+
| lt / lte | Less than | \`.lt("price", 100)\` |
|
|
4536
|
+
| in | In array | \`.in("role", ["admin", "editor"])\` |
|
|
4537
|
+
| like / ilike | Pattern match | \`.ilike("name", "%john%")\` |
|
|
4538
|
+
| is | IS NULL check | \`.is("deleted_at", null)\` |
|
|
4539
|
+
|
|
4540
|
+
## Auth Headers
|
|
4541
|
+
|
|
4542
|
+
| Mode | Header | Used For |
|
|
4543
|
+
|------|--------|----------|
|
|
4544
|
+
| API Key | \`x-vaif-key: vk_xxx\` | Data-plane: CRUD, storage, functions |
|
|
4545
|
+
| JWT Token | \`Authorization: Bearer <jwt>\` | Control-plane: schema, project management |
|
|
4546
|
+
|
|
4547
|
+
## Environment Variables
|
|
4548
|
+
|
|
4549
|
+
\`\`\`bash
|
|
4550
|
+
VAIF_API_URL=https://api.vaif.studio
|
|
4551
|
+
VAIF_PROJECT_ID=proj_xxx
|
|
4552
|
+
VAIF_API_KEY=vk_xxx
|
|
4553
|
+
\`\`\`
|
|
4554
|
+
`,saas:`# VAIF Studio \u2014 SaaS Backend
|
|
4555
|
+
|
|
4556
|
+
This project uses **VAIF Studio** as its backend for a SaaS application.
|
|
4557
|
+
|
|
4558
|
+
## SDK Setup
|
|
4559
|
+
|
|
4560
|
+
\`\`\`bash
|
|
4561
|
+
npm install @vaiftech/client @vaiftech/react
|
|
4562
|
+
\`\`\`
|
|
4563
|
+
|
|
4564
|
+
\`\`\`typescript
|
|
4565
|
+
import { createVaifClient } from "@vaiftech/client";
|
|
4566
|
+
|
|
4567
|
+
const vaif = createVaifClient({
|
|
4568
|
+
baseUrl: "https://api.vaif.studio",
|
|
4569
|
+
projectId: process.env.VAIF_PROJECT_ID!,
|
|
4570
|
+
apiKey: process.env.VAIF_API_KEY!,
|
|
4571
|
+
});
|
|
4572
|
+
\`\`\`
|
|
4573
|
+
|
|
4574
|
+
## Common SaaS Schema Patterns
|
|
4575
|
+
|
|
4576
|
+
### Multi-Tenant with Organizations
|
|
4577
|
+
|
|
4578
|
+
\`\`\`sql
|
|
4579
|
+
-- Organizations (tenants)
|
|
4580
|
+
CREATE TABLE orgs (
|
|
4581
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
4582
|
+
name TEXT NOT NULL,
|
|
4583
|
+
slug TEXT UNIQUE NOT NULL,
|
|
4584
|
+
plan TEXT DEFAULT 'free',
|
|
4585
|
+
created_at TIMESTAMPTZ DEFAULT now()
|
|
4586
|
+
);
|
|
4587
|
+
|
|
4588
|
+
-- Organization members
|
|
4589
|
+
CREATE TABLE org_members (
|
|
4590
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
4591
|
+
org_id UUID REFERENCES orgs(id) ON DELETE CASCADE,
|
|
4592
|
+
user_id UUID NOT NULL,
|
|
4593
|
+
role TEXT DEFAULT 'member', -- owner, admin, member
|
|
4594
|
+
created_at TIMESTAMPTZ DEFAULT now(),
|
|
4595
|
+
UNIQUE(org_id, user_id)
|
|
4596
|
+
);
|
|
4597
|
+
|
|
4598
|
+
-- Tenant-scoped data
|
|
4599
|
+
CREATE TABLE projects (
|
|
4600
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
4601
|
+
org_id UUID REFERENCES orgs(id) ON DELETE CASCADE,
|
|
4602
|
+
name TEXT NOT NULL,
|
|
4603
|
+
settings JSONB DEFAULT '{}',
|
|
4604
|
+
created_at TIMESTAMPTZ DEFAULT now()
|
|
4605
|
+
);
|
|
4606
|
+
CREATE INDEX idx_projects_org ON projects(org_id);
|
|
4607
|
+
\`\`\`
|
|
4608
|
+
|
|
4609
|
+
### Row-Level Security for Multi-Tenancy
|
|
4610
|
+
|
|
4611
|
+
\`\`\`typescript
|
|
4612
|
+
// Scope all queries to the current organization
|
|
4613
|
+
const projects = await vaif.from("projects").select().eq("org_id", currentOrgId);
|
|
4614
|
+
|
|
4615
|
+
// Or use RLS headers for automatic scoping
|
|
4616
|
+
const data = await vaif.from("projects").select({
|
|
4617
|
+
headers: { "x-vaif-rls": \\\`org_id:\${currentOrgId}\\\` },
|
|
4618
|
+
});
|
|
4619
|
+
\`\`\`
|
|
4620
|
+
|
|
4621
|
+
### Auth Flows
|
|
4622
|
+
|
|
4623
|
+
\`\`\`typescript
|
|
4624
|
+
// Sign up \u2192 create org \u2192 add as owner
|
|
4625
|
+
const { data: user } = await vaif.auth.signUp({ email, password });
|
|
4626
|
+
const { data: org } = await vaif.from("orgs").insert({ name: orgName, slug });
|
|
4627
|
+
await vaif.from("org_members").insert({ org_id: org.id, user_id: user.id, role: "owner" });
|
|
4628
|
+
\`\`\`
|
|
4629
|
+
|
|
4630
|
+
### Billing Integration
|
|
4631
|
+
|
|
4632
|
+
\`\`\`typescript
|
|
4633
|
+
// Store Stripe customer/subscription IDs
|
|
4634
|
+
CREATE TABLE billing_accounts (
|
|
4635
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
4636
|
+
org_id UUID UNIQUE REFERENCES orgs(id),
|
|
4637
|
+
stripe_customer_id TEXT,
|
|
4638
|
+
stripe_subscription_id TEXT,
|
|
4639
|
+
plan TEXT DEFAULT 'free',
|
|
4640
|
+
period_start TIMESTAMPTZ,
|
|
4641
|
+
period_end TIMESTAMPTZ
|
|
4642
|
+
);
|
|
4643
|
+
|
|
4644
|
+
// Webhook handler (VAIF Function)
|
|
4645
|
+
export default async function handler(req, ctx) {
|
|
4646
|
+
const event = req.body;
|
|
4647
|
+
if (event.type === "customer.subscription.updated") {
|
|
4648
|
+
const sub = event.data.object;
|
|
4649
|
+
await vaif.from("billing_accounts")
|
|
4650
|
+
.update({ plan: sub.metadata.plan, period_end: sub.current_period_end })
|
|
4651
|
+
.eq("stripe_subscription_id", sub.id);
|
|
4652
|
+
}
|
|
4653
|
+
return { received: true };
|
|
4654
|
+
}
|
|
4655
|
+
\`\`\`
|
|
4656
|
+
|
|
4657
|
+
### Invite System
|
|
4658
|
+
|
|
4659
|
+
\`\`\`sql
|
|
4660
|
+
CREATE TABLE invites (
|
|
4661
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
4662
|
+
org_id UUID REFERENCES orgs(id) ON DELETE CASCADE,
|
|
4663
|
+
email TEXT NOT NULL,
|
|
4664
|
+
role TEXT DEFAULT 'member',
|
|
4665
|
+
token TEXT UNIQUE NOT NULL,
|
|
4666
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
4667
|
+
accepted_at TIMESTAMPTZ
|
|
4668
|
+
);
|
|
4669
|
+
\`\`\`
|
|
4670
|
+
|
|
4671
|
+
## API Key Scoping
|
|
4672
|
+
|
|
4673
|
+
\`\`\`
|
|
4674
|
+
x-vaif-key: vk_xxx \u2192 Data-plane (CRUD, storage, functions)
|
|
4675
|
+
Authorization: Bearer <jwt> \u2192 Control-plane (schema, project management)
|
|
4676
|
+
\`\`\`
|
|
4677
|
+
|
|
4678
|
+
## Environment Variables
|
|
4679
|
+
|
|
4680
|
+
\`\`\`bash
|
|
4681
|
+
VAIF_API_URL=https://api.vaif.studio
|
|
4682
|
+
VAIF_PROJECT_ID=proj_xxx
|
|
4683
|
+
VAIF_API_KEY=vk_xxx
|
|
4684
|
+
STRIPE_SECRET_KEY=sk_live_xxx
|
|
4685
|
+
STRIPE_WEBHOOK_SECRET=whsec_xxx
|
|
4686
|
+
\`\`\`
|
|
4687
|
+
`,mobile:`# VAIF Studio \u2014 Mobile App Backend
|
|
4688
|
+
|
|
4689
|
+
This project uses **VAIF Studio** as its backend for a mobile application (React Native/Expo, Flutter, or Swift).
|
|
4690
|
+
|
|
4691
|
+
## SDK Setup
|
|
4692
|
+
|
|
4693
|
+
### React Native / Expo
|
|
4694
|
+
\`\`\`bash
|
|
4695
|
+
npm install @vaiftech/sdk-expo
|
|
4696
|
+
\`\`\`
|
|
4697
|
+
|
|
4698
|
+
\`\`\`typescript
|
|
4699
|
+
import { VaifProvider, useVaif } from "@vaiftech/sdk-expo";
|
|
4700
|
+
|
|
4701
|
+
export default function App() {
|
|
4702
|
+
return (
|
|
4703
|
+
<VaifProvider
|
|
4704
|
+
config={{
|
|
4705
|
+
baseUrl: "https://api.vaif.studio",
|
|
4706
|
+
projectId: "proj_xxx",
|
|
4707
|
+
apiKey: "vk_xxx",
|
|
4708
|
+
}}
|
|
4709
|
+
>
|
|
4710
|
+
<MainApp />
|
|
4711
|
+
</VaifProvider>
|
|
4712
|
+
);
|
|
4713
|
+
}
|
|
4714
|
+
\`\`\`
|
|
4715
|
+
|
|
4716
|
+
### Flutter / Dart
|
|
4717
|
+
\`\`\`yaml
|
|
4718
|
+
# pubspec.yaml
|
|
4719
|
+
dependencies:
|
|
4720
|
+
vaif_client: ^1.0.0
|
|
4721
|
+
\`\`\`
|
|
4722
|
+
|
|
4723
|
+
\`\`\`dart
|
|
4724
|
+
import 'package:vaif_client/vaif_client.dart';
|
|
4725
|
+
|
|
4726
|
+
final vaif = VaifClient(
|
|
4727
|
+
baseUrl: 'https://api.vaif.studio',
|
|
4728
|
+
projectId: 'proj_xxx',
|
|
4729
|
+
apiKey: 'vk_xxx',
|
|
4730
|
+
);
|
|
4731
|
+
\`\`\`
|
|
4732
|
+
|
|
4733
|
+
### Swift / iOS
|
|
4734
|
+
\`\`\`swift
|
|
4735
|
+
// Package.swift
|
|
4736
|
+
.package(url: "https://github.com/vaifllc/vaif-swift", from: "0.2.0")
|
|
4737
|
+
|
|
4738
|
+
import VaifClient
|
|
4739
|
+
|
|
4740
|
+
let vaif = VaifClient(
|
|
4741
|
+
baseUrl: "https://api.vaif.studio",
|
|
4742
|
+
projectId: "proj_xxx",
|
|
4743
|
+
apiKey: "vk_xxx"
|
|
4744
|
+
)
|
|
4745
|
+
\`\`\`
|
|
4746
|
+
|
|
4747
|
+
## Common Mobile Patterns
|
|
4748
|
+
|
|
4749
|
+
### User Profiles with Avatars
|
|
4750
|
+
|
|
4751
|
+
\`\`\`sql
|
|
4752
|
+
CREATE TABLE profiles (
|
|
4753
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
4754
|
+
user_id UUID UNIQUE NOT NULL,
|
|
4755
|
+
display_name TEXT,
|
|
4756
|
+
avatar_url TEXT,
|
|
4757
|
+
bio TEXT,
|
|
4758
|
+
push_token TEXT,
|
|
4759
|
+
device_type TEXT, -- ios, android
|
|
4760
|
+
last_seen TIMESTAMPTZ DEFAULT now()
|
|
4761
|
+
);
|
|
4762
|
+
\`\`\`
|
|
4763
|
+
|
|
4764
|
+
### Push Notifications
|
|
4765
|
+
|
|
4766
|
+
\`\`\`typescript
|
|
4767
|
+
// Store push tokens
|
|
4768
|
+
await vaif.from("profiles").update({
|
|
4769
|
+
push_token: expoPushToken,
|
|
4770
|
+
device_type: Platform.OS,
|
|
4771
|
+
}).eq("user_id", userId);
|
|
4772
|
+
|
|
4773
|
+
// Send notification (VAIF Function)
|
|
4774
|
+
export default async function handler(req, ctx) {
|
|
4775
|
+
const { userId, title, body } = req.body;
|
|
4776
|
+
const { data: profile } = await vaif.from("profiles")
|
|
4777
|
+
.select("push_token, device_type")
|
|
4778
|
+
.eq("user_id", userId)
|
|
4779
|
+
.single();
|
|
4780
|
+
|
|
4781
|
+
// Send via Expo Push API or APNs/FCM
|
|
4782
|
+
await fetch("https://exp.host/--/api/v2/push/send", {
|
|
4783
|
+
method: "POST",
|
|
4784
|
+
headers: { "Content-Type": "application/json" },
|
|
4785
|
+
body: JSON.stringify({
|
|
4786
|
+
to: profile.push_token,
|
|
4787
|
+
title, body,
|
|
4788
|
+
}),
|
|
4789
|
+
});
|
|
4790
|
+
return { sent: true };
|
|
4791
|
+
}
|
|
4792
|
+
\`\`\`
|
|
4793
|
+
|
|
4794
|
+
### Offline-First with Sync
|
|
4795
|
+
|
|
4796
|
+
\`\`\`typescript
|
|
4797
|
+
// Cache data locally, sync when online
|
|
4798
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
4799
|
+
|
|
4800
|
+
// Fetch and cache
|
|
4801
|
+
const { data } = await vaif.from("items").select();
|
|
4802
|
+
await AsyncStorage.setItem("items_cache", JSON.stringify(data));
|
|
4803
|
+
|
|
4804
|
+
// Read from cache when offline
|
|
4805
|
+
const cached = JSON.parse(await AsyncStorage.getItem("items_cache") || "[]");
|
|
4806
|
+
\`\`\`
|
|
4807
|
+
|
|
4808
|
+
### Image Upload from Camera
|
|
4809
|
+
|
|
4810
|
+
\`\`\`typescript
|
|
4811
|
+
import * as ImagePicker from "expo-image-picker";
|
|
4812
|
+
|
|
4813
|
+
const result = await ImagePicker.launchCameraAsync({ quality: 0.8 });
|
|
4814
|
+
if (!result.canceled) {
|
|
4815
|
+
const uri = result.assets[0].uri;
|
|
4816
|
+
const response = await fetch(uri);
|
|
4817
|
+
const blob = await response.blob();
|
|
4818
|
+
|
|
4819
|
+
await vaif.storage.from("avatars").upload(
|
|
4820
|
+
\\\`\${userId}/avatar.jpg\\\`,
|
|
4821
|
+
blob,
|
|
4822
|
+
{ contentType: "image/jpeg" }
|
|
4823
|
+
);
|
|
4824
|
+
}
|
|
4825
|
+
\`\`\`
|
|
4826
|
+
|
|
4827
|
+
### Realtime Chat
|
|
4828
|
+
|
|
4829
|
+
\`\`\`typescript
|
|
4830
|
+
// Subscribe to new messages
|
|
4831
|
+
const subscription = vaif.realtime
|
|
4832
|
+
.channel("messages")
|
|
4833
|
+
.on("INSERT", (payload) => {
|
|
4834
|
+
setMessages(prev => [...prev, payload.new]);
|
|
4835
|
+
})
|
|
4836
|
+
.subscribe();
|
|
4837
|
+
|
|
4838
|
+
// Send a message
|
|
4839
|
+
await vaif.from("messages").insert({
|
|
4840
|
+
room_id: roomId,
|
|
4841
|
+
user_id: userId,
|
|
4842
|
+
content: messageText,
|
|
4843
|
+
});
|
|
4844
|
+
\`\`\`
|
|
4845
|
+
|
|
4846
|
+
## Auth with Secure Token Storage
|
|
4847
|
+
|
|
4848
|
+
The Expo SDK automatically stores auth tokens in SecureStore (iOS Keychain / Android Keystore).
|
|
4849
|
+
|
|
4850
|
+
\`\`\`typescript
|
|
4851
|
+
const { useAuth } = require("@vaiftech/sdk-expo");
|
|
4852
|
+
|
|
4853
|
+
function LoginScreen() {
|
|
4854
|
+
const { signIn, signUp, user, loading } = useAuth();
|
|
4855
|
+
|
|
4856
|
+
const handleLogin = async () => {
|
|
4857
|
+
const { error } = await signIn({ email, password });
|
|
4858
|
+
if (error) Alert.alert("Error", error.message);
|
|
4859
|
+
};
|
|
4860
|
+
}
|
|
4861
|
+
\`\`\`
|
|
4862
|
+
|
|
4863
|
+
## Environment Variables
|
|
4864
|
+
|
|
4865
|
+
\`\`\`bash
|
|
4866
|
+
VAIF_API_URL=https://api.vaif.studio
|
|
4867
|
+
VAIF_PROJECT_ID=proj_xxx
|
|
4868
|
+
VAIF_API_KEY=vk_xxx
|
|
4869
|
+
EXPO_PUBLIC_VAIF_PROJECT_ID=proj_xxx
|
|
4870
|
+
EXPO_PUBLIC_VAIF_API_KEY=vk_xxx
|
|
4871
|
+
\`\`\`
|
|
4872
|
+
`,ecommerce:`# VAIF Studio \u2014 E-Commerce Backend
|
|
4873
|
+
|
|
4874
|
+
This project uses **VAIF Studio** as its backend for an e-commerce application.
|
|
4875
|
+
|
|
4876
|
+
## SDK Setup
|
|
4877
|
+
|
|
4878
|
+
\`\`\`bash
|
|
4879
|
+
npm install @vaiftech/client
|
|
4880
|
+
\`\`\`
|
|
4881
|
+
|
|
4882
|
+
\`\`\`typescript
|
|
4883
|
+
import { createVaifClient } from "@vaiftech/client";
|
|
4884
|
+
|
|
4885
|
+
const vaif = createVaifClient({
|
|
4886
|
+
baseUrl: "https://api.vaif.studio",
|
|
4887
|
+
projectId: process.env.VAIF_PROJECT_ID!,
|
|
4888
|
+
apiKey: process.env.VAIF_API_KEY!,
|
|
4889
|
+
});
|
|
4890
|
+
\`\`\`
|
|
4891
|
+
|
|
4892
|
+
## E-Commerce Schema
|
|
4893
|
+
|
|
4894
|
+
\`\`\`sql
|
|
4895
|
+
-- Products
|
|
4896
|
+
CREATE TABLE products (
|
|
4897
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
4898
|
+
name TEXT NOT NULL,
|
|
4899
|
+
slug TEXT UNIQUE NOT NULL,
|
|
4900
|
+
description TEXT,
|
|
4901
|
+
price NUMERIC(10,2) NOT NULL,
|
|
4902
|
+
compare_at_price NUMERIC(10,2),
|
|
4903
|
+
currency TEXT DEFAULT 'USD',
|
|
4904
|
+
sku TEXT UNIQUE,
|
|
4905
|
+
inventory_count INTEGER DEFAULT 0,
|
|
4906
|
+
category_id UUID REFERENCES categories(id),
|
|
4907
|
+
images JSONB DEFAULT '[]', -- [{url, alt, position}]
|
|
4908
|
+
metadata JSONB DEFAULT '{}',
|
|
4909
|
+
status TEXT DEFAULT 'draft', -- draft, active, archived
|
|
4910
|
+
created_at TIMESTAMPTZ DEFAULT now(),
|
|
4911
|
+
updated_at TIMESTAMPTZ DEFAULT now()
|
|
4912
|
+
);
|
|
4913
|
+
CREATE INDEX idx_products_category ON products(category_id);
|
|
4914
|
+
CREATE INDEX idx_products_status ON products(status);
|
|
4915
|
+
|
|
4916
|
+
-- Categories
|
|
4917
|
+
CREATE TABLE categories (
|
|
4918
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
4919
|
+
name TEXT NOT NULL,
|
|
4920
|
+
slug TEXT UNIQUE NOT NULL,
|
|
4921
|
+
parent_id UUID REFERENCES categories(id),
|
|
4922
|
+
sort_order INTEGER DEFAULT 0
|
|
4923
|
+
);
|
|
4924
|
+
|
|
4925
|
+
-- Orders
|
|
4926
|
+
CREATE TABLE orders (
|
|
4927
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
4928
|
+
user_id UUID NOT NULL,
|
|
4929
|
+
status TEXT DEFAULT 'pending', -- pending, confirmed, shipped, delivered, cancelled
|
|
4930
|
+
subtotal NUMERIC(10,2) NOT NULL,
|
|
4931
|
+
tax NUMERIC(10,2) DEFAULT 0,
|
|
4932
|
+
shipping NUMERIC(10,2) DEFAULT 0,
|
|
4933
|
+
total NUMERIC(10,2) NOT NULL,
|
|
4934
|
+
currency TEXT DEFAULT 'USD',
|
|
4935
|
+
shipping_address JSONB,
|
|
4936
|
+
billing_address JSONB,
|
|
4937
|
+
stripe_payment_intent_id TEXT,
|
|
4938
|
+
notes TEXT,
|
|
4939
|
+
created_at TIMESTAMPTZ DEFAULT now(),
|
|
4940
|
+
updated_at TIMESTAMPTZ DEFAULT now()
|
|
4941
|
+
);
|
|
4942
|
+
CREATE INDEX idx_orders_user ON orders(user_id);
|
|
4943
|
+
CREATE INDEX idx_orders_status ON orders(status);
|
|
4944
|
+
|
|
4945
|
+
-- Order items
|
|
4946
|
+
CREATE TABLE order_items (
|
|
4947
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
4948
|
+
order_id UUID REFERENCES orders(id) ON DELETE CASCADE,
|
|
4949
|
+
product_id UUID REFERENCES products(id),
|
|
4950
|
+
quantity INTEGER NOT NULL,
|
|
4951
|
+
unit_price NUMERIC(10,2) NOT NULL,
|
|
4952
|
+
total NUMERIC(10,2) NOT NULL
|
|
4953
|
+
);
|
|
4954
|
+
CREATE INDEX idx_order_items_order ON order_items(order_id);
|
|
4955
|
+
|
|
4956
|
+
-- Cart (session-based)
|
|
4957
|
+
CREATE TABLE carts (
|
|
4958
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
4959
|
+
user_id UUID,
|
|
4960
|
+
session_id TEXT,
|
|
4961
|
+
items JSONB DEFAULT '[]', -- [{productId, quantity, price}]
|
|
4962
|
+
created_at TIMESTAMPTZ DEFAULT now(),
|
|
4963
|
+
updated_at TIMESTAMPTZ DEFAULT now()
|
|
4964
|
+
);
|
|
4965
|
+
|
|
4966
|
+
-- Reviews
|
|
4967
|
+
CREATE TABLE reviews (
|
|
4968
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
4969
|
+
product_id UUID REFERENCES products(id) ON DELETE CASCADE,
|
|
4970
|
+
user_id UUID NOT NULL,
|
|
4971
|
+
rating INTEGER CHECK (rating >= 1 AND rating <= 5),
|
|
4972
|
+
title TEXT,
|
|
4973
|
+
body TEXT,
|
|
4974
|
+
created_at TIMESTAMPTZ DEFAULT now()
|
|
4975
|
+
);
|
|
4976
|
+
CREATE INDEX idx_reviews_product ON reviews(product_id);
|
|
4977
|
+
\`\`\`
|
|
4978
|
+
|
|
4979
|
+
## Common Patterns
|
|
4980
|
+
|
|
4981
|
+
### Product Listing with Filters
|
|
4982
|
+
|
|
4983
|
+
\`\`\`typescript
|
|
4984
|
+
// Browse products with category filter and pagination
|
|
4985
|
+
const { data: products } = await vaif
|
|
4986
|
+
.from("products")
|
|
4987
|
+
.select("id, name, slug, price, compare_at_price, images, category:categories(name)")
|
|
4988
|
+
.eq("status", "active")
|
|
4989
|
+
.eq("category_id", categoryId)
|
|
4990
|
+
.order("created_at", { ascending: false })
|
|
4991
|
+
.limit(20)
|
|
4992
|
+
.offset(page * 20);
|
|
4993
|
+
\`\`\`
|
|
4994
|
+
|
|
4995
|
+
### Cart Management
|
|
4996
|
+
|
|
4997
|
+
\`\`\`typescript
|
|
4998
|
+
// Add to cart
|
|
4999
|
+
const cart = await vaif.from("carts").select().eq("user_id", userId).single();
|
|
5000
|
+
const items = [...(cart.data?.items || [])];
|
|
5001
|
+
const existing = items.findIndex(i => i.productId === productId);
|
|
5002
|
+
if (existing >= 0) {
|
|
5003
|
+
items[existing].quantity += quantity;
|
|
5004
|
+
} else {
|
|
5005
|
+
items.push({ productId, quantity, price });
|
|
5006
|
+
}
|
|
5007
|
+
await vaif.from("carts").update({ items, updated_at: new Date().toISOString() }).eq("id", cart.data.id);
|
|
5008
|
+
\`\`\`
|
|
5009
|
+
|
|
5010
|
+
### Order Creation
|
|
5011
|
+
|
|
5012
|
+
\`\`\`typescript
|
|
5013
|
+
// Create order from cart
|
|
5014
|
+
const { data: order } = await vaif.from("orders").insert({
|
|
5015
|
+
user_id: userId,
|
|
5016
|
+
subtotal, tax, shipping,
|
|
5017
|
+
total: subtotal + tax + shipping,
|
|
5018
|
+
shipping_address: addressData,
|
|
5019
|
+
});
|
|
5020
|
+
|
|
5021
|
+
// Create order items
|
|
5022
|
+
const orderItems = cartItems.map(item => ({
|
|
5023
|
+
order_id: order.id,
|
|
5024
|
+
product_id: item.productId,
|
|
5025
|
+
quantity: item.quantity,
|
|
5026
|
+
unit_price: item.price,
|
|
5027
|
+
total: item.price * item.quantity,
|
|
5028
|
+
}));
|
|
5029
|
+
await vaif.from("order_items").insert(orderItems);
|
|
5030
|
+
\`\`\`
|
|
5031
|
+
|
|
5032
|
+
### Inventory Management
|
|
5033
|
+
|
|
5034
|
+
\`\`\`typescript
|
|
5035
|
+
// Decrement inventory on order (VAIF Function)
|
|
5036
|
+
export default async function handler(req, ctx) {
|
|
5037
|
+
const { items } = req.body; // [{productId, quantity}]
|
|
5038
|
+
for (const item of items) {
|
|
5039
|
+
const { data: product } = await vaif.from("products")
|
|
5040
|
+
.select("inventory_count")
|
|
5041
|
+
.eq("id", item.productId)
|
|
5042
|
+
.single();
|
|
5043
|
+
|
|
5044
|
+
if (product.inventory_count < item.quantity) {
|
|
5045
|
+
return { error: \\\`Insufficient stock for \${item.productId}\\\` };
|
|
5046
|
+
}
|
|
5047
|
+
|
|
5048
|
+
await vaif.from("products")
|
|
5049
|
+
.update({ inventory_count: product.inventory_count - item.quantity })
|
|
5050
|
+
.eq("id", item.productId);
|
|
5051
|
+
}
|
|
5052
|
+
return { success: true };
|
|
5053
|
+
}
|
|
5054
|
+
\`\`\`
|
|
5055
|
+
|
|
5056
|
+
### Payment Webhooks
|
|
5057
|
+
|
|
5058
|
+
\`\`\`typescript
|
|
5059
|
+
// Stripe webhook handler (VAIF Function)
|
|
5060
|
+
export default async function handler(req, ctx) {
|
|
5061
|
+
const sig = req.headers["stripe-signature"];
|
|
5062
|
+
const event = stripe.webhooks.constructEvent(req.rawBody, sig, ctx.secrets.STRIPE_WEBHOOK_SECRET);
|
|
5063
|
+
|
|
5064
|
+
switch (event.type) {
|
|
5065
|
+
case "payment_intent.succeeded":
|
|
5066
|
+
await vaif.from("orders")
|
|
5067
|
+
.update({ status: "confirmed" })
|
|
5068
|
+
.eq("stripe_payment_intent_id", event.data.object.id);
|
|
5069
|
+
break;
|
|
5070
|
+
case "payment_intent.payment_failed":
|
|
5071
|
+
await vaif.from("orders")
|
|
5072
|
+
.update({ status: "cancelled" })
|
|
5073
|
+
.eq("stripe_payment_intent_id", event.data.object.id);
|
|
5074
|
+
break;
|
|
5075
|
+
}
|
|
5076
|
+
return { received: true };
|
|
5077
|
+
}
|
|
5078
|
+
\`\`\`
|
|
5079
|
+
|
|
5080
|
+
### Product Image Upload
|
|
5081
|
+
|
|
5082
|
+
\`\`\`typescript
|
|
5083
|
+
// Upload product images to storage
|
|
5084
|
+
const { url } = await vaif.storage
|
|
5085
|
+
.from("product-images")
|
|
5086
|
+
.upload(\\\`\${productId}/\${fileName}\\\`, file, { contentType: "image/webp" });
|
|
5087
|
+
|
|
5088
|
+
// Update product images array
|
|
5089
|
+
const { data: product } = await vaif.from("products").select("images").eq("id", productId).single();
|
|
5090
|
+
const images = [...(product.images || []), { url, alt: altText, position: product.images.length }];
|
|
5091
|
+
await vaif.from("products").update({ images }).eq("id", productId);
|
|
5092
|
+
\`\`\`
|
|
5093
|
+
|
|
5094
|
+
## Environment Variables
|
|
5095
|
+
|
|
5096
|
+
\`\`\`bash
|
|
5097
|
+
VAIF_API_URL=https://api.vaif.studio
|
|
5098
|
+
VAIF_PROJECT_ID=proj_xxx
|
|
5099
|
+
VAIF_API_KEY=vk_xxx
|
|
5100
|
+
STRIPE_SECRET_KEY=sk_live_xxx
|
|
5101
|
+
STRIPE_PUBLISHABLE_KEY=pk_live_xxx
|
|
5102
|
+
STRIPE_WEBHOOK_SECRET=whsec_xxx
|
|
5103
|
+
\`\`\`
|
|
5104
|
+
|
|
5105
|
+
> **Note**: \`numeric\` columns (like \`price\`) serialize to JSON strings to preserve precision. Parse with \`parseFloat()\` before arithmetic.
|
|
5106
|
+
`};var Ve={$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 Ot(n){if(n.claude!==void 0){let t=typeof n.claude=="string"?n.claude:null;if(t&&t in k){let i=j("Generating CLAUDE.md from template...").start(),s=k[t],l=Oe();l&&(s+=`
|
|
5107
|
+
## Team Conventions
|
|
5108
|
+
|
|
5109
|
+
${l}
|
|
5110
|
+
`);let r=I.resolve("CLAUDE.md");v.existsSync(r)&&!n.force&&(i.fail("CLAUDE.md already exists"),console.log(u.yellow(`
|
|
5111
|
+
Use --force to overwrite.`)),process.exit(1)),v.writeFileSync(r,s,"utf-8"),i.succeed(`Created CLAUDE.md (${t} template)`),l&&console.log(u.gray(" Imported team conventions from existing config files.")),console.log(u.gray(`
|
|
5112
|
+
Customize the generated CLAUDE.md with your project details.`)),console.log(u.gray(` For a personalized version from live data, run: vaif claude-setup
|
|
5113
|
+
`));return}t&&!(t in k)&&(console.log(u.red(`
|
|
5114
|
+
Unknown template type: "${t}"`)),console.log(u.gray(" Available types: base, saas, mobile, ecommerce")),console.log(u.gray(` Or omit the type to auto-generate from your live project: vaif init --claude
|
|
5115
|
+
`)),process.exit(1));let o=je();o?(console.log(u.gray(`
|
|
5116
|
+
Detected project type: ${u.cyan(o)}`)),console.log(u.gray(` Generating from live project data with template context...
|
|
5117
|
+
`))):console.log(u.gray(`
|
|
5118
|
+
Generating from live project data...
|
|
5119
|
+
`)),await ne({skipMcp:false});return}if(n.addFeatures){n.template||(console.log(u.red(`
|
|
5120
|
+
--add-features requires --template to know which template to use.`)),console.log(u.gray("Example: vaif init --template react-spa --add-features functions,storage")),process.exit(1));let t=n.addFeatures.split(",").map(o=>o.trim());await q(n.template,{force:n.force,features:t,addOnly:true});return}let e=j("Initializing VAIF configuration...").start(),a=I.resolve("vaif.config.json");v.existsSync(a)&&!n.force&&(e.fail("vaif.config.json already exists"),console.log(u.yellow(`
|
|
5121
|
+
Use --force to overwrite existing configuration.`)),process.exit(1));try{if(v.writeFileSync(a,JSON.stringify(Ve,null,2),"utf-8"),e.succeed("Created vaif.config.json"),n.template){let t=n.features?n.features.split(",").map(o=>o.trim()):void 0;await q(n.template,{force:n.force,features:t});}else {let t=I.resolve(".env.example");if(v.existsSync(t)||(v.writeFileSync(t,`# VAIF Configuration
|
|
3786
5122
|
DATABASE_URL=postgresql://user:password@localhost:5432/database
|
|
3787
5123
|
VAIF_API_KEY=your-api-key
|
|
3788
|
-
`,"utf-8"),console.log(
|
|
3789
|
-
Error: ${
|
|
5124
|
+
`,"utf-8"),console.log(u.gray("Created .env.example"))),n.typescript){let o=I.resolve("src/types");v.existsSync(o)||(v.mkdirSync(o,{recursive:!0}),console.log(u.gray("Created src/types directory")));}console.log(""),console.log(u.green("VAIF initialized successfully!")),console.log(""),console.log(u.gray("Next steps:")),console.log(u.gray(" 1. Update vaif.config.json with your project ID")),console.log(u.gray(" 2. Set DATABASE_URL in your environment")),console.log(u.gray(" 3. Run: npx vaif generate")),console.log("");}}catch(t){e.fail("Failed to initialize"),t instanceof Error&&console.error(u.red(`
|
|
5125
|
+
Error: ${t.message}`)),process.exit(1);}}function je(){try{let n=I.resolve("package.json");if(!v.existsSync(n))return null;let e=JSON.parse(v.readFileSync(n,"utf-8")),a={...e.dependencies,...e.devDependencies};return a.expo||a["react-native"]||a["@vaiftech/sdk-expo"]||v.existsSync(I.resolve("pubspec.yaml"))||v.existsSync(I.resolve("Package.swift"))||v.existsSync(I.resolve("*.xcodeproj"))?"mobile":(a.stripe||a["@stripe/stripe-js"]||a["shopify-api-node"]||a["@shopify/shopify-api"])&&(v.existsSync(I.resolve("src/models/product.ts"))||v.existsSync(I.resolve("src/models/order.ts"))||v.existsSync(I.resolve("src/pages/products"))||v.existsSync(I.resolve("src/pages/cart")))?"ecommerce":a.stripe||a["@stripe/stripe-js"]||e.name?.includes("saas")||e.description?.toLowerCase().includes("saas")?"saas":"base"}catch{return null}}function Oe(){let n=[{path:".cursorrules",label:"Cursor Rules"},{path:".github/copilot-instructions.md",label:"GitHub Copilot Instructions"},{path:".windsurfrules",label:"Windsurf Rules"},{path:".clinerules",label:"Cline Rules"}],e=[];for(let a of n){let t=I.resolve(a.path);if(v.existsSync(t))try{let o=v.readFileSync(t,"utf-8").trim();o.length>0&&o.length<1e4&&e.push(`### From ${a.label} (\`${a.path}\`)
|
|
5126
|
+
|
|
5127
|
+
${o}`);}catch{}}return e.length>0?e.join(`
|
|
5128
|
+
|
|
5129
|
+
`):null}export{N as a,R as b,Qe as c,Ze as d,et as e,ut as f,vt as g,ne as h,Ot as i};
|