diff --git a/docs/pages/getting-started/adapters/dgraph.mdx b/docs/pages/getting-started/adapters/dgraph.mdx index aee857758e..0d86f3dba6 100644 --- a/docs/pages/getting-started/adapters/dgraph.mdx +++ b/docs/pages/getting-started/adapters/dgraph.mdx @@ -92,6 +92,15 @@ app.use( +### Schema + + + Note that this adapter is designed so that it uses Dgraph internal ID's, if + you are interested in using external id's you should modify your schema `id: + ID` to `id: String @id` for instance and modify the adapter + methods(createUser, linkAccount...). + + ### Unsecure Schema The quickest way to use Dgraph is by applying the unsecure schema to your [local](https://dgraph.io/docs/graphql/admin/#modifying-a-schema) Dgraph instance or if using Dgraph [cloud](https://dgraph.io/docs/cloud/cloud-quick-start/#the-schema) you can paste the schema in the codebox to update. @@ -235,7 +244,7 @@ type VerificationToken expires: DateTime } -# Dgraph.Authorization {"VerificationKey":"","Header":"","Namespace":"","Algo":"HS256"} +# Dgraph.Authorization {"VerificationKey":"","Header":"","Namespace":"","Algo":"HS512"} ``` ### Dgraph.Authorization @@ -244,7 +253,7 @@ In order to secure your graphql backend define the `Dgraph.Authorization` object bottom of your schema and provide `authHeader` and `jwtSecret` values to the DgraphClient. ```js -# Dgraph.Authorization {"VerificationKey":"","Header":"","Namespace":"YOUR CUSTOM NAMESPACE HERE","Algo":"HS256"} +# Dgraph.Authorization {"VerificationKey":"","Header":"","Namespace":"YOUR CUSTOM NAMESPACE HERE","Algo":"HS512"} ``` ### VerificationKey and jwtSecret @@ -276,31 +285,4 @@ type VerificationRequest ### JWT session and `@auth` directive -Dgraph only works with HS256 or RS256 algorithms. If you want to use session jwt to securely interact with your dgraph -database you must customize next-auth `encode` and `decode` functions, as the default algorithm is HS512. You can -further customize the jwt with roles if you want to implement [`RBAC logic`](https://dgraph.io/docs/graphql/authorization/directive/#role-based-access-control). - -```js filename="./auth.js" -import NextAuth from "next-auth" -import * as jwt from "jsonwebtoken" - -export const { handlers, auth, signIn, signOut } = NextAuth({ - session: { - strategy: "jwt", - }, - jwt: { - secret: process.env.SECRET, - encode: async ({ secret, token }) => { - return jwt.sign({ ...token, userId: token.id }, secret, { - algorithm: "HS256", - expiresIn: 30 * 24 * 60 * 60, // 30 days - }) - }, - decode: async ({ secret, token }) => { - return jwt.verify(token, secret, { algorithms: ["HS256"] }) - }, - }, -}) -``` - -Once your `Dgraph.Authorization` is defined in your schema and the JWT settings are set, this will allow you to define [`@auth rules`](https://dgraph.io/docs/graphql/authorization/authorization-overview/) for every part of your schema. +Once your `Dgraph.Authorization` is defined in your schema and the JWT settings are set, this will allow you to define [`@auth rules`](https://dgraph.io/docs/graphql/schema/directives/auth/) for every part of your schema. diff --git a/packages/adapter-dgraph/src/index.ts b/packages/adapter-dgraph/src/index.ts index 677a16c527..deee52860a 100644 --- a/packages/adapter-dgraph/src/index.ts +++ b/packages/adapter-dgraph/src/index.ts @@ -15,10 +15,10 @@ * @module @auth/dgraph-adapter */ import { client as dgraphClient } from "./lib/client.js" -import { isDate, type Adapter } from "@auth/core/adapters" import type { DgraphClientParams } from "./lib/client.js" import * as defaultFragments from "./lib/graphql/fragments.js" -import { +import type { + Adapter, AdapterAccount, AdapterSession, AdapterUser, @@ -49,11 +49,13 @@ export function DgraphAdapter( options?: DgraphAdapterOptions ): Adapter { const c = dgraphClient(client) - const fragments = { ...defaultFragments, ...options?.fragments } + return { - async createUser(input: AdapterUser) { - const result = await c.run<{ user: any[] }>( + async createUser(user: AdapterUser) { + // Remove the id from the user object so that it can be generated by Dgraph + const { id, ...userWithoutId } = user + const result = await c.run<{ user: AdapterUser[] }>( /* GraphQL */ ` mutation ($input: [AddUserInput!]!) { addUser(input: $input) { @@ -64,13 +66,18 @@ export function DgraphAdapter( } ${fragments.User} `, - { input } + { input: [userWithoutId] } ) - return format.from(result?.user[0]) + if (!result || !result.user) { + throw new Error("Failed to create user") + } + + return result.user[0] }, + async getUser(id: string) { - const result = await c.run( + return await c.run( /* GraphQL */ ` query ($id: ID!) { getUser(id: $id) { @@ -81,13 +88,12 @@ export function DgraphAdapter( `, { id } ) - - return format.from(result) }, + async getUserByEmail(email: string) { - const [user] = await c.run( + const result = await c.run<{ user: AdapterUser[] }>( /* GraphQL */ ` - query ($email: String = "") { + query ($email: String!) { queryUser(filter: { email: { eq: $email } }) { ...UserFragment } @@ -96,40 +102,45 @@ export function DgraphAdapter( `, { email } ) - return format.from(user) + return result?.user?.[0] || null }, - async getUserByAccount(provider_providerAccountId: { + + async getUserByAccount({ + provider, + providerAccountId, + }: { provider: string providerAccountId: string }) { - const [account] = await c.run( + const result = await c.run<{ user: AdapterUser }[]>( /* GraphQL */ ` - query ($providerAccountId: String = "", $provider: String = "") { + query ($providerAccountId: String!, $provider: String!) { queryAccount( filter: { - and: { - providerAccountId: { eq: $providerAccountId } - provider: { eq: $provider } - } + and: [ + { providerAccountId: { eq: $providerAccountId } } + { provider: { eq: $provider } } + ] } ) { user { ...UserFragment } - id } } ${fragments.User} `, - provider_providerAccountId + { providerAccountId, provider } ) - return format.from(account?.user) + return result?.[0]?.user || null }, - async updateUser({ id, ...input }: { id: string }) { - const result = await c.run( + + async updateUser(user: Partial & { id: string }) { + const { id, ...update } = user + const result = await c.run<{ user: AdapterUser[] }>( /* GraphQL */ ` - mutation ($id: [ID!] = "", $input: UserPatch) { - updateUser(input: { filter: { id: $id }, set: $input }) { + mutation ($id: ID!, $input: UserPatch!) { + updateUser(input: { filter: { id: [$id] }, set: $input }) { user { ...UserFragment } @@ -137,55 +148,92 @@ export function DgraphAdapter( } ${fragments.User} `, - { id, input } + { id, input: update } ) - return format.from(result.user[0]) + + if (!result?.user?.[0]) { + throw new Error("Failed to update user") + } + + return result.user[0] }, - async deleteUser(id: string) { - const result = await c.run( + + async deleteUser(userId: string) { + const fetchResult = await c.run<{ + user: (AdapterUser & { + accounts: { id: string }[] + sessions: { sessionToken: string }[] + })[] + }>( /* GraphQL */ ` - mutation ($id: [ID!] = "") { - deleteUser(filter: { id: $id }) { - numUids - user { - accounts { - id - } - sessions { - id - } + query ($userId: ID!) { + getUser(id: $userId) { + ...UserFragment + accounts { + id + } + sessions { + sessionToken } } } + ${fragments.User} `, - { id } + { userId } ) - const deletedUser = format.from(result.user[0]) + const user = fetchResult?.user?.[0] + if (!user) { + return null // User not found + } - await c.run( + const deleteResult = await c.run<{ user: AdapterUser[] }>( /* GraphQL */ ` - mutation ($accounts: [ID!], $sessions: [ID!]) { - deleteAccount(filter: { id: $accounts }) { - numUids - } - deleteSession(filter: { id: $sessions }) { - numUids + mutation ($userId: [ID!]!) { + deleteUser(filter: { id: $userId }) { + user { + ...UserFragment + } } } + ${fragments.User} `, - { - sessions: deletedUser.sessions.map((x: any) => x.id), - accounts: deletedUser.accounts.map((x: any) => x.id), - } + { userId: [userId] } ) + const deletedUser = deleteResult?.user?.[0] + if (!deletedUser) { + throw new Error("Failed to delete user") + } + + if (user.accounts.length > 0 || user.sessions.length > 0) { + await c.run<{ + deleteAccount: { numUids: number } + deleteSession: { numUids: number } + }>( + /* GraphQL */ ` + mutation ($accountIds: [ID!], $sessionTokens: [String!]) { + deleteAccount(filter: { id: $accountIds }) { + numUids + } + deleteSession(filter: { sessionToken: $sessionTokens }) { + numUids + } + } + `, + { + accountIds: user.accounts.map((x) => x.id), + sessionTokens: user.sessions.map((x) => x.sessionToken), + } + ) + } + return deletedUser }, - async linkAccount(data: AdapterAccount) { - const { userId, ...input } = data - await c.run( + async linkAccount(account: AdapterAccount) { + const { id, userId, ...inputWithoutId } = account + const result = await c.run<{ account: AdapterAccount[] }>( /* GraphQL */ ` mutation ($input: [AddAccountInput!]!) { addAccount(input: $input) { @@ -196,64 +244,84 @@ export function DgraphAdapter( } ${fragments.Account} `, - { input: { ...input, user: { id: userId } } } + { input: [{ ...inputWithoutId, user: { id: userId } }] } ) - return data + + if (!result?.account?.[0]) { + throw new Error("Failed to link account") + } + + return result.account[0] }, - async unlinkAccount(provider_providerAccountId: { - provider: string - providerAccountId: string - }) { - await c.run( + + async unlinkAccount( + account: Pick + ) { + const { provider, providerAccountId } = account + const result = await c.run<{ account: AdapterAccount[] }>( /* GraphQL */ ` - mutation ($providerAccountId: String = "", $provider: String = "") { + mutation ($providerAccountId: String!, $provider: String!) { deleteAccount( filter: { - and: { - providerAccountId: { eq: $providerAccountId } - provider: { eq: $provider } - } + and: [ + { providerAccountId: { eq: $providerAccountId } } + { provider: { eq: $provider } } + ] } ) { - numUids + account { + ...AccountFragment + } } } + ${fragments.Account} `, - provider_providerAccountId + { providerAccountId, provider } ) + + return result?.account?.[0] }, async getSessionAndUser(sessionToken: string) { - const [sessionAndUser] = await c.run( + const result = await c.run<{ + getSessionAndUser: { + session: AdapterSession + user: AdapterUser + }[] + }>( /* GraphQL */ ` - query ($sessionToken: String = "") { - querySession(filter: { sessionToken: { eq: $sessionToken } }) { - ...SessionFragment + query GetSessionAndUser($sessionToken: String!) { + getSessionAndUser(filter: { sessionToken: { eq: $sessionToken } }) { + session { + ...SessionFragment + } user { ...UserFragment } } } - ${fragments.User} ${fragments.Session} + ${fragments.User} `, { sessionToken } ) - if (!sessionAndUser) return null - - const { user, ...session } = sessionAndUser - return { - user: format.from(user), - session: { ...format.from(session), userId: user.id }, + const sessionAndUser = result?.getSessionAndUser?.[0] + if (!sessionAndUser || !sessionAndUser.user || !sessionAndUser.session) { + return null } + + return sessionAndUser }, - async createSession(data: AdapterSession) { - const { userId, ...input } = data - await c.run( + async createSession({ sessionToken, userId, expires }: AdapterSession) { + if (userId === undefined) { + throw new Error("userId is undefined in createSession") + } + + const result = await c.run<{ session: AdapterSession[] }>( /* GraphQL */ ` - mutation ($input: [AddSessionInput!]!) { + mutation CreateSession($input: [AddSessionInput!]!) { addSession(input: $input) { session { ...SessionFragment @@ -262,73 +330,74 @@ export function DgraphAdapter( } ${fragments.Session} `, - { input: { ...input, user: { id: userId } } } + { input: [{ sessionToken, expires, user: { id: userId } }] } ) - return data as any + if (!result?.session?.[0]) { + throw new Error("Failed to create session") + } + + return result.session[0] }, - async updateSession({ sessionToken, ...input }: { sessionToken: string }) { - const result = await c.run( + + async deleteSession(sessionToken: string) { + const result = await c.run<{ session: AdapterSession[] }>( /* GraphQL */ ` - mutation ($input: SessionPatch = {}, $sessionToken: String) { - updateSession( - input: { - filter: { sessionToken: { eq: $sessionToken } } - set: $input - } - ) { + mutation DeleteSession($sessionToken: String!) { + deleteSession(filter: { sessionToken: { eq: $sessionToken } }) { session { ...SessionFragment - user { - id - } } } } ${fragments.Session} `, - { sessionToken, input } - ) - const session = format.from(result.session[0]) - - if (!session?.user?.id) return null - - return { ...session, userId: session.user.id } - }, - async deleteSession(sessionToken: string) { - await c.run( - /* GraphQL */ ` - mutation ($sessionToken: String = "") { - deleteSession(filter: { sessionToken: { eq: $sessionToken } }) { - numUids - } - } - `, { sessionToken } ) + + return result?.session?.[0] || null }, async createVerificationToken(input: VerificationToken) { - const result = await c.run( + const result = await c.run<{ + verificationToken: VerificationToken[] + }>( /* GraphQL */ ` - mutation ($input: [AddVerificationTokenInput!]!) { + mutation CreateVerificationToken( + $input: [AddVerificationTokenInput!]! + ) { addVerificationToken(input: $input) { - numUids + verificationToken { + identifier + token + expires + } } } `, - { input } + { input: [input] } ) - return format.from(result) + + const createdToken = result?.verificationToken?.[0] + if (!createdToken) { + throw new Error("Failed to create verification token") + } + + return createdToken }, async useVerificationToken(params: { identifier: string; token: string }) { - const result = await c.run( + const result = await c.run<{ + verificationToken: VerificationToken[] + }>( /* GraphQL */ ` - mutation ($token: String = "", $identifier: String = "") { + mutation UseVerificationToken($token: String!, $identifier: String!) { deleteVerificationToken( filter: { - and: { token: { eq: $token }, identifier: { eq: $identifier } } + and: [ + { token: { eq: $token } } + { identifier: { eq: $identifier } } + ] } ) { verificationToken { @@ -341,24 +410,7 @@ export function DgraphAdapter( params ) - return format.from(result.verificationToken[0]) + return result?.verificationToken?.[0] || null }, } } - -export const format = { - from(object?: Record): T | null { - const newObject: Record = {} - if (!object) return null - for (const key in object) { - const value = object[key] - if (isDate(value)) { - newObject[key] = new Date(value) - } else { - newObject[key] = value - } - } - - return newObject as T - }, -} diff --git a/packages/adapter-dgraph/src/lib/client.ts b/packages/adapter-dgraph/src/lib/client.ts index de0fd1b143..a9c366d50b 100644 --- a/packages/adapter-dgraph/src/lib/client.ts +++ b/packages/adapter-dgraph/src/lib/client.ts @@ -8,14 +8,19 @@ export interface DgraphClientParams { * [Dgraph Cloud Authentication](https://dgraph.io/docs/cloud/cloud-api/overview/#dgraph-cloud-authentication) */ authToken: string - /** [Using JWT and authorization claims](https://dgraph.io/docs/graphql/authorization/authorization-overview#using-jwts-and-authorization-claims) */ + /** + * [Using JWT and authorization claims](https://dgraph.io/docs/graphql/authorization/authorization-overview#using-jwts-and-authorization-claims) + */ jwtSecret?: string /** - * @default "RS256" + * @default "HS512" + * + * Note: The default JWT algorithm is now HS512, since [Dgraph now supports HS512 algorithm](https://github.com/dgraph-io/dgraph/pull/8912) and it aligns with NextAuth.js defaults. + * HS256 and RS256 are still supported for backward compatibility. * * [Using JWT and authorization claims](https://dgraph.io/docs/graphql/authorization/authorization-overview#using-jwts-and-authorization-claims) */ - jwtAlgorithm?: "HS256" | "RS256" + jwtAlgorithm?: "HS512" | "HS256" | "RS256" /** * @default "Authorization" * @@ -26,9 +31,16 @@ export interface DgraphClientParams { export class DgraphClientError extends Error { name = "DgraphClientError" + query: string + variables: any + originalErrors: any[] + constructor(errors: any[], query: string, variables: any) { - super(errors.map((error) => error.message).join("\n")) - console.error({ query, variables }) + super(`GraphQL query failed with ${errors.length} errors.`) + this.originalErrors = errors + this.query = query + this.variables = variables + Error.captureStackTrace(this, this.constructor) } } @@ -46,15 +58,16 @@ export function client(params: DgraphClientParams) { endpoint, authToken, jwtSecret, - jwtAlgorithm = "HS256", + jwtAlgorithm = "HS512", authHeader = "Authorization", } = params + const headers: HeadersInit = { "Content-Type": "application/json", "X-Auth-Token": authToken, } - if (authHeader && jwtSecret) { + if (jwtSecret) { headers[authHeader] = jwt.sign({ nextAuth: true }, jwtSecret, { algorithm: jwtAlgorithm, }) @@ -65,17 +78,32 @@ export function client(params: DgraphClientParams) { query: string, variables?: Record ): Promise { - const response = await fetch(endpoint, { - method: "POST", - headers, - body: JSON.stringify({ query, variables }), - }) + try { + const response = await fetch(endpoint, { + method: "POST", + headers, + body: JSON.stringify({ query, variables }), + }) + + if (!response.ok) { + throw new Error( + `HTTP error ${response.status}: ${response.statusText}` + ) + } + + const { data = {}, errors } = await response.json() + if (errors?.length) { + throw new DgraphClientError(errors, query, variables) + } - const { data = {}, errors } = await response.json() - if (errors?.length) { - throw new DgraphClientError(errors, query, variables) + return Object.values(data)[0] as T | null + } catch (error) { + console.error(`Error executing GraphQL query: ${error}`, { + query, + variables, + }) + throw error } - return Object.values(data)[0] as any }, } } diff --git a/packages/adapter-dgraph/test/hs512.key b/packages/adapter-dgraph/test/hs512.key new file mode 100644 index 0000000000..b8cab49b38 --- /dev/null +++ b/packages/adapter-dgraph/test/hs512.key @@ -0,0 +1 @@ +GnaXu1rdxsquR+3y17VWU+/o4rs+URBZJqQwEizWLec= diff --git a/packages/adapter-dgraph/test/index.test.ts b/packages/adapter-dgraph/test/index.test.ts index e6047784d2..8f1978da79 100644 --- a/packages/adapter-dgraph/test/index.test.ts +++ b/packages/adapter-dgraph/test/index.test.ts @@ -7,13 +7,22 @@ import path from "path" import type { DgraphClientParams } from "../src" +let jwtSecret; +try { + jwtSecret = fs.readFileSync(path.join(process.cwd(), "/test/hs512.key"), { + encoding: "utf8", + }); + console.log("Loaded JWT secret from file", jwtSecret); +} catch (error) { + console.error("Failed to load JWT secret from file:", error); + process.exit(1); +} + const params: DgraphClientParams = { endpoint: "http://localhost:8080/graphql", authToken: "test", - jwtAlgorithm: "RS256", - jwtSecret: fs.readFileSync(path.join(process.cwd(), "/test/private.key"), { - encoding: "utf8", - }), + jwtAlgorithm: "HS512", + jwtSecret: jwtSecret, } /** TODO: Add test to `dgraphClient` */ diff --git a/packages/adapter-dgraph/test/private.key b/packages/adapter-dgraph/test/private.key deleted file mode 100644 index 285573fee3..0000000000 --- a/packages/adapter-dgraph/test/private.key +++ /dev/null @@ -1,51 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -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 ------END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/packages/adapter-dgraph/test/public.key b/packages/adapter-dgraph/test/public.key deleted file mode 100644 index 9c694ed1ba..0000000000 --- a/packages/adapter-dgraph/test/public.key +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxqyvd82VacXMBLUADZt+ -euSNUNJ276XgvH4HW4ms5iQZDgYIPKxyaZ+wk8EMYSB1dymJ3WQpm0JKHqgTW+z/ -edfYFQXkduHN/zoIpxMAMyZGsTBidGo0xJSHTCDCdYCCBlG9R1ljjhf0l9ChBP7W -7lSXaRU/XS/tMH1qYMpsUwDav4G/RDI3A4t29JRGqU4mnFa5o3XBCxU4ANCp1JaQ -evzAYox8EGPZ1YZGmhRgca51dBeed9QKqWjfXP4wboC1ppglm+kPgFUaCiXB8Kyf -IixhlvzZiO4RLvZw+cILt586vXGzNy49eVUTiIOoTZuG/79pCeBS8BCbB4l6y274 -y42hUN83gHxQ32Y++DI40jz5iGN85Dj6yDDjKwvwqVhCx/kVJFrmyrTJz/E0cp38 -FeIi7D6e0eXj7G97K+wkNdc4oTs1DsDPzhO/7wxQOZIjvNp+DJAfxin5MbM+UKoo -pvJj3sUMHVrTteWxZg94mmLjg2KnJYBuSn8kiFPYQ0F5MjE7df4tDDTGJ/VEFIG5 -EkQffaNYhW0Z5ORLvW1R1Yd1/ew3UWo+mZ7XAUGLF6clsWSQvzSrrNMYzCk5Fa0L -wvMtQdEVLL3q7/KsEHD7N78EVlmEDlOtC21UidUqXnawCE1QIjAHqFsNNPR2j0lg -OoEjrGdzrvUg6hNV9m6CbSECAwEAAQ== ------END PUBLIC KEY----- \ No newline at end of file diff --git a/packages/adapter-dgraph/test/test.sh b/packages/adapter-dgraph/test/test.sh index 46f4b62054..60020ee21c 100755 --- a/packages/adapter-dgraph/test/test.sh +++ b/packages/adapter-dgraph/test/test.sh @@ -1,29 +1,102 @@ #!/usr/bin/env bash -CONTAINER_NAME=authjs-dgraph +# ============================================================================== +# Initial Configuration +# ============================================================================== +CONTAINER_NAME="authjs-dgraph" +SCHEMA_FILE="src/lib/graphql/schema.gql" +HS512_KEY=$(test/test.schema.gql -PUBLIC_KEY=$(sed 's/$/\\n/' test/public.key | tr -d '\n') -echo "# Dgraph.Authorization {\"VerificationKey\":\"$PUBLIC_KEY\",\"Namespace\":\"https://dgraph.io/jwt/claims\",\"Header\":\"Authorization\",\"Algo\":\"RS256\"}" >>test/test.schema.gql + echo "----------------------------------------" + for ((i=1; i<=max_attempts; i++)); do + if $command; then + echo "${success_message}" + # Interactive or non-interactive handling + # [[ -t 0 ]] && read -p "Pausing, press any key to continue..." + return 0 + else + echo "${fail_message} attempt $i" + fi + sleep ${sleep_duration} + done + echo "${fail_message}: Condition not met after ${max_attempts} attempts." + echo "----------------------------------------" + return 1 +} -curl -X POST localhost:8080/admin/schema --data-binary '@test/test.schema.gql' +# Checks if Dgraph server is up and accessible +function check_dgraph_ready { + curl -sSf "http://localhost:${PORT_8080}/health" >/dev/null 2>&1 +} -printf "\nWaiting 5s for schema to be uploaded..." && sleep 5 +# Function to upload schema and check success +function upload_schema_and_check { + local response=$(echo "${FINAL_SCHEMA}" | curl -s -w "%{http_code}" -o /tmp/dgraph_response.json -X POST "http://localhost:${PORT_8080}/admin/schema" --data-binary "@-") + local http_code=$(echo "${response}" | tail -n1) # Extract only the HTTP status code + [[ "$http_code" -eq 200 ]] +} -# Always stop container, but exit with 1 when tests are failing -if vitest run -c ../utils/vitest.config.ts; then - docker stop "${CONTAINER_NAME}" +# ============================================================================== +# Main Execution +# ============================================================================== + +# Check and remove any existing container +if docker ps -a | grep -q "${CONTAINER_NAME}"; then + echo "Stopping and removing existing container..." + docker stop "${CONTAINER_NAME}" + docker rm "${CONTAINER_NAME}" +fi + +# Start the Dgraph container +echo "----------------------------------------" +echo "Starting Dgraph container..." +docker run -d --rm -p "${PORT_8080}:${PORT_8080}" -p "${PORT_9080}:${PORT_9080}" --name "${CONTAINER_NAME}" "${CONTAINER_IMAGE}" + +# Wait for Dgraph to start +if ! wait_for_condition check_dgraph_ready "Dgraph is up!" "Dgraph not up..."; then + echo "Dgraph failed to start." + docker stop "${CONTAINER_NAME}" + exit 1 +fi + +# Prepare the Dgraph schema without the last line +SCHEMA=$(sed '$ d' "${SCHEMA_FILE}") + +# Append the Dgraph authorization header +FINAL_SCHEMA="${SCHEMA} +# Dgraph.Authorization {\"VerificationKey\":\"${HS512_KEY}\",\"Namespace\":\"https://dgraph.io/jwt/claims\",\"Header\":\"Authorization\",\"Algo\":\"HS512\"}" + +# Proceed with uploading the schema to Dgraph, include proper handling for multiline JSON +if echo "${FINAL_SCHEMA}" | curl -s -w "%{http_code}" -o /tmp/dgraph_response.json -X POST "http://localhost:${PORT_8080}/admin/schema" --data-binary "@-"; then + echo "Schema has been successfully uploaded." +else + echo "Failed to upload schema." + exit 1 +fi + +# Run tests +if vitest run -c "../utils/vitest.config.ts"; then + echo "Tests passed." else - docker stop "${CONTAINER_NAME}" && exit 1 + echo "Tests failed." fi -rm test/test.schema.gql +# Always stop container after tests +echo "----------------------------------------" +echo "Stopping Dgraph container..." +docker stop "${CONTAINER_NAME}" \ No newline at end of file