diff --git a/__fixtures__/test-project/api/src/__tests__/context.test.ts b/__fixtures__/test-project/api/src/__tests__/context.test.ts index 7be9e46d3294..972c4756e85d 100644 --- a/__fixtures__/test-project/api/src/__tests__/context.test.ts +++ b/__fixtures__/test-project/api/src/__tests__/context.test.ts @@ -10,5 +10,5 @@ test('Set a mock user on the context', async () => { }) test('Context is isolated between tests', () => { - expect(context).toStrictEqual({ currentUser: undefined }) + expect(context).toStrictEqual({}) }) diff --git a/packages/api-server/package.json b/packages/api-server/package.json index cee8fd7e05c1..3903cf09576e 100644 --- a/packages/api-server/package.json +++ b/packages/api-server/package.json @@ -32,6 +32,7 @@ "@fastify/http-proxy": "9.3.0", "@fastify/static": "6.12.0", "@fastify/url-data": "5.4.0", + "@redwoodjs/context": "6.0.7", "@redwoodjs/project-config": "6.0.7", "ansi-colors": "4.1.3", "chalk": "4.1.2", diff --git a/packages/api-server/src/__tests__/fastify.test.ts b/packages/api-server/src/__tests__/fastify.test.ts index 72c444f1c279..dad4b55b33e3 100644 --- a/packages/api-server/src/__tests__/fastify.test.ts +++ b/packages/api-server/src/__tests__/fastify.test.ts @@ -8,6 +8,7 @@ jest.mock('fastify', () => { return jest.fn(() => { return { register: () => {}, + addHook: () => {}, } }) }) diff --git a/packages/api-server/src/fastify.ts b/packages/api-server/src/fastify.ts index 5b73d2c45204..f6d419176c89 100644 --- a/packages/api-server/src/fastify.ts +++ b/packages/api-server/src/fastify.ts @@ -4,6 +4,8 @@ import path from 'path' import type { FastifyInstance, FastifyServerOptions } from 'fastify' import Fastify from 'fastify' +import type { GlobalContext } from '@redwoodjs/context' +import { getAsyncStoreInstance } from '@redwoodjs/context/dist/store' import { getPaths, getConfig } from '@redwoodjs/project-config' import type { FastifySideConfigFn } from './types' @@ -60,6 +62,11 @@ export const createFastifyInstance = ( const fastify = Fastify(options || config || DEFAULT_OPTIONS) + // Ensure that each request has a unique global context + fastify.addHook('onRequest', (_req, _reply, done) => { + getAsyncStoreInstance().run(new Map(), done) + }) + return fastify } diff --git a/packages/babel-config/src/__tests__/api.test.ts b/packages/babel-config/src/__tests__/api.test.ts index 471f6cfc2430..a3c7af764927 100644 --- a/packages/babel-config/src/__tests__/api.test.ts +++ b/packages/babel-config/src/__tests__/api.test.ts @@ -212,7 +212,7 @@ describe('api', () => { }, { members: ['context'], - path: '@redwoodjs/graphql-server', + path: '@redwoodjs/context', }, ], }, diff --git a/packages/babel-config/src/__tests__/prebuildApiFile.test.ts b/packages/babel-config/src/__tests__/prebuildApiFile.test.ts index 717f8953b144..3bfadf7a4368 100644 --- a/packages/babel-config/src/__tests__/prebuildApiFile.test.ts +++ b/packages/babel-config/src/__tests__/prebuildApiFile.test.ts @@ -443,9 +443,7 @@ describe('api prebuild ', () => { }) it('auto imports', () => { - expect(code).toContain( - 'import { context } from "@redwoodjs/graphql-server"' - ) + expect(code).toContain('import { context } from "@redwoodjs/context"') expect(code).toContain('import gql from "graphql-tag"') }) }) diff --git a/packages/babel-config/src/api.ts b/packages/babel-config/src/api.ts index 97fc71e7333c..c9842d739648 100644 --- a/packages/babel-config/src/api.ts +++ b/packages/babel-config/src/api.ts @@ -110,9 +110,9 @@ export const getApiSideBabelPlugins = ( path: 'graphql-tag', }, { - // import { context } from '@redwoodjs/graphql-server' + // import { context } from '@redwoodjs/context' members: ['context'], - path: '@redwoodjs/graphql-server', + path: '@redwoodjs/context', }, ], }, @@ -144,10 +144,25 @@ export const getApiSideBabelConfigPath = () => { } } +export const getApiSideBabelOverrides = () => { + const overrides = [ + // Apply context wrapping to all functions + { + // match */api/src/functions/*.js|ts + test: /.+api(?:[\\|/])src(?:[\\|/])functions(?:[\\|/]).+.(?:js|ts)$/, + plugins: [ + require('./plugins/babel-plugin-redwood-context-wrapping').default, + ], + }, + ].filter(Boolean) + return overrides as TransformOptions[] +} + export const getApiSideDefaultBabelConfig = () => { return { presets: getApiSideBabelPresets(), plugins: getApiSideBabelPlugins(), + overrides: getApiSideBabelOverrides(), extends: getApiSideBabelConfigPath(), babelrc: false, ignore: ['node_modules'], diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/context-wrapping/auth/code.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/context-wrapping/auth/code.js new file mode 100644 index 000000000000..c95c23b252fe --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/context-wrapping/auth/code.js @@ -0,0 +1,173 @@ +import { DbAuthHandler, DbAuthHandlerOptions } from '@redwoodjs/auth-dbauth-api' + +import { db } from 'src/lib/db' + +export const handler = async ( + event, + context +) => { + const forgotPasswordOptions = { + // handler() is invoked after verifying that a user was found with the given + // username. This is where you can send the user an email with a link to + // reset their password. With the default dbAuth routes and field names, the + // URL to reset the password will be: + // + // https://example.com/reset-password?resetToken=${user.resetToken} + // + // Whatever is returned from this function will be returned from + // the `forgotPassword()` function that is destructured from `useAuth()` + // You could use this return value to, for example, show the email + // address in a toast message so the user will know it worked and where + // to look for the email. + handler: (user) => { + return user + }, + + // How long the resetToken is valid for, in seconds (default is 24 hours) + expires: 60 * 60 * 24, + + errors: { + // for security reasons you may want to be vague here rather than expose + // the fact that the email address wasn't found (prevents fishing for + // valid email addresses) + usernameNotFound: 'Username not found', + // if the user somehow gets around client validation + usernameRequired: 'Username is required', + }, + } + + const loginOptions = { + // handler() is called after finding the user that matches the + // username/password provided at login, but before actually considering them + // logged in. The `user` argument will be the user in the database that + // matched the username/password. + // + // If you want to allow this user to log in simply return the user. + // + // If you want to prevent someone logging in for another reason (maybe they + // didn't validate their email yet), throw an error and it will be returned + // by the `logIn()` function from `useAuth()` in the form of: + // `{ message: 'Error message' }` + handler: (user) => { + return user + }, + + errors: { + usernameOrPasswordMissing: 'Both username and password are required', + usernameNotFound: 'Username ${username} not found', + // For security reasons you may want to make this the same as the + // usernameNotFound error so that a malicious user can't use the error + // to narrow down if it's the username or password that's incorrect + incorrectPassword: 'Incorrect password for ${username}', + }, + + // How long a user will remain logged in, in seconds + expires: 60 * 60 * 24 * 365 * 10, + } + + const resetPasswordOptions = { + // handler() is invoked after the password has been successfully updated in + // the database. Returning anything truthy will automatically log the user + // in. Return `false` otherwise, and in the Reset Password page redirect the + // user to the login page. + handler: (_user) => { + return true + }, + + // If `false` then the new password MUST be different from the current one + allowReusedPassword: true, + + errors: { + // the resetToken is valid, but expired + resetTokenExpired: 'resetToken is expired', + // no user was found with the given resetToken + resetTokenInvalid: 'resetToken is invalid', + // the resetToken was not present in the URL + resetTokenRequired: 'resetToken is required', + // new password is the same as the old password (apparently they did not forget it) + reusedPassword: 'Must choose a new password', + }, + } + + const signupOptions = { + // Whatever you want to happen to your data on new user signup. Redwood will + // check for duplicate usernames before calling this handler. At a minimum + // you need to save the `username`, `hashedPassword` and `salt` to your + // user table. `userAttributes` contains any additional object members that + // were included in the object given to the `signUp()` function you got + // from `useAuth()`. + // + // If you want the user to be immediately logged in, return the user that + // was created. + // + // If this handler throws an error, it will be returned by the `signUp()` + // function in the form of: `{ error: 'Error message' }`. + // + // If this returns anything else, it will be returned by the + // `signUp()` function in the form of: `{ message: 'String here' }`. + handler: ({ username, hashedPassword, salt, userAttributes }) => { + return db.user.create({ + data: { + email: username, + hashedPassword: hashedPassword, + salt: salt, + fullName: userAttributes['full-name'], + }, + }) + }, + + // Include any format checks for password here. Return `true` if the + // password is valid, otherwise throw a `PasswordValidationError`. + // Import the error along with `DbAuthHandler` from `@redwoodjs/api` above. + passwordValidation: (_password) => { + return true + }, + + errors: { + // `field` will be either "username" or "password" + fieldMissing: '${field} is required', + usernameTaken: 'Username `${username}` already in use', + }, + } + + const authHandler = new DbAuthHandler(event, context, { + // Provide prisma db client + db: db, + + // The name of the property you'd call on `db` to access your user table. + // i.e. if your Prisma model is named `User` this value would be `user`, as in `db.user` + authModelAccessor: 'user', + + // A map of what dbAuth calls a field to what your database calls it. + // `id` is whatever column you use to uniquely identify a user (probably + // something like `id` or `userId` or even `email`) + authFields: { + id: 'id', + username: 'email', + hashedPassword: 'hashedPassword', + salt: 'salt', + resetToken: 'resetToken', + resetTokenExpiresAt: 'resetTokenExpiresAt', + }, + + // Specifies attributes on the cookie that dbAuth sets in order to remember + // who is logged in. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies + cookie: { + HttpOnly: true, + Path: '/', + SameSite: 'Strict', + Secure: process.env.NODE_ENV !== 'development', + + // If you need to allow other domains (besides the api side) access to + // the dbAuth session cookie: + // Domain: 'example.com', + }, + + forgotPassword: forgotPasswordOptions, + login: loginOptions, + resetPassword: resetPasswordOptions, + signup: signupOptions, + }) + + return await authHandler.invoke() +} diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/context-wrapping/auth/output.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/context-wrapping/auth/output.js new file mode 100644 index 000000000000..871120a0843e --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/context-wrapping/auth/output.js @@ -0,0 +1,165 @@ +import { DbAuthHandler, DbAuthHandlerOptions } from '@redwoodjs/auth-dbauth-api' +import { db } from 'src/lib/db' +import { getAsyncStoreInstance as __rw_getAsyncStoreInstance } from '@redwoodjs/context/dist/store' +const __rw_handler = async (event, context) => { + const forgotPasswordOptions = { + // handler() is invoked after verifying that a user was found with the given + // username. This is where you can send the user an email with a link to + // reset their password. With the default dbAuth routes and field names, the + // URL to reset the password will be: + // + // https://example.com/reset-password?resetToken=${user.resetToken} + // + // Whatever is returned from this function will be returned from + // the `forgotPassword()` function that is destructured from `useAuth()` + // You could use this return value to, for example, show the email + // address in a toast message so the user will know it worked and where + // to look for the email. + handler: (user) => { + return user + }, + // How long the resetToken is valid for, in seconds (default is 24 hours) + expires: 60 * 60 * 24, + errors: { + // for security reasons you may want to be vague here rather than expose + // the fact that the email address wasn't found (prevents fishing for + // valid email addresses) + usernameNotFound: 'Username not found', + // if the user somehow gets around client validation + usernameRequired: 'Username is required', + }, + } + const loginOptions = { + // handler() is called after finding the user that matches the + // username/password provided at login, but before actually considering them + // logged in. The `user` argument will be the user in the database that + // matched the username/password. + // + // If you want to allow this user to log in simply return the user. + // + // If you want to prevent someone logging in for another reason (maybe they + // didn't validate their email yet), throw an error and it will be returned + // by the `logIn()` function from `useAuth()` in the form of: + // `{ message: 'Error message' }` + handler: (user) => { + return user + }, + errors: { + usernameOrPasswordMissing: 'Both username and password are required', + usernameNotFound: 'Username ${username} not found', + // For security reasons you may want to make this the same as the + // usernameNotFound error so that a malicious user can't use the error + // to narrow down if it's the username or password that's incorrect + incorrectPassword: 'Incorrect password for ${username}', + }, + // How long a user will remain logged in, in seconds + expires: 60 * 60 * 24 * 365 * 10, + } + const resetPasswordOptions = { + // handler() is invoked after the password has been successfully updated in + // the database. Returning anything truthy will automatically log the user + // in. Return `false` otherwise, and in the Reset Password page redirect the + // user to the login page. + handler: (_user) => { + return true + }, + // If `false` then the new password MUST be different from the current one + allowReusedPassword: true, + errors: { + // the resetToken is valid, but expired + resetTokenExpired: 'resetToken is expired', + // no user was found with the given resetToken + resetTokenInvalid: 'resetToken is invalid', + // the resetToken was not present in the URL + resetTokenRequired: 'resetToken is required', + // new password is the same as the old password (apparently they did not forget it) + reusedPassword: 'Must choose a new password', + }, + } + const signupOptions = { + // Whatever you want to happen to your data on new user signup. Redwood will + // check for duplicate usernames before calling this handler. At a minimum + // you need to save the `username`, `hashedPassword` and `salt` to your + // user table. `userAttributes` contains any additional object members that + // were included in the object given to the `signUp()` function you got + // from `useAuth()`. + // + // If you want the user to be immediately logged in, return the user that + // was created. + // + // If this handler throws an error, it will be returned by the `signUp()` + // function in the form of: `{ error: 'Error message' }`. + // + // If this returns anything else, it will be returned by the + // `signUp()` function in the form of: `{ message: 'String here' }`. + handler: ({ username, hashedPassword, salt, userAttributes }) => { + return db.user.create({ + data: { + email: username, + hashedPassword: hashedPassword, + salt: salt, + fullName: userAttributes['full-name'], + }, + }) + }, + // Include any format checks for password here. Return `true` if the + // password is valid, otherwise throw a `PasswordValidationError`. + // Import the error along with `DbAuthHandler` from `@redwoodjs/api` above. + passwordValidation: (_password) => { + return true + }, + errors: { + // `field` will be either "username" or "password" + fieldMissing: '${field} is required', + usernameTaken: 'Username `${username}` already in use', + }, + } + const authHandler = new DbAuthHandler(event, context, { + // Provide prisma db client + db: db, + // The name of the property you'd call on `db` to access your user table. + // i.e. if your Prisma model is named `User` this value would be `user`, as in `db.user` + authModelAccessor: 'user', + // A map of what dbAuth calls a field to what your database calls it. + // `id` is whatever column you use to uniquely identify a user (probably + // something like `id` or `userId` or even `email`) + authFields: { + id: 'id', + username: 'email', + hashedPassword: 'hashedPassword', + salt: 'salt', + resetToken: 'resetToken', + resetTokenExpiresAt: 'resetTokenExpiresAt', + }, + // Specifies attributes on the cookie that dbAuth sets in order to remember + // who is logged in. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies + cookie: { + HttpOnly: true, + Path: '/', + SameSite: 'Strict', + Secure: process.env.NODE_ENV !== 'development', + + // If you need to allow other domains (besides the api side) access to + // the dbAuth session cookie: + // Domain: 'example.com', + }, + forgotPassword: forgotPasswordOptions, + login: loginOptions, + resetPassword: resetPasswordOptions, + signup: signupOptions, + }) + return await authHandler.invoke() +} +export const handler = async (__rw_event, __rw__context) => { + // The store will be undefined if no context isolation has been performed yet + const __rw_contextStore = __rw_getAsyncStoreInstance().getStore() + if (__rw_contextStore === undefined) { + return __rw_getAsyncStoreInstance().run( + new Map(), + __rw_handler, + __rw_event, + __rw__context + ) + } + return __rw_handler(__rw_event, __rw__context) +} \ No newline at end of file diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/context-wrapping/custom/code.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/context-wrapping/custom/code.js new file mode 100644 index 000000000000..b487854d6633 --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/context-wrapping/custom/code.js @@ -0,0 +1,31 @@ +import { logger } from 'src/lib/logger' + +/** + * The handler function is your code that processes http request events. + * You can use return and throw to send a response or error, respectively. + * + * Important: When deployed, a custom serverless function is an open API endpoint and + * is your responsibility to secure appropriately. + * + * @see {@link https://redwoodjs.com/docs/serverless-functions#security-considerations|Serverless Function Considerations} + * in the RedwoodJS documentation for more information. + * + * @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent + * @typedef { import('aws-lambda').Context } Context + * @param { APIGatewayEvent } event - an object which contains information from the invoker. + * @param { Context } context - contains information about the invocation, + * function, and execution environment. + */ +export const handler = async (event, _context) => { + logger.info(`${event.httpMethod} ${event.path}: custom function`) + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: 'custom function', + }), + } +} diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/context-wrapping/custom/output.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/context-wrapping/custom/output.js new file mode 100644 index 000000000000..636deea78910 --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/context-wrapping/custom/output.js @@ -0,0 +1,44 @@ +import { logger } from 'src/lib/logger' + +/** + * The handler function is your code that processes http request events. + * You can use return and throw to send a response or error, respectively. + * + * Important: When deployed, a custom serverless function is an open API endpoint and + * is your responsibility to secure appropriately. + * + * @see {@link https://redwoodjs.com/docs/serverless-functions#security-considerations|Serverless Function Considerations} + * in the RedwoodJS documentation for more information. + * + * @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent + * @typedef { import('aws-lambda').Context } Context + * @param { APIGatewayEvent } event - an object which contains information from the invoker. + * @param { Context } context - contains information about the invocation, + * function, and execution environment. + */ +import { getAsyncStoreInstance as __rw_getAsyncStoreInstance } from '@redwoodjs/context/dist/store' +const __rw_handler = async (event, _context) => { + logger.info(`${event.httpMethod} ${event.path}: custom function`) + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: 'custom function', + }), + } +} +export const handler = async (__rw_event, __rw__context) => { + // The store will be undefined if no context isolation has been performed yet + const __rw_contextStore = __rw_getAsyncStoreInstance().getStore() + if (__rw_contextStore === undefined) { + return __rw_getAsyncStoreInstance().run( + new Map(), + __rw_handler, + __rw_event, + __rw__context + ) + } + return __rw_handler(__rw_event, __rw__context) +} \ No newline at end of file diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/context-wrapping/graphql/code.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/context-wrapping/graphql/code.js new file mode 100644 index 000000000000..5d8db6ab8f2a --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/context-wrapping/graphql/code.js @@ -0,0 +1,23 @@ +import { authDecoder } from '@redwoodjs/auth-dbauth-api' +import { createGraphQLHandler } from '@redwoodjs/graphql-server' + +import directives from 'src/directives/**/*.{js,ts}' +import sdls from 'src/graphql/**/*.sdl.{js,ts}' +import services from 'src/services/**/*.{js,ts}' + +import { getCurrentUser } from 'src/lib/auth' +import { db } from 'src/lib/db' +import { logger } from 'src/lib/logger' + +export const handler = createGraphQLHandler({ + authDecoder, + getCurrentUser, + loggerConfig: { logger, options: {} }, + directives, + sdls, + services, + onException: () => { + // Disconnect from your database with an unhandled exception. + db.$disconnect() + }, +}) diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/context-wrapping/graphql/output.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/context-wrapping/graphql/output.js new file mode 100644 index 000000000000..ba3386d5b18f --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/context-wrapping/graphql/output.js @@ -0,0 +1,37 @@ +import { authDecoder } from '@redwoodjs/auth-dbauth-api' +import { createGraphQLHandler } from '@redwoodjs/graphql-server' +import directives from 'src/directives/**/*.{js,ts}' +import sdls from 'src/graphql/**/*.sdl.{js,ts}' +import services from 'src/services/**/*.{js,ts}' +import { getCurrentUser } from 'src/lib/auth' +import { db } from 'src/lib/db' +import { logger } from 'src/lib/logger' +import { getAsyncStoreInstance as __rw_getAsyncStoreInstance } from '@redwoodjs/context/dist/store' +const __rw_handler = createGraphQLHandler({ + authDecoder, + getCurrentUser, + loggerConfig: { + logger, + options: {}, + }, + directives, + sdls, + services, + onException: () => { + // Disconnect from your database with an unhandled exception. + db.$disconnect() + }, +}) +export const handler = (__rw_event, __rw__context) => { + // The store will be undefined if no context isolation has been performed yet + const __rw_contextStore = __rw_getAsyncStoreInstance().getStore() + if (__rw_contextStore === undefined) { + return __rw_getAsyncStoreInstance().run( + new Map(), + __rw_handler, + __rw_event, + __rw__context + ) + } + return __rw_handler(__rw_event, __rw__context) +} \ No newline at end of file diff --git a/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-context-wrapping.test.ts b/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-context-wrapping.test.ts new file mode 100644 index 000000000000..25eecfd9be96 --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-context-wrapping.test.ts @@ -0,0 +1,11 @@ +import path from 'path' + +import pluginTester from 'babel-plugin-tester' + +import redwoodOtelWrappingPlugin from '../babel-plugin-redwood-context-wrapping' + +pluginTester({ + plugin: redwoodOtelWrappingPlugin, + pluginName: 'babel-plugin-redwood-context-wrapping', + fixtures: path.join(__dirname, '__fixtures__/context-wrapping'), +}) diff --git a/packages/babel-config/src/plugins/babel-plugin-redwood-context-wrapping.ts b/packages/babel-config/src/plugins/babel-plugin-redwood-context-wrapping.ts new file mode 100644 index 000000000000..9da33bc3ba77 --- /dev/null +++ b/packages/babel-config/src/plugins/babel-plugin-redwood-context-wrapping.ts @@ -0,0 +1,126 @@ +import type { PluginObj, types } from '@babel/core' + +// This wraps user API functions to ensure context isolation has been performed. This should already +// be done at the request level but in serverless environments like Netlify we need to do +// this at the function level as a safeguard. + +function generateWrappedHandler(t: typeof types, isAsync: boolean) { + const contextStoreVariableDeclaration = t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier('__rw_contextStore'), + t.callExpression( + t.memberExpression( + t.callExpression(t.identifier('__rw_getAsyncStoreInstance'), []), + t.identifier('getStore') + ), + [] + ) + ), + ]) + t.addComment( + contextStoreVariableDeclaration, + 'leading', + ' The store will be undefined if no context isolation has been performed yet', + true + ) + return t.arrowFunctionExpression( + [t.identifier('__rw_event'), t.identifier('__rw__context')], + t.blockStatement([ + contextStoreVariableDeclaration, + t.ifStatement( + t.binaryExpression( + '===', + t.identifier('__rw_contextStore'), + t.identifier('undefined') + ), + t.blockStatement([ + t.returnStatement( + t.callExpression( + t.memberExpression( + t.callExpression( + t.identifier('__rw_getAsyncStoreInstance'), + [] + ), + t.identifier('run') + ), + [ + t.newExpression(t.identifier('Map'), []), + t.identifier('__rw_handler'), + t.identifier('__rw_event'), + t.identifier('__rw__context'), + ] + ) + ), + ]) + ), + t.returnStatement( + t.callExpression(t.identifier('__rw_handler'), [ + t.identifier('__rw_event'), + t.identifier('__rw__context'), + ]) + ), + ]), + isAsync + ) +} + +export default function ({ types: t }: { types: typeof types }): PluginObj { + return { + name: 'babel-plugin-redwood-context-wrapping', + visitor: { + ExportNamedDeclaration(path, _state) { + // Confirm we're at the "handler" export + const declaration = path.node.declaration + if (!t.isVariableDeclaration(declaration)) { + return + } + const identifier = declaration.declarations[0].id + if (!t.isIdentifier(identifier)) { + return + } + if (identifier.name !== 'handler') { + return + } + + // Import the context package + const parentNode = path.parentPath.node + if (!t.isProgram(parentNode)) { + // This should be unreachable + return + } + path.insertBefore( + // import { getAsyncStoreInstance as __rw_getAsyncStoreInstance } from '@redwoodjs/context/dist/store' + t.importDeclaration( + [ + t.importSpecifier( + t.identifier('__rw_getAsyncStoreInstance'), + t.identifier('getAsyncStoreInstance') + ), + ], + t.stringLiteral('@redwoodjs/context/dist/store') + ) + ) + + // Copy the original handler function to a new renamed function + path.insertBefore( + t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier('__rw_handler'), + declaration.declarations[0].init + ), + ]) + ) + + // Attempt to determine if we should mark the handler as async + let isAsync = false + const originalInit = declaration.declarations[0].init + if (t.isFunction(originalInit)) { + isAsync = originalInit.async + } + + // Update the original handler to check the context status and call the renamed function + declaration.declarations[0].init = generateWrappedHandler(t, isAsync) + }, + }, + } +} diff --git a/packages/context/.babelrc.js b/packages/context/.babelrc.js new file mode 100644 index 000000000000..3b2c815712d9 --- /dev/null +++ b/packages/context/.babelrc.js @@ -0,0 +1 @@ +module.exports = { extends: '../../babel.config.js' } diff --git a/packages/context/README.md b/packages/context/README.md new file mode 100644 index 000000000000..fb07a71c2fe3 --- /dev/null +++ b/packages/context/README.md @@ -0,0 +1,17 @@ +# Context + +## About + +This package contains code for the global context used on the API side of a +Redwood application. It's automatically available in services, auth functions +and custom functions. + +## Serveful environments + +In serverful environments with Fastify the global context is injected by a +Fastify `onRequest` hook. + +## Serverless environments + +Babel is used to automatically wrap functions with code that makes the context +available. diff --git a/packages/context/build.mjs b/packages/context/build.mjs new file mode 100644 index 000000000000..95bdb1e83cfb --- /dev/null +++ b/packages/context/build.mjs @@ -0,0 +1,27 @@ +import fs from 'node:fs' + +import * as esbuild from 'esbuild' +import fg from 'fast-glob' + +// Get source files +const sourceFiles = fg.sync(['./src/**/*.ts'], { + ignore: ['**/*.test.ts'], +}) + +// Build general source files +const result = await esbuild.build({ + entryPoints: sourceFiles, + outdir: 'dist', + + format: 'cjs', + platform: 'node', + target: ['node18'], + + logLevel: 'info', + + // For visualizing dist. + // See https://esbuild.github.io/api/#metafile and https://esbuild.github.io/analyze/. + metafile: true, +}) + +fs.writeFileSync('meta.json', JSON.stringify(result.metafile, null, 2)) diff --git a/packages/context/package.json b/packages/context/package.json new file mode 100644 index 000000000000..3328ad93507c --- /dev/null +++ b/packages/context/package.json @@ -0,0 +1,33 @@ +{ + "name": "@redwoodjs/context", + "version": "6.0.7", + "repository": { + "type": "git", + "url": "https://github.com/redwoodjs/redwood.git", + "directory": "packages/context" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "yarn node ./build.mjs && yarn build:types", + "build:types": "tsc --build --verbose", + "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx\" --ignore dist --exec \"yarn build\"", + "prepublishOnly": "NODE_ENV=production yarn build" + }, + "jest": { + "testPathIgnorePatterns": [ + "/dist/" + ] + }, + "devDependencies": { + "esbuild": "0.19.9", + "fast-glob": "3.3.2", + "jest": "29.7.0", + "typescript": "5.3.3" + }, + "gitHead": "3905ed045508b861b495f8d5630d76c7a157d8f1" +} diff --git a/packages/context/src/context.ts b/packages/context/src/context.ts new file mode 100644 index 000000000000..76b2e70e5f1b --- /dev/null +++ b/packages/context/src/context.ts @@ -0,0 +1,45 @@ +/* eslint-disable react-hooks/rules-of-hooks */ + +import { getAsyncStoreInstance } from './store' + +export interface GlobalContext extends Record {} + +export const createContextProxy = (target: GlobalContext) => { + return new Proxy(target, { + get: (_target, property: string) => { + const store = getAsyncStoreInstance().getStore() + const ctx = store?.get('context') || {} + return ctx[property] + }, + set: (_target, property: string, newVal) => { + const store = getAsyncStoreInstance().getStore() + const ctx = store?.get('context') || {} + ctx[property] = newVal + store?.set('context', ctx) + return true + }, + }) +} + +export let context: GlobalContext = createContextProxy({}) + +/** + * Set the contents of the global context object. + * + * This completely replaces the existing context values such as currentUser. + * + * If you wish to extend the context simply use the `context` object directly, + * such as `context.magicNumber = 1`, or `setContext({ ...context, magicNumber: 1 })` + */ +export const setContext = (newContext: GlobalContext): GlobalContext => { + // re-init the proxy against the new context object, + // so things like `console.log(context)` is the actual object, + // not one initialized earlier. + context = createContextProxy(newContext) + + // Replace the value of context stored in the current async store + const store = getAsyncStoreInstance().getStore() + store?.set('context', newContext) + + return context +} diff --git a/packages/context/src/global.api-auto-imports.ts b/packages/context/src/global.api-auto-imports.ts new file mode 100644 index 000000000000..dcddf15efc8f --- /dev/null +++ b/packages/context/src/global.api-auto-imports.ts @@ -0,0 +1,6 @@ +/* eslint-disable no-redeclare, no-undef */ +import type { GlobalContext } from './context' + +declare global { + const context: GlobalContext +} diff --git a/packages/context/src/index.ts b/packages/context/src/index.ts new file mode 100644 index 000000000000..1f8bd9fe5a85 --- /dev/null +++ b/packages/context/src/index.ts @@ -0,0 +1,5 @@ +export * from './context' +// Note: store is not exported here to discourage direct usage. + +import './global.api-auto-imports' +export * from './global.api-auto-imports' diff --git a/packages/context/src/store.ts b/packages/context/src/store.ts new file mode 100644 index 000000000000..a688ed296d35 --- /dev/null +++ b/packages/context/src/store.ts @@ -0,0 +1,19 @@ +/* eslint-disable react-hooks/rules-of-hooks */ + +import { AsyncLocalStorage } from 'async_hooks' + +import type { GlobalContext } from './context' + +let CONTEXT_STORAGE: AsyncLocalStorage> + +/** + * This returns a AsyncLocalStorage instance, not the actual store. + * Should not be used by Redwood apps directly. The framework handles + * this. + */ +export const getAsyncStoreInstance = () => { + if (!CONTEXT_STORAGE) { + CONTEXT_STORAGE = new AsyncLocalStorage>() + } + return CONTEXT_STORAGE as AsyncLocalStorage> +} diff --git a/packages/context/tsconfig.json b/packages/context/tsconfig.json new file mode 100644 index 000000000000..fabf60e3eb27 --- /dev/null +++ b/packages/context/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.compilerOption.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": "src", + "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo", + "outDir": "dist", + }, + "include": ["src"], +} diff --git a/packages/eslint-config/index.js b/packages/eslint-config/index.js index d977dcdf3ade..02dba1341107 100644 --- a/packages/eslint-config/index.js +++ b/packages/eslint-config/index.js @@ -15,15 +15,18 @@ const getProjectBabelOptions = () => { // We can't nest the web overrides inside the overrides block // So we just take it out and put it as a separate item // Ignoring overrides, as I don't think it has any impact on linting - const { overrides: _overrides, ...otherWebConfig } = + const { overrides: _webOverrides, ...otherWebConfig } = getWebSideDefaultBabelConfig() + const { overrides: _apiOverrides, ...otherApiConfig } = + getApiSideDefaultBabelConfig() + return { plugins: getCommonPlugins(), overrides: [ { test: ['./api/', './scripts/'], - ...getApiSideDefaultBabelConfig(), + ...otherApiConfig, }, { test: ['./web/'], diff --git a/packages/fastify/package.json b/packages/fastify/package.json index 06c2a0a7383c..bebf2419beba 100644 --- a/packages/fastify/package.json +++ b/packages/fastify/package.json @@ -21,6 +21,7 @@ "@fastify/http-proxy": "9.3.0", "@fastify/static": "6.12.0", "@fastify/url-data": "5.4.0", + "@redwoodjs/context": "6.0.7", "@redwoodjs/graphql-server": "6.0.7", "@redwoodjs/project-config": "6.0.7", "ansi-colors": "4.1.3", diff --git a/packages/fastify/src/api.ts b/packages/fastify/src/api.ts index 795d6af1debc..47e5205d01e0 100644 --- a/packages/fastify/src/api.ts +++ b/packages/fastify/src/api.ts @@ -2,8 +2,8 @@ import fastifyUrlData from '@fastify/url-data' import type { FastifyInstance, HookHandlerDoneFunction } from 'fastify' import fastifyRawBody from 'fastify-raw-body' -import type { GlobalContext } from '@redwoodjs/graphql-server' -import { getAsyncStoreInstance } from '@redwoodjs/graphql-server' +import type { GlobalContext } from '@redwoodjs/context' +import { getAsyncStoreInstance } from '@redwoodjs/context/dist/store' import { loadFastifyConfig } from './config' import { lambdaRequestHandler, loadFunctionsFromDist } from './lambda' diff --git a/packages/fastify/src/graphql.ts b/packages/fastify/src/graphql.ts index 7c83145077ef..bf564f33aace 100644 --- a/packages/fastify/src/graphql.ts +++ b/packages/fastify/src/graphql.ts @@ -9,14 +9,10 @@ import type { import fastifyRawBody from 'fastify-raw-body' import type { Plugin } from 'graphql-yoga' -import type { - GraphQLYogaOptions, - GlobalContext, -} from '@redwoodjs/graphql-server' -import { - createGraphQLYoga, - getAsyncStoreInstance, -} from '@redwoodjs/graphql-server' +import type { GlobalContext } from '@redwoodjs/context' +import { getAsyncStoreInstance } from '@redwoodjs/context/dist/store' +import type { GraphQLYogaOptions } from '@redwoodjs/graphql-server' +import { createGraphQLYoga } from '@redwoodjs/graphql-server' /** * Transform a Fastify Request to an event compatible with the RedwoodGraphQLContext's event diff --git a/packages/graphql-server/package.json b/packages/graphql-server/package.json index 2fa3967e884f..e2739e6e9aa7 100644 --- a/packages/graphql-server/package.json +++ b/packages/graphql-server/package.json @@ -35,6 +35,7 @@ "@graphql-yoga/plugin-persisted-operations": "2.0.5", "@opentelemetry/api": "1.7.0", "@redwoodjs/api": "6.0.7", + "@redwoodjs/context": "6.0.7", "core-js": "3.34.0", "graphql": "16.8.1", "graphql-scalars": "1.22.4", diff --git a/packages/graphql-server/src/functions/__tests__/authDecoders.test.ts b/packages/graphql-server/src/functions/__tests__/authDecoders.test.ts index a02b2fa06e1c..5b294742b626 100644 --- a/packages/graphql-server/src/functions/__tests__/authDecoders.test.ts +++ b/packages/graphql-server/src/functions/__tests__/authDecoders.test.ts @@ -3,7 +3,6 @@ import type { APIGatewayProxyEvent, Context } from 'aws-lambda' import { createLogger } from '@redwoodjs/api/logger' import { createGraphQLHandler } from '../../functions/graphql' -import { context } from '../../globalContext' jest.mock('../../makeMergedSchema', () => { const { makeExecutableSchema } = require('@graphql-tools/schema') @@ -28,7 +27,7 @@ jest.mock('../../makeMergedSchema', () => { resolvers: { Query: { me: () => { - const globalContext = require('../../globalContext').context + const globalContext = require('@redwoodjs/context').context const currentUser = globalContext.currentUser return { diff --git a/packages/graphql-server/src/functions/__tests__/fixtures/auth.ts b/packages/graphql-server/src/functions/__tests__/fixtures/auth.ts index 59d65cd71c2b..bf783eaaddc1 100644 --- a/packages/graphql-server/src/functions/__tests__/fixtures/auth.ts +++ b/packages/graphql-server/src/functions/__tests__/fixtures/auth.ts @@ -4,7 +4,7 @@ import { APIGatewayEvent } from 'aws-lambda' interface Context extends Record {} -import { context } from '../../../globalContext' +import { context } from '@redwoodjs/context' /** * Represents the user attributes returned by the decoding the diff --git a/packages/graphql-server/src/functions/__tests__/globalContext.test.ts b/packages/graphql-server/src/functions/__tests__/globalContext.test.ts index 5806f22661e5..7df76ce30531 100644 --- a/packages/graphql-server/src/functions/__tests__/globalContext.test.ts +++ b/packages/graphql-server/src/functions/__tests__/globalContext.test.ts @@ -1,5 +1,5 @@ -import { context as globalContext, setContext } from '../../globalContext' -import { getAsyncStoreInstance } from '../../globalContextStore' +import { context as globalContext, setContext } from '@redwoodjs/context' +import { getAsyncStoreInstance } from '@redwoodjs/context/dist/store' describe('Global context with context isolation', () => { it('Should work when assigning directly into context', async () => { diff --git a/packages/graphql-server/src/functions/__tests__/useRequireAuth.test.ts b/packages/graphql-server/src/functions/__tests__/useRequireAuth.test.ts index 14eefa57b1fd..cbc088767611 100644 --- a/packages/graphql-server/src/functions/__tests__/useRequireAuth.test.ts +++ b/packages/graphql-server/src/functions/__tests__/useRequireAuth.test.ts @@ -64,7 +64,7 @@ const handler = async ( ): Promise => { // @MARK // Don't use globalContext until beforeAll runs - const globalContext = require('../../globalContext').context + const globalContext = require('@redwoodjs/context').context const currentUser = globalContext.currentUser return { @@ -114,7 +114,7 @@ const handlerWithError = async ( ): Promise => { // @MARK // Don't use globalContext until beforeAll runs - const globalContext = require('../../globalContext').context + const globalContext = require('@redwoodjs/context').context const currentUser = globalContext.currentUser try { diff --git a/packages/graphql-server/src/functions/graphql.ts b/packages/graphql-server/src/functions/graphql.ts index 28e33070d7b2..f934e856fb1f 100644 --- a/packages/graphql-server/src/functions/graphql.ts +++ b/packages/graphql-server/src/functions/graphql.ts @@ -4,9 +4,10 @@ import type { Context as LambdaContext, } from 'aws-lambda' +import type { GlobalContext } from '@redwoodjs/context' +import { getAsyncStoreInstance } from '@redwoodjs/context/dist/store' + import { createGraphQLYoga } from '../createGraphQLYoga' -import type { GlobalContext } from '../globalContext' -import { getAsyncStoreInstance } from '../globalContextStore' import type { GraphQLHandlerOptions } from '../types' /** diff --git a/packages/graphql-server/src/functions/useRequireAuth.ts b/packages/graphql-server/src/functions/useRequireAuth.ts index 669a0757cf33..2f0b421d33a7 100644 --- a/packages/graphql-server/src/functions/useRequireAuth.ts +++ b/packages/graphql-server/src/functions/useRequireAuth.ts @@ -2,10 +2,10 @@ import type { APIGatewayEvent, Context as LambdaContext } from 'aws-lambda' import type { Decoder } from '@redwoodjs/api' import { getAuthenticationContext } from '@redwoodjs/api' +import type { GlobalContext } from '@redwoodjs/context' +import { context as globalContext } from '@redwoodjs/context' +import { getAsyncStoreInstance } from '@redwoodjs/context/dist/store' -import type { GlobalContext } from '../globalContext' -import { context as globalContext } from '../globalContext' -import { getAsyncStoreInstance } from '../globalContextStore' import type { GetCurrentUser } from '../types' interface Args { diff --git a/packages/graphql-server/src/global.api-auto-imports.ts b/packages/graphql-server/src/global.api-auto-imports.ts index 77effa89383b..72077eec8a47 100644 --- a/packages/graphql-server/src/global.api-auto-imports.ts +++ b/packages/graphql-server/src/global.api-auto-imports.ts @@ -1,9 +1,6 @@ /* eslint-disable no-redeclare, no-undef */ import type _gql from 'graphql-tag' -import type { GlobalContext } from './globalContext' - declare global { const gql: typeof _gql - const context: GlobalContext } diff --git a/packages/graphql-server/src/globalContext.ts b/packages/graphql-server/src/globalContext.ts index 799ce70708d7..f0a86bbd8863 100644 --- a/packages/graphql-server/src/globalContext.ts +++ b/packages/graphql-server/src/globalContext.ts @@ -2,8 +2,14 @@ import { getAsyncStoreInstance } from './globalContextStore' +/** + * @deprecated This type will be available only from the `@redwoodjs/context` package in a future release. + */ export interface GlobalContext extends Record {} +/** + * @deprecated This function will be available only from the `@redwoodjs/context` package in a future release. + */ export const createContextProxy = (target: GlobalContext) => { return new Proxy(target, { get: (_target, property: string) => { @@ -21,6 +27,9 @@ export const createContextProxy = (target: GlobalContext) => { }) } +/** + * @deprecated This value will be available only from the `@redwoodjs/context` package in a future release. + */ export let context: GlobalContext = createContextProxy({}) /** @@ -30,6 +39,8 @@ export let context: GlobalContext = createContextProxy({}) * * If you wish to extend the context simply use the `context` object directly, * such as `context.magicNumber = 1`, or `setContext({ ...context, magicNumber: 1 })` + * + * @deprecated This function will be available only from the `@redwoodjs/context` package in a future release. */ export const setContext = (newContext: GlobalContext): GlobalContext => { // re-init the proxy against the new context object, diff --git a/packages/graphql-server/src/globalContextStore.ts b/packages/graphql-server/src/globalContextStore.ts index a8f9e1b578f9..af87f07da435 100644 --- a/packages/graphql-server/src/globalContextStore.ts +++ b/packages/graphql-server/src/globalContextStore.ts @@ -8,6 +8,8 @@ let CONTEXT_STORAGE: AsyncLocalStorage> /** * This returns a AsyncLocalStorage instance, not the actual store + * + * @deprecated This function will be available only from the `@redwoodjs/context` package in a future release. */ export const getAsyncStoreInstance = () => { if (!CONTEXT_STORAGE) { diff --git a/packages/graphql-server/src/index.ts b/packages/graphql-server/src/index.ts index 71d69da2a8b4..756d2abd9af2 100644 --- a/packages/graphql-server/src/index.ts +++ b/packages/graphql-server/src/index.ts @@ -1,8 +1,6 @@ import './global.api-auto-imports' export * from './global.api-auto-imports' -export * from './globalContext' -export * from './globalContextStore' export * from './errors' export * from './functions/graphql' diff --git a/packages/graphql-server/src/plugins/__tests__/useRedwoodGlobalContextSetter.test.ts b/packages/graphql-server/src/plugins/__tests__/useRedwoodGlobalContextSetter.test.ts index 84da0d36fb5a..d8b5f6a99bc2 100644 --- a/packages/graphql-server/src/plugins/__tests__/useRedwoodGlobalContextSetter.test.ts +++ b/packages/graphql-server/src/plugins/__tests__/useRedwoodGlobalContextSetter.test.ts @@ -2,8 +2,10 @@ import { useEngine } from '@envelop/core' import { createTestkit } from '@envelop/testing' import * as GraphQLJS from 'graphql' -import type { GlobalContext } from '../../index' -import { context, getAsyncStoreInstance, setContext } from '../../index' +import type { GlobalContext } from '@redwoodjs/context' +import { context, setContext } from '@redwoodjs/context' +import { getAsyncStoreInstance } from '@redwoodjs/context/dist/store' + import { testSchema, testQuery } from '../__fixtures__/common' import { useRedwoodGlobalContextSetter } from '../useRedwoodGlobalContextSetter' import { useRedwoodPopulateContext } from '../useRedwoodPopulateContext' diff --git a/packages/graphql-server/src/plugins/useRedwoodDirective.ts b/packages/graphql-server/src/plugins/useRedwoodDirective.ts index 66d990b86ce6..66212bdc27b6 100644 --- a/packages/graphql-server/src/plugins/useRedwoodDirective.ts +++ b/packages/graphql-server/src/plugins/useRedwoodDirective.ts @@ -10,7 +10,7 @@ import type { import { defaultFieldResolver, getDirectiveValues } from 'graphql' import type { Plugin } from 'graphql-yoga' -import type { GlobalContext } from '../index' +import type { GlobalContext } from '@redwoodjs/context' export interface DirectiveParams< FieldType = any, diff --git a/packages/graphql-server/src/plugins/useRedwoodGlobalContextSetter.ts b/packages/graphql-server/src/plugins/useRedwoodGlobalContextSetter.ts index 12f7ba2dbfd9..c867828b02b7 100644 --- a/packages/graphql-server/src/plugins/useRedwoodGlobalContextSetter.ts +++ b/packages/graphql-server/src/plugins/useRedwoodGlobalContextSetter.ts @@ -1,6 +1,7 @@ import type { Plugin } from 'graphql-yoga' -import { setContext } from '../index' +import { setContext } from '@redwoodjs/context' + import type { RedwoodGraphQLContext } from '../types' /** diff --git a/packages/graphql-server/src/rootSchema.ts b/packages/graphql-server/src/rootSchema.ts index 948156f04c33..7332d49b94ed 100644 --- a/packages/graphql-server/src/rootSchema.ts +++ b/packages/graphql-server/src/rootSchema.ts @@ -9,14 +9,12 @@ import { } from 'graphql-scalars' import gql from 'graphql-tag' +import type { GlobalContext } from '@redwoodjs/context' + // @TODO move prismaVersion & redwoodVersion to internal? // We don't want a circular dependency here.. const { prismaVersion, redwoodVersion } = require('@redwoodjs/api') -// We duplicate this here, because we don't want circular dependency with graphql-server -// This type doesn't have any real impact outside this file -interface GlobalContext extends Record {} - /** * This adds scalar types for dealing with Date, Time, DateTime, and JSON. * This also adds a root Query type which is needed to start the GraphQL server on a fresh install. diff --git a/packages/internal/src/generate/templates/all-currentUser.d.ts.template b/packages/internal/src/generate/templates/all-currentUser.d.ts.template index d2e095ee6fe4..c9138e88b2a6 100644 --- a/packages/internal/src/generate/templates/all-currentUser.d.ts.template +++ b/packages/internal/src/generate/templates/all-currentUser.d.ts.template @@ -17,7 +17,7 @@ type UndefinedRoles = { type Overwrite = Omit & U -declare module '@redwoodjs/graphql-server' { +declare module '@redwoodjs/context' { interface GlobalContext { currentUser?: Overwrite } diff --git a/packages/internal/src/generate/templates/api-globalContext.d.ts.template b/packages/internal/src/generate/templates/api-globalContext.d.ts.template index 6ede4029cc92..c13d883e4e16 100644 --- a/packages/internal/src/generate/templates/api-globalContext.d.ts.template +++ b/packages/internal/src/generate/templates/api-globalContext.d.ts.template @@ -1,4 +1,4 @@ -import type { GlobalContext } from "@redwoodjs/graphql-server" +import type { GlobalContext } from "@redwoodjs/context" export {} diff --git a/packages/testing/config/jest/api/jest.setup.js b/packages/testing/config/jest/api/jest.setup.js index e1781eef73be..a77bb0ba4abe 100644 --- a/packages/testing/config/jest/api/jest.setup.js +++ b/packages/testing/config/jest/api/jest.setup.js @@ -4,7 +4,6 @@ // @NOTE without these imports in the setup file, mockCurrentUser // will remain undefined in the user's tests // Remember to use specific imports -const { setContext } = require('@redwoodjs/graphql-server/dist/globalContext') const { defineScenario } = require('@redwoodjs/testing/dist/api/scenario') // @NOTE we do this because jest.setup.js runs every time in each context @@ -191,10 +190,6 @@ const seedScenario = async (scenario) => { global.scenario = buildScenario(global.it, global.testPath) global.scenario.only = buildScenario(global.it.only, global.testPath) -global.mockCurrentUser = (currentUser) => { - setContext({ currentUser }) -} - /** * * All these hooks run in the VM/Context that the test runs in since we're using "setupAfterEnv". @@ -218,24 +213,41 @@ const wasDbUsed = () => { } } +// Attempt to emulate the request context isolation behavior +// This is a little more complicated than it would necessarily need to be +// but we're following the same pattern as in `@redwoodjs/context` +const mockContextStore = new Map() +const mockContext = new Proxy( + {}, + { + get: (_target, prop) => { + // Handle toJSON() calls, i.e. JSON.stringify(context) + if (prop === 'toJSON') { + return () => mockContextStore.get('context') + } + return mockContextStore.get('context')[prop] + }, + set: (_target, prop, value) => { + const ctx = mockContextStore.get('context') + ctx[prop] = value + return true + }, + } +) +jest.mock('@redwoodjs/context', () => { + return { + context: mockContext, + setContext: (newContext) => { + mockContextStore.set('context', newContext) + }, + } +}) beforeEach(() => { - // Attempt to emulate the request context isolation behavior - const mockContextStore = new Map() mockContextStore.set('context', {}) - jest - .spyOn( - require('@redwoodjs/graphql-server/dist/globalContextStore'), - 'getAsyncStoreInstance' - ) - // @ts-expect-error - We are not providing the full functionality of the AsyncLocalStorage in this returned object - .mockImplementation(() => { - return { - getStore: () => { - return mockContextStore - }, - } - }) }) +global.mockCurrentUser = (currentUser) => { + mockContextStore.set('context', { currentUser }) +} beforeAll(async () => { if (wasDbUsed()) { diff --git a/packages/testing/package.json b/packages/testing/package.json index e1900250bfa7..aa375801cd13 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -30,6 +30,7 @@ "@babel/runtime-corejs3": "7.23.6", "@redwoodjs/auth": "6.0.7", "@redwoodjs/babel-config": "6.0.7", + "@redwoodjs/context": "6.0.7", "@redwoodjs/graphql-server": "6.0.7", "@redwoodjs/project-config": "6.0.7", "@redwoodjs/router": "6.0.7", diff --git a/packages/testing/src/api/directive.ts b/packages/testing/src/api/directive.ts index 478f01ce2ef9..529a37d82c9a 100644 --- a/packages/testing/src/api/directive.ts +++ b/packages/testing/src/api/directive.ts @@ -1,15 +1,12 @@ import type { A } from 'ts-toolbelt' +import { setContext, context as globalContext } from '@redwoodjs/context' import type { DirectiveParams, ValidatorDirective, TransformerDirective, } from '@redwoodjs/graphql-server' -import { - DirectiveType, - setContext, - context as globalContext, -} from '@redwoodjs/graphql-server' +import { DirectiveType } from '@redwoodjs/graphql-server' export { getDirectiveName } from '@redwoodjs/graphql-server' diff --git a/tasks/k6-test/setups/context_magic_number/templates/benchmarks.ts b/tasks/k6-test/setups/context_magic_number/templates/benchmarks.ts index 0d8be240ebad..791f82cc762d 100644 --- a/tasks/k6-test/setups/context_magic_number/templates/benchmarks.ts +++ b/tasks/k6-test/setups/context_magic_number/templates/benchmarks.ts @@ -1,6 +1,6 @@ import type { MutationResolvers } from 'types/graphql' -import { setContext } from '@redwoodjs/graphql-server' +import { setContext } from '@redwoodjs/context' export const magicNumber: MutationResolvers['magicNumber'] = async ({ value, diff --git a/tasks/k6-test/setups/context_magic_number/templates/func.ts b/tasks/k6-test/setups/context_magic_number/templates/func.ts index 6132e50942ef..d97e08c8a114 100644 --- a/tasks/k6-test/setups/context_magic_number/templates/func.ts +++ b/tasks/k6-test/setups/context_magic_number/templates/func.ts @@ -1,6 +1,6 @@ import type { APIGatewayProxyEvent, Context } from 'aws-lambda' -import { setContext } from '@redwoodjs/graphql-server' +import { setContext } from '@redwoodjs/context' export const handler = async ( event: APIGatewayProxyEvent, diff --git a/yarn.lock b/yarn.lock index 8582c353400c..9a90e154c7af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -992,7 +992,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.0.0, @babel/plugin-syntax-jsx@npm:^7.23.3, @babel/plugin-syntax-jsx@npm:^7.7.2": +"@babel/plugin-syntax-jsx@npm:^7.0.0, @babel/plugin-syntax-jsx@npm:^7.22.5, @babel/plugin-syntax-jsx@npm:^7.23.3, @babel/plugin-syntax-jsx@npm:^7.7.2": version: 7.23.3 resolution: "@babel/plugin-syntax-jsx@npm:7.23.3" dependencies: @@ -1626,17 +1626,17 @@ __metadata: linkType: hard "@babel/plugin-transform-react-jsx@npm:^7.0.0, @babel/plugin-transform-react-jsx@npm:^7.22.15, @babel/plugin-transform-react-jsx@npm:^7.22.5": - version: 7.23.4 - resolution: "@babel/plugin-transform-react-jsx@npm:7.23.4" + version: 7.22.15 + resolution: "@babel/plugin-transform-react-jsx@npm:7.22.15" dependencies: "@babel/helper-annotate-as-pure": "npm:^7.22.5" "@babel/helper-module-imports": "npm:^7.22.15" "@babel/helper-plugin-utils": "npm:^7.22.5" - "@babel/plugin-syntax-jsx": "npm:^7.23.3" - "@babel/types": "npm:^7.23.4" + "@babel/plugin-syntax-jsx": "npm:^7.22.5" + "@babel/types": "npm:^7.22.15" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 8851b3adc515cd91bdb06ff3a23a0f81f0069cfef79dfb3fa744da4b7a82e3555ccb6324c4fa71ecf22508db13b9ff6a0ed96675f95fc87903b9fc6afb699580 + checksum: db37491e3eea5530521e177380312f308f01f806866fa0ce08d48fc5a8c9eaf9a954f778fa1ff477248afb72e916eb66ab3d35254bb6a8979f8b8e74a0fd8873 languageName: node linkType: hard @@ -1748,16 +1748,16 @@ __metadata: linkType: hard "@babel/plugin-transform-typescript@npm:^7.22.15, @babel/plugin-transform-typescript@npm:^7.23.3": - version: 7.23.6 - resolution: "@babel/plugin-transform-typescript@npm:7.23.6" + version: 7.23.3 + resolution: "@babel/plugin-transform-typescript@npm:7.23.3" dependencies: "@babel/helper-annotate-as-pure": "npm:^7.22.5" - "@babel/helper-create-class-features-plugin": "npm:^7.23.6" + "@babel/helper-create-class-features-plugin": "npm:^7.22.15" "@babel/helper-plugin-utils": "npm:^7.22.5" "@babel/plugin-syntax-typescript": "npm:^7.23.3" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: e08f7a981fe157e32031070b92cd77030018b002d063e4be3711ffb7ec04539478b240d8967a4748abb56eccc0ba376f094f30711ef6a028b2a89d15d6ddc01f + checksum: a3c738efcf491ceb1eee646f57c44990ee0c80465527b88fcfa0b7602688c4ff8c165a4c5b62caf05d968b095212018fd30a02879c12d37c657081f57b31fb26 languageName: node linkType: hard @@ -2025,7 +2025,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.1.6, @babel/types@npm:^7.16.8, @babel/types@npm:^7.18.13, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.4, @babel/types@npm:^7.23.6, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.1.6, @babel/types@npm:^7.16.8, @babel/types@npm:^7.18.13, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.6, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.23.6 resolution: "@babel/types@npm:7.23.6" dependencies: @@ -8076,6 +8076,7 @@ __metadata: "@fastify/http-proxy": "npm:9.3.0" "@fastify/static": "npm:6.12.0" "@fastify/url-data": "npm:5.4.0" + "@redwoodjs/context": "npm:6.0.7" "@redwoodjs/project-config": "npm:6.0.7" "@types/aws-lambda": "npm:8.10.126" "@types/lodash": "npm:4.14.201" @@ -8820,6 +8821,17 @@ __metadata: languageName: unknown linkType: soft +"@redwoodjs/context@npm:6.0.7, @redwoodjs/context@workspace:packages/context": + version: 0.0.0-use.local + resolution: "@redwoodjs/context@workspace:packages/context" + dependencies: + esbuild: "npm:0.19.9" + fast-glob: "npm:3.3.2" + jest: "npm:29.7.0" + typescript: "npm:5.3.3" + languageName: unknown + linkType: soft + "@redwoodjs/core@workspace:packages/core": version: 0.0.0-use.local resolution: "@redwoodjs/core@workspace:packages/core" @@ -8933,6 +8945,7 @@ __metadata: "@fastify/http-proxy": "npm:9.3.0" "@fastify/static": "npm:6.12.0" "@fastify/url-data": "npm:5.4.0" + "@redwoodjs/context": "npm:6.0.7" "@redwoodjs/graphql-server": "npm:6.0.7" "@redwoodjs/project-config": "npm:6.0.7" "@types/aws-lambda": "npm:8.10.126" @@ -8999,6 +9012,7 @@ __metadata: "@graphql-yoga/plugin-persisted-operations": "npm:2.0.5" "@opentelemetry/api": "npm:1.7.0" "@redwoodjs/api": "npm:6.0.7" + "@redwoodjs/context": "npm:6.0.7" "@redwoodjs/project-config": "npm:6.0.7" "@redwoodjs/realtime": "npm:6.0.7" "@types/jsonwebtoken": "npm:9.0.5" @@ -9456,6 +9470,7 @@ __metadata: "@babel/runtime-corejs3": "npm:7.23.6" "@redwoodjs/auth": "npm:6.0.7" "@redwoodjs/babel-config": "npm:6.0.7" + "@redwoodjs/context": "npm:6.0.7" "@redwoodjs/graphql-server": "npm:6.0.7" "@redwoodjs/project-config": "npm:6.0.7" "@redwoodjs/router": "npm:6.0.7" @@ -10817,13 +10832,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.3.101": - version: 1.3.101 - resolution: "@swc/core-darwin-arm64@npm:1.3.101" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@swc/core-darwin-arm64@npm:1.3.60": version: 1.3.60 resolution: "@swc/core-darwin-arm64@npm:1.3.60" @@ -10831,10 +10839,10 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.3.101": - version: 1.3.101 - resolution: "@swc/core-darwin-x64@npm:1.3.101" - conditions: os=darwin & cpu=x64 +"@swc/core-darwin-arm64@npm:1.3.96": + version: 1.3.96 + resolution: "@swc/core-darwin-arm64@npm:1.3.96" + conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -10845,10 +10853,10 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.3.101": - version: 1.3.101 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.101" - conditions: os=linux & cpu=arm +"@swc/core-darwin-x64@npm:1.3.96": + version: 1.3.96 + resolution: "@swc/core-darwin-x64@npm:1.3.96" + conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -10859,10 +10867,10 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.3.101": - version: 1.3.101 - resolution: "@swc/core-linux-arm64-gnu@npm:1.3.101" - conditions: os=linux & cpu=arm64 & libc=glibc +"@swc/core-linux-arm-gnueabihf@npm:1.3.96": + version: 1.3.96 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.96" + conditions: os=linux & cpu=arm languageName: node linkType: hard @@ -10873,10 +10881,10 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.3.101": - version: 1.3.101 - resolution: "@swc/core-linux-arm64-musl@npm:1.3.101" - conditions: os=linux & cpu=arm64 & libc=musl +"@swc/core-linux-arm64-gnu@npm:1.3.96": + version: 1.3.96 + resolution: "@swc/core-linux-arm64-gnu@npm:1.3.96" + conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard @@ -10887,10 +10895,10 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.3.101": - version: 1.3.101 - resolution: "@swc/core-linux-x64-gnu@npm:1.3.101" - conditions: os=linux & cpu=x64 & libc=glibc +"@swc/core-linux-arm64-musl@npm:1.3.96": + version: 1.3.96 + resolution: "@swc/core-linux-arm64-musl@npm:1.3.96" + conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard @@ -10901,10 +10909,10 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.3.101": - version: 1.3.101 - resolution: "@swc/core-linux-x64-musl@npm:1.3.101" - conditions: os=linux & cpu=x64 & libc=musl +"@swc/core-linux-x64-gnu@npm:1.3.96": + version: 1.3.96 + resolution: "@swc/core-linux-x64-gnu@npm:1.3.96" + conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard @@ -10915,10 +10923,10 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.3.101": - version: 1.3.101 - resolution: "@swc/core-win32-arm64-msvc@npm:1.3.101" - conditions: os=win32 & cpu=arm64 +"@swc/core-linux-x64-musl@npm:1.3.96": + version: 1.3.96 + resolution: "@swc/core-linux-x64-musl@npm:1.3.96" + conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard @@ -10929,10 +10937,10 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.3.101": - version: 1.3.101 - resolution: "@swc/core-win32-ia32-msvc@npm:1.3.101" - conditions: os=win32 & cpu=ia32 +"@swc/core-win32-arm64-msvc@npm:1.3.96": + version: 1.3.96 + resolution: "@swc/core-win32-arm64-msvc@npm:1.3.96" + conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -10943,10 +10951,10 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.3.101": - version: 1.3.101 - resolution: "@swc/core-win32-x64-msvc@npm:1.3.101" - conditions: os=win32 & cpu=x64 +"@swc/core-win32-ia32-msvc@npm:1.3.96": + version: 1.3.96 + resolution: "@swc/core-win32-ia32-msvc@npm:1.3.96" + conditions: os=win32 & cpu=ia32 languageName: node linkType: hard @@ -10957,6 +10965,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-x64-msvc@npm:1.3.96": + version: 1.3.96 + resolution: "@swc/core-win32-x64-msvc@npm:1.3.96" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@swc/core@npm:1.3.60": version: 1.3.60 resolution: "@swc/core@npm:1.3.60" @@ -11002,19 +11017,19 @@ __metadata: linkType: hard "@swc/core@npm:^1.3.82": - version: 1.3.101 - resolution: "@swc/core@npm:1.3.101" - dependencies: - "@swc/core-darwin-arm64": "npm:1.3.101" - "@swc/core-darwin-x64": "npm:1.3.101" - "@swc/core-linux-arm-gnueabihf": "npm:1.3.101" - "@swc/core-linux-arm64-gnu": "npm:1.3.101" - "@swc/core-linux-arm64-musl": "npm:1.3.101" - "@swc/core-linux-x64-gnu": "npm:1.3.101" - "@swc/core-linux-x64-musl": "npm:1.3.101" - "@swc/core-win32-arm64-msvc": "npm:1.3.101" - "@swc/core-win32-ia32-msvc": "npm:1.3.101" - "@swc/core-win32-x64-msvc": "npm:1.3.101" + version: 1.3.96 + resolution: "@swc/core@npm:1.3.96" + dependencies: + "@swc/core-darwin-arm64": "npm:1.3.96" + "@swc/core-darwin-x64": "npm:1.3.96" + "@swc/core-linux-arm-gnueabihf": "npm:1.3.96" + "@swc/core-linux-arm64-gnu": "npm:1.3.96" + "@swc/core-linux-arm64-musl": "npm:1.3.96" + "@swc/core-linux-x64-gnu": "npm:1.3.96" + "@swc/core-linux-x64-musl": "npm:1.3.96" + "@swc/core-win32-arm64-msvc": "npm:1.3.96" + "@swc/core-win32-ia32-msvc": "npm:1.3.96" + "@swc/core-win32-x64-msvc": "npm:1.3.96" "@swc/counter": "npm:^0.1.1" "@swc/types": "npm:^0.1.5" peerDependencies: @@ -11043,7 +11058,7 @@ __metadata: peerDependenciesMeta: "@swc/helpers": optional: true - checksum: 167e9decb494fbd66b57115eab8fa1ae23c7dae009597812db04df2c8434283ae028adfd4bfe5a6ac15ffbba8f2651c0460da8025d532efc1212ef94d70e271f + checksum: 273d4894d9f62b72a3f4e84d351bc426ba33055bb2fd38f743777c5ac802365bf61dec2e12552252fbdb705c96dd9688534740e23746ddb98a59b4e45af64369 languageName: node linkType: hard @@ -11783,12 +11798,12 @@ __metadata: linkType: hard "@types/eslint@npm:*, @types/eslint@npm:8": - version: 8.44.7 - resolution: "@types/eslint@npm:8.44.7" + version: 8.40.2 + resolution: "@types/eslint@npm:8.40.2" dependencies: "@types/estree": "npm:*" "@types/json-schema": "npm:*" - checksum: 447b55ccff47668fc63466728e7e598ae16a03de8d489350e855b6020ad16f58a703e75b875376dd6cd5fcab630311a805fa7f934476637ea35819f01c9db3ca + checksum: 5797dce7805f601ee34b2f63d6a80dba21302e2fe2614c7990eca7a22472f9e0c386d56d82fe79a7cdede57c8dcc1e0f9b1e5dc384adf736833b901ffcc29628 languageName: node linkType: hard @@ -12112,11 +12127,11 @@ __metadata: linkType: hard "@types/memjs@npm:1": - version: 1.3.3 - resolution: "@types/memjs@npm:1.3.3" + version: 1.3.0 + resolution: "@types/memjs@npm:1.3.0" dependencies: "@types/node": "npm:*" - checksum: 7179534b5f16e750fdc4e45242bf657b061ccbb876c721fc85c45b46cf9603503f286a9f2139cc86a95e1e2c6c7c8fce678fd8765ad5e1f9a888e5014d03a2b0 + checksum: 71d8dda576405a8ca16b6b312a546e4703f102a24d39c64d486b9bf6cdb13498ebf1c14e3b5ce583baeae47f84f21576c64950ba094b14ccd72639106a17dfd5 languageName: node linkType: hard @@ -12170,11 +12185,11 @@ __metadata: linkType: hard "@types/mjml@npm:4": - version: 4.7.4 - resolution: "@types/mjml@npm:4.7.4" + version: 4.7.1 + resolution: "@types/mjml@npm:4.7.1" dependencies: "@types/mjml-core": "npm:*" - checksum: 6f4bbdf709e1f6c9b26be67146b1e4c759142fb4ddfa4c079b600835701bb2039c60cd530d016f1d2f1aef4256580e30cdc942c3acd6935e2fe56b5a665795ae + checksum: 52dc7cf2388f8def26ae3fae3a230650d4aa76de10a0ef832a4db0c9a8a20daed7520ceeecee037992a884e422a18735c096ca48e8bc2bbb6a3e76fa02ba3d74 languageName: node linkType: hard @@ -14299,9 +14314,9 @@ __metadata: linkType: hard "async@npm:^3.1.0, async@npm:^3.2.0, async@npm:^3.2.3, async@npm:^3.2.4": - version: 3.2.5 - resolution: "async@npm:3.2.5" - checksum: 1408287b26c6db67d45cb346e34892cee555b8b59e6c68e6f8c3e495cad5ca13b4f218180e871f3c2ca30df4ab52693b66f2f6ff43644760cab0b2198bda79c1 + version: 3.2.4 + resolution: "async@npm:3.2.4" + checksum: b5d02fed64717edf49e35b2b156debd9cf524934ea670108fa5528e7615ed66a5e0bf6c65f832c9483b63aa7f0bffe3e588ebe8d58a539b833798d324516e1c9 languageName: node linkType: hard @@ -14985,7 +15000,7 @@ __metadata: languageName: node linkType: hard -"bn.js@npm:^5.0.0, bn.js@npm:^5.2.1": +"bn.js@npm:^5.0.0, bn.js@npm:^5.1.1": version: 5.2.1 resolution: "bn.js@npm:5.2.1" checksum: bed3d8bd34ec89dbcf9f20f88bd7d4a49c160fda3b561c7bb227501f974d3e435a48fb9b61bc3de304acab9215a3bda0803f7017ffb4d0016a0c3a740a283caa @@ -15160,7 +15175,7 @@ __metadata: languageName: node linkType: hard -"browserify-rsa@npm:^4.0.0, browserify-rsa@npm:^4.1.0": +"browserify-rsa@npm:^4.0.0, browserify-rsa@npm:^4.0.1": version: 4.1.0 resolution: "browserify-rsa@npm:4.1.0" dependencies: @@ -15171,19 +15186,19 @@ __metadata: linkType: hard "browserify-sign@npm:^4.0.0": - version: 4.2.2 - resolution: "browserify-sign@npm:4.2.2" + version: 4.2.1 + resolution: "browserify-sign@npm:4.2.1" dependencies: - bn.js: "npm:^5.2.1" - browserify-rsa: "npm:^4.1.0" + bn.js: "npm:^5.1.1" + browserify-rsa: "npm:^4.0.1" create-hash: "npm:^1.2.0" create-hmac: "npm:^1.1.7" - elliptic: "npm:^6.5.4" + elliptic: "npm:^6.5.3" inherits: "npm:^2.0.4" - parse-asn1: "npm:^5.1.6" - readable-stream: "npm:^3.6.2" - safe-buffer: "npm:^5.2.1" - checksum: 4d1292e5c165d93455630515003f0e95eed9239c99e2d373920c5b56903d16296a3d23cd4bdc4d298f55ad9b83714a9e63bc4839f1166c303349a16e84e9b016 + parse-asn1: "npm:^5.1.5" + readable-stream: "npm:^3.6.0" + safe-buffer: "npm:^5.2.0" + checksum: 8f00a370e3e97060977dc58e51251d3ca398ee73523994a44430321e8de2c7d85395362d59014b2b07efe4190f369baee2ff28eb8f405ff4660b776651cf052d languageName: node linkType: hard @@ -18568,7 +18583,7 @@ __metadata: languageName: node linkType: hard -"elliptic@npm:^6.5.3, elliptic@npm:^6.5.4": +"elliptic@npm:^6.5.3": version: 6.5.4 resolution: "elliptic@npm:6.5.4" dependencies: @@ -28669,7 +28684,7 @@ __metadata: languageName: node linkType: hard -"parse-asn1@npm:^5.0.0, parse-asn1@npm:^5.1.6": +"parse-asn1@npm:^5.0.0, parse-asn1@npm:^5.1.5": version: 5.1.6 resolution: "parse-asn1@npm:5.1.6" dependencies: @@ -30906,7 +30921,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^3.0.0, readable-stream@npm:^3.0.2, readable-stream@npm:^3.0.6, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0, readable-stream@npm:^3.6.2": +"readable-stream@npm:^3.0.0, readable-stream@npm:^3.0.2, readable-stream@npm:^3.0.6, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: @@ -31817,7 +31832,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: 6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 @@ -33104,12 +33119,12 @@ __metadata: linkType: hard "streamx@npm:^2.15.0": - version: 2.15.5 - resolution: "streamx@npm:2.15.5" + version: 2.15.4 + resolution: "streamx@npm:2.15.4" dependencies: fast-fifo: "npm:^1.1.0" queue-tick: "npm:^1.0.1" - checksum: 7998d1fa3324131ed94efc4a4e8b22e0f60267b21d8f8fac8c605eaa1a6d6358adbc38c35b407be0eb8cc09a223c641962afb0db29ecbe92118242118946d93c + checksum: 878aeea3a82dc2cdfe74055279cea49ac94daebc5abd46468f29ce001bbcd4c9a0cf5bae771971f6abc73e1e3d3156b609cf1702bd63f97eaa7216386a975ef7 languageName: node linkType: hard