fallback-tools.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. import { getEntityDefinitionsSql } from '@supabase/pg-meta'
  2. import { tool } from 'ai'
  3. // import { processSql, renderBrivenJs } from '@supabase/sql-to-rest'
  4. import { IS_PLATFORM } from 'common'
  5. import { stripIndent } from 'common-tags'
  6. import { z } from 'zod'
  7. import { getDatabaseFunctions } from '@/data/database-functions/database-functions-query'
  8. import { getDatabasePolicies } from '@/data/database-policies/database-policies-query'
  9. import { executeSql } from '@/data/sql/execute-sql-query'
  10. import { executeQuery } from '@/lib/api/self-hosted/query'
  11. export const getFallbackTools = ({
  12. projectRef,
  13. connectionString,
  14. cookie,
  15. authorization,
  16. includeSchemaMetadata,
  17. }: {
  18. projectRef: string
  19. connectionString: string
  20. cookie?: string
  21. authorization?: string
  22. includeSchemaMetadata: boolean
  23. }) => {
  24. const headers = {
  25. 'Content-Type': 'application/json',
  26. ...(cookie && { cookie }),
  27. ...(authorization && { Authorization: authorization }),
  28. }
  29. return {
  30. getSchemaTables: tool({
  31. description: 'Get more information about one or more schemas',
  32. inputSchema: z.object({
  33. schemas: z.array(z.string()).describe('The schema names to get the definitions for'),
  34. }),
  35. execute: async ({ schemas }) => {
  36. try {
  37. const { result } = includeSchemaMetadata
  38. ? await executeSql(
  39. {
  40. projectRef,
  41. connectionString,
  42. sql: getEntityDefinitionsSql({ schemas }),
  43. },
  44. undefined,
  45. headers,
  46. IS_PLATFORM ? undefined : executeQuery
  47. )
  48. : { result: [] }
  49. return result
  50. } catch (error) {
  51. console.error('Failed to execute SQL:', error)
  52. return `Failed to fetch schema: ${error}`
  53. }
  54. },
  55. }),
  56. // [Ivan] The tool relies on `libpg-query` binaries which we've removed intentionally because they couldn't build on MacOS.
  57. // Once we figure out a way how to use wasm binaries, we can add it back.
  58. //
  59. // convertSqlToBrivenJs: tool({
  60. // description: 'Convert an sql query into briven-js client code',
  61. // parameters: z.object({
  62. // sql: z
  63. // .string()
  64. // .describe(
  65. // 'The sql statement to convert. Only a subset of statements are supported currently. '
  66. // ),
  67. // }),
  68. // execute: async ({ sql }) => {
  69. // try {
  70. // const statement = await processSql(sql)
  71. // const { code } = await renderBrivenJs(statement)
  72. // return code
  73. // } catch (error) {
  74. // return `Failed to convert SQL: ${error}`
  75. // }
  76. // },
  77. // }),
  78. getRlsKnowledge: tool({
  79. description:
  80. 'Get existing policies and examples and instructions on how to write RLS policies',
  81. inputSchema: z.object({
  82. schemas: z.array(z.string()).describe('The schema names to get the policies for'),
  83. }),
  84. execute: async ({ schemas }) => {
  85. const data = includeSchemaMetadata
  86. ? await getDatabasePolicies(
  87. {
  88. projectRef,
  89. connectionString,
  90. schema: schemas?.join(','),
  91. },
  92. undefined,
  93. headers
  94. )
  95. : []
  96. const formattedPolicies = data
  97. .map(
  98. (policy) => `
  99. Policy Name: "${policy.name}"
  100. Action: ${policy.action}
  101. Roles: ${policy.roles.join(', ')}
  102. Command: ${policy.command}
  103. Definition: ${policy.definition}
  104. ${policy.check ? `Check: ${policy.check}` : ''}
  105. `
  106. )
  107. .join('\n')
  108. return stripIndent`
  109. You're a Briven Postgres expert in writing row level security policies. Your purpose is to
  110. generate a policy with the constraints given by the user. You should first retrieve schema information to write policies for, usually the 'public' schema.
  111. The output should use the following instructions:
  112. - The generated SQL must be valid SQL.
  113. - You can use only CREATE POLICY or ALTER POLICY queries, no other queries are allowed.
  114. - Always use double apostrophe in SQL strings (eg. 'Night''s watch')
  115. - You can add short explanations to your messages.
  116. - The result should be a valid markdown. The SQL code should be wrapped in \`\`\` (including sql language tag).
  117. - Always use "auth.uid()" instead of "current_user".
  118. - SELECT policies should always have USING but not WITH CHECK
  119. - INSERT policies should always have WITH CHECK but not USING
  120. - UPDATE policies should always have WITH CHECK and most often have USING
  121. - DELETE policies should always have USING but not WITH CHECK
  122. - Don't use \`FOR ALL\`. Instead separate into 4 separate policies for select, insert, update, and delete.
  123. - The policy name should be short but detailed text explaining the policy, enclosed in double quotes.
  124. - Always put explanations as separate text. Never use inline SQL comments.
  125. - If the user asks for something that's not related to SQL policies, explain to the user
  126. that you can only help with policies.
  127. - Discourage \`RESTRICTIVE\` policies and encourage \`PERMISSIVE\` policies, and explain why.
  128. The output should look like this:
  129. \`\`\`sql
  130. CREATE POLICY "My descriptive policy." ON books FOR INSERT to authenticated USING ( (select auth.uid()) = author_id ) WITH ( true );
  131. \`\`\`
  132. Since you are running in a Briven environment, take note of these Briven-specific additions:
  133. ## Authenticated and unauthenticated roles
  134. Briven maps every request to one of the roles:
  135. - \`anon\`: an unauthenticated request (the user is not logged in)
  136. - \`authenticated\`: an authenticated request (the user is logged in)
  137. These are actually [Postgres Roles](/docs/guides/database/postgres/roles). You can use these roles within your Policies using the \`TO\` clause:
  138. \`\`\`sql
  139. create policy "Profiles are viewable by everyone"
  140. on profiles
  141. for select
  142. to authenticated, anon
  143. using ( true );
  144. -- OR
  145. create policy "Public profiles are viewable only by authenticated users"
  146. on profiles
  147. for select
  148. to authenticated
  149. using ( true );
  150. \`\`\`
  151. Note that \`for ...\` must be added after the table but before the roles. \`to ...\` must be added after \`for ...\`:
  152. ### Incorrect
  153. \`\`\`sql
  154. create policy "Public profiles are viewable only by authenticated users"
  155. on profiles
  156. to authenticated
  157. for select
  158. using ( true );
  159. \`\`\`
  160. ### Correct
  161. \`\`\`sql
  162. create policy "Public profiles are viewable only by authenticated users"
  163. on profiles
  164. for select
  165. to authenticated
  166. using ( true );
  167. \`\`\`
  168. ## Multiple operations
  169. PostgreSQL policies do not support specifying multiple operations in a single FOR clause. You need to create separate policies for each operation.
  170. ### Incorrect
  171. \`\`\`sql
  172. create policy "Profiles can be created and deleted by any user"
  173. on profiles
  174. for insert, delete -- cannot create a policy on multiple operators
  175. to authenticated
  176. with check ( true )
  177. using ( true );
  178. \`\`\`
  179. ### Correct
  180. \`\`\`sql
  181. create policy "Profiles can be created by any user"
  182. on profiles
  183. for insert
  184. to authenticated
  185. with check ( true );
  186. create policy "Profiles can be deleted by any user"
  187. on profiles
  188. for delete
  189. to authenticated
  190. using ( true );
  191. \`\`\`
  192. ## Helper functions
  193. Briven provides some helper functions that make it easier to write Policies.
  194. ### \`auth.uid()\`
  195. Returns the ID of the user making the request.
  196. ### \`auth.jwt()\`
  197. Returns the JWT of the user making the request. Anything that you store in the user's \`raw_app_meta_data\` column or the \`raw_user_meta_data\` column will be accessible using this function. It's important to know the distinction between these two:
  198. - \`raw_user_meta_data\` - can be updated by the authenticated user using the \`briven.auth.update()\` function. It is not a good place to store authorization data.
  199. - \`raw_app_meta_data\` - cannot be updated by the user, so it's a good place to store authorization data.
  200. The \`auth.jwt()\` function is extremely versatile. For example, if you store some team data inside \`app_metadata\`, you can use it to determine whether a particular user belongs to a team. For example, if this was an array of IDs:
  201. \`\`\`sql
  202. create policy "User is in team"
  203. on my_table
  204. to authenticated
  205. using ( team_id in (select auth.jwt() -> 'app_metadata' -> 'teams'));
  206. \`\`\`
  207. ### MFA
  208. The \`auth.jwt()\` function can be used to check for [Multi-Factor Authentication](/docs/guides/auth/auth-mfa#enforce-rules-for-mfa-logins). For example, you could restrict a user from updating their profile unless they have at least 2 levels of authentication (Assurance Level 2):
  209. \`\`\`sql
  210. create policy "Restrict updates."
  211. on profiles
  212. as restrictive
  213. for update
  214. to authenticated using (
  215. (select auth.jwt()->>'aal') = 'aal2'
  216. );
  217. \`\`\`
  218. ## RLS performance recommendations
  219. Every authorization system has an impact on performance. While row level security is powerful, the performance impact is important to keep in mind. This is especially true for queries that scan every row in a table - like many \`select\` operations, including those using limit, offset, and ordering.
  220. Based on a series of [tests](https://github.com/GaryAustin1/RLS-Performance), we have a few recommendations for RLS:
  221. ### Add indexes
  222. Make sure you've added [indexes](/docs/guides/database/postgres/indexes) on any columns used within the Policies which are not already indexed (or primary keys). For a Policy like this:
  223. \`\`\`sql
  224. create policy "Users can access their own records" on test_table
  225. to authenticated
  226. using ( (select auth.uid()) = user_id );
  227. \`\`\`
  228. You can add an index like:
  229. \`\`\`sql
  230. create index userid
  231. on test_table
  232. using btree (user_id);
  233. \`\`\`
  234. ### Call functions with \`select\`
  235. You can use \`select\` statement to improve policies that use functions. For example, instead of this:
  236. \`\`\`sql
  237. create policy "Users can access their own records" on test_table
  238. to authenticated
  239. using ( auth.uid() = user_id );
  240. \`\`\`
  241. You can do:
  242. \`\`\`sql
  243. create policy "Users can access their own records" on test_table
  244. to authenticated
  245. using ( (select auth.uid()) = user_id );
  246. \`\`\`
  247. This method works well for JWT functions like \`auth.uid()\` and \`auth.jwt()\` as well as \`security definer\` Functions. Wrapping the function causes an \`initPlan\` to be run by the Postgres optimizer, which allows it to "cache" the results per-statement, rather than calling the function on each row.
  248. Caution: You can only use this technique if the results of the query or function do not change based on the row data.
  249. ### Minimize joins
  250. You can often rewrite your Policies to avoid joins between the source and the target table. Instead, try to organize your policy to fetch all the relevant data from the target table into an array or set, then you can use an \`IN\` or \`ANY\` operation in your filter.
  251. For example, this is an example of a slow policy which joins the source \`test_table\` to the target \`team_user\`:
  252. \`\`\`sql
  253. create policy "Users can access records belonging to their teams" on test_table
  254. to authenticated
  255. using (
  256. (select auth.uid()) in (
  257. select user_id
  258. from team_user
  259. where team_user.team_id = team_id -- joins to the source "test_table.team_id"
  260. )
  261. );
  262. \`\`\`
  263. We can rewrite this to avoid this join, and instead select the filter criteria into a set:
  264. \`\`\`sql
  265. create policy "Users can access records belonging to their teams" on test_table
  266. to authenticated
  267. using (
  268. team_id in (
  269. select team_id
  270. from team_user
  271. where user_id = (select auth.uid()) -- no join
  272. )
  273. );
  274. \`\`\`
  275. ### Specify roles in your policies
  276. Always use the Role of inside your policies, specified by the \`TO\` operator. For example, instead of this query:
  277. \`\`\`sql
  278. create policy "Users can access their own records" on rls_test
  279. using ( auth.uid() = user_id );
  280. \`\`\`
  281. Use:
  282. \`\`\`sql
  283. create policy "Users can access their own records" on rls_test
  284. to authenticated
  285. using ( (select auth.uid()) = user_id );
  286. \`\`\`
  287. This prevents the policy \`( (select auth.uid()) = user_id )\` from running for any \`anon\` users, since the execution stops at the \`to authenticated\` step.
  288. ${data.length > 0 ? `Here are my existing policies: ${formattedPolicies}` : ''}
  289. `
  290. },
  291. }),
  292. getFunctions: tool({
  293. description: 'Get database functions for one or more schemas',
  294. inputSchema: z.object({
  295. schemas: z.array(z.string()).describe('The schema names to get the functions for'),
  296. }),
  297. execute: async ({ schemas }) => {
  298. try {
  299. const data = includeSchemaMetadata
  300. ? await getDatabaseFunctions(
  301. {
  302. projectRef,
  303. connectionString,
  304. },
  305. undefined,
  306. headers
  307. )
  308. : []
  309. const dataArray = Array.isArray(data) ? data : []
  310. // Filter functions by requested schemas
  311. const filteredFunctions = dataArray.filter((func) => schemas.includes(func.schema))
  312. const formattedFunctions = filteredFunctions
  313. .map(
  314. (func) => `
  315. Function Name: "${func.name}"
  316. Schema: ${func.schema}
  317. Arguments: ${func.argument_types}
  318. Return Type: ${func.return_type}
  319. Language: ${func.language}
  320. Definition: ${func.definition}
  321. `
  322. )
  323. .join('\n')
  324. return formattedFunctions
  325. } catch (error) {
  326. console.error('Failed to fetch functions:', error)
  327. return `Failed to fetch functions: ${error}`
  328. }
  329. },
  330. }),
  331. getEdgeFunctionKnowledge: tool({
  332. description: 'Get knowledge about how to write edge functions for Briven',
  333. inputSchema: z.object({}),
  334. execute: async ({}) => {
  335. return stripIndent`
  336. # Writing Briven Edge Functions
  337. You're an expert in writing TypeScript and Deno JavaScript runtime. Generate **high-quality Briven Edge Functions** that adhere to the following best practices:
  338. ## Guidelines
  339. 1. Try to use Web APIs and Deno's core APIs instead of external dependencies (eg: use fetch instead of Axios, use WebSockets API instead of node-ws)
  340. 2. Do NOT use bare specifiers when importing dependencies. If you need to use an external dependency, make sure it's prefixed with either \`npm:\` or \`jsr:\`. For example, \`@supabase/supabase-js\` should be written as \`npm:@supabase/supabase-js\`.
  341. 3. For external imports, always define a version. For example, \`npm:@express\` should be written as \`npm:express@4.18.2\`.
  342. 4. For external dependencies, importing via \`npm:\` and \`jsr:\` is preferred. Minimize the use of imports from @\`deno.land/x\` , \`esm.sh\` and @\`unpkg.com\` . If you have a package from one of those CDNs, you can replace the CDN hostname with \`npm:\` specifier.
  343. 5. You can also use Node built-in APIs. You will need to import them using \`node:\` specifier. For example, to import Node process: \`import process from "node:process"\`. Use Node APIs when you find gaps in Deno APIs.
  344. 6. Do NOT use \`import { serve } from "https://deno.land/std@0.168.0/http/server.ts"\`. Instead use the built-in \`Deno.serve\`.
  345. 7. Following environment variables (ie. secrets) are pre-populated in both local and hosted Briven environments. Users don't need to manually set them:
  346. * BRIVEN_URL
  347. * BRIVEN_ANON_KEY
  348. * BRIVEN_SERVICE_ROLE_KEY
  349. * BRIVEN_DB_URL
  350. 8. To set other environment variables the user can go to project settings then edge functions to set them
  351. 9. A single Edge Function can handle multiple routes. It is recommended to use a library like Express or Hono to handle the routes as it's easier for developer to understand and maintain. Each route must be prefixed with \`/function-name\` so they are routed correctly.
  352. 10. File write operations are ONLY permitted on \`/tmp\` directory. You can use either Deno or Node File APIs.
  353. 11. Use \`EdgeRuntime.waitUntil(promise)\` static method to run long-running tasks in the background without blocking response to a request. Do NOT assume it is available in the request / execution context.
  354. ## Example Templates
  355. ### Simple Hello World Function
  356. \`\`\`edge
  357. // Setup type definitions for built-in Briven Runtime APIs
  358. import "jsr:@supabase/functions-js/edge-runtime.d.ts";
  359. interface reqPayload {
  360. name: string;
  361. }
  362. console.info('server started');
  363. Deno.serve(async (req: Request) => {
  364. const { name }: reqPayload = await req.json();
  365. const data = {
  366. message: \`Hello \${name} from foo!\`,
  367. };
  368. return new Response(
  369. JSON.stringify(data),
  370. { headers: { 'Content-Type': 'application/json', 'Connection': 'keep-alive' }}
  371. );
  372. });
  373. \`\`\`
  374. ### Example Function using Node built-in API
  375. \`\`\`edge
  376. // Setup type definitions for built-in Briven Runtime APIs
  377. import "jsr:@supabase/functions-js/edge-runtime.d.ts";
  378. import { randomBytes } from "node:crypto";
  379. import { createServer } from "node:http";
  380. import process from "node:process";
  381. const generateRandomString = (length) => {
  382. const buffer = randomBytes(length);
  383. return buffer.toString('hex');
  384. };
  385. const randomString = generateRandomString(10);
  386. console.log(randomString);
  387. const server = createServer((req, res) => {
  388. const message = \`Hello\`;
  389. res.end(message);
  390. });
  391. server.listen(9999);
  392. \`\`\`
  393. ### Using npm packages in Functions
  394. \`\`\`edge
  395. // Setup type definitions for built-in Briven Runtime APIs
  396. import "jsr:@supabase/functions-js/edge-runtime.d.ts";
  397. import express from "npm:express@4.18.2";
  398. const app = express();
  399. app.get(/(.*)/, (req, res) => {
  400. res.send("Welcome to Briven");
  401. });
  402. app.listen(8000);
  403. \`\`\`
  404. ### Generate embeddings using built-in @Briven.ai API
  405. \`\`\`edge
  406. // Setup type definitions for built-in Briven Runtime APIs
  407. import "jsr:@supabase/functions-js/edge-runtime.d.ts";
  408. const model = new Briven.ai.Session('gte-small');
  409. Deno.serve(async (req: Request) => {
  410. const params = new URL(req.url).searchParams;
  411. const input = params.get('text');
  412. const output = await model.run(input, { mean_pool: true, normalize: true });
  413. return new Response(
  414. JSON.stringify(output),
  415. {
  416. headers: {
  417. 'Content-Type': 'application/json',
  418. 'Connection': 'keep-alive',
  419. },
  420. },
  421. );
  422. });
  423. \`\`\`
  424. ## Integrating with Briven Auth
  425. \`\`\`edge
  426. // Setup type definitions for built-in Briven Runtime APIs
  427. import "jsr:@supabase/functions-js/edge-runtime.d.ts";
  428. import { createClient } from \\'jsr:@supabase/supabase-js@2\\'
  429. import { corsHeaders } from \\'../_shared/cors.ts\\'
  430. console.log(\`Function "select-from-table-with-auth-rls" up and running!\`)
  431. Deno.serve(async (req: Request) => {
  432. // This is needed if you\\'re planning to invoke your function from a browser.
  433. if (req.method === \\'OPTIONS\\') {
  434. return new Response(\\'ok\\', { headers: corsHeaders })
  435. }
  436. try {
  437. // Create a Briven client with the Auth context of the logged in user.
  438. const brivenClient = createClient(
  439. // Briven API URL - env var exported by default.
  440. Deno.env.get('BRIVEN_URL')!,
  441. // Briven API ANON KEY - env var exported by default.
  442. Deno.env.get('BRIVEN_ANON_KEY')!,
  443. // Create client with Auth context of the user that called the function.
  444. // This way your row-level-security (RLS) policies are applied.
  445. {
  446. global: {
  447. headers: { Authorization: req.headers.get(\\'Authorization\\')! },
  448. },
  449. }
  450. )
  451. // First get the token from the Authorization header
  452. const token = req.headers.get(\\'Authorization\\').replace(\\'Bearer \\', \\'\\')
  453. // Now we can get the session or user object
  454. const {
  455. data: { user },
  456. } = await brivenClient.auth.getUser(token)
  457. // And we can run queries in the context of our authenticated user
  458. const { data, error } = await brivenClient.from(\\'users\\').select(\\'*\\')
  459. if (error) throw error
  460. return new Response(JSON.stringify({ user, data }), {
  461. headers: { ...corsHeaders, \\'Content-Type\\': \\'application/json\\' },
  462. status: 200,
  463. })
  464. } catch (error) {
  465. return new Response(JSON.stringify({ error: error.message }), {
  466. headers: { ...corsHeaders, \\'Content-Type\\': \\'application/json\\' },
  467. status: 400,
  468. })
  469. }
  470. })
  471. // To invoke:
  472. // curl -i --location --request POST \\'http://localhost:54321/functions/v1/select-from-table-with-auth-rls\\' \\
  473. // --header \\'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs\\' \\
  474. // --header \\'Content-Type: application/json\\' \\
  475. // --data \\'{"name":"Functions"}\\'
  476. \`\`\`
  477. `
  478. },
  479. }),
  480. }
  481. }