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