Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/meshArtifact.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ function resolveComposerAsTypeScriptModule(data) {
* ```
*/
function resolveComposerAsJavaScriptModule(data) {
return data.replace(
/"composer":\s*"((?!https:\/\/)[^#]+)#([^"]+)"/,
return data.replaceAll(
/"composer":\s*"((?!https:\/\/)[^#]+)#([^"]+)"/g,
`"module": __importStar(require("$1")), "fn": "$2"`,
);
}
Expand Down
8 changes: 6 additions & 2 deletions src/server.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { getMesh } from '@graphql-mesh/runtime';

import { KvStateApiImpl } from './state';

const { getCorsOptions } = require('./cors');
const { createYoga } = require('graphql-yoga');
const { GraphQLError } = require('graphql/error');
Expand Down Expand Up @@ -46,13 +48,14 @@ async function getBuiltMesh(meshArtifacts, meshConfig) {
}

const buildServer = async (loggerInstance, env, meshArtifacts, meshConfig) => {
const { Secret: secret } = env;
const { SECRETS } = env;
const tenantMesh = await getBuiltMesh(meshArtifacts, meshConfig);
const meshSecrets = loadMeshSecrets(loggerInstance, secret);
const meshSecrets = loadMeshSecrets(loggerInstance, SECRETS);
return await buildYogaServer(env, tenantMesh, meshConfig, meshSecrets);
};

async function buildYogaServer(env, tenantMesh, meshConfig, meshSecrets) {
const stateApi = new KvStateApiImpl(env, meshConfig);
const secretsProxy = new Proxy(meshSecrets, getSecretsHandler);
return createYoga({
plugins: tenantMesh.plugins,
Expand All @@ -61,6 +64,7 @@ async function buildYogaServer(env, tenantMesh, meshConfig, meshSecrets) {
context: initialContext => ({
...initialContext,
secrets: secretsProxy,
state: stateApi,
}),
maskedErrors: {
maskError: maskError,
Expand Down
155 changes: 155 additions & 0 deletions src/state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { GraphQLError } from 'graphql/error';

/**
* Whether string is empty.
* @param value {string | null | undefined} String value.
*/
const isEmptyString = value => {
return value === '' || value === null || value === undefined;
};

/**
* Get byte length of a string.
* @param value {string} String value.
*/
const getByteLength = value => {
return new TextEncoder().encode(value).length;
};

/**
* Abstract class for state API implementations.
*/
class StateApiAbstract {
#env;

constructor(env) {
this.#env = env;
}

getEnv() {
return this.#env;
}

/**
* Validate a key.
* @param key {string} Key to validate.
*/
validateKey(key) {
if (isEmptyString(key)) {
throw new Error(`Invalid key: ${key}`);
}
const keyBytes = getByteLength(key);
const env = this.getEnv();
if (keyBytes > env.MAX_STATE_KEY_SIZE_BYTES) {
throw new Error(
`Key ${key} exceeds maximum key size of ${env.MAX_STATE_KEY_SIZE_BYTES}. Received ${keyBytes} B.`,
);
}
}

/**
* Validate a value
* @param key {string} Key.
* @param value {string} Value to validate.
*/
validateValue(key, value) {
if (isEmptyString(value)) {
throw new Error(`Invalid value for key: ${key}`);
}
const valueBytes = getByteLength(value);
const env = this.getEnv();
if (valueBytes > env.MAX_STATE_VALUE_SIZE_BYTES) {
throw new Error(
`Key ${key} value exceeds maximum key size of ${env.MAX_STATE_VALUE_SIZE_BYTES}. Received ${valueBytes} B.`,
);
}
}

/**
* Get the TTL in milliseconds based on the provided config or environment defaults.
* @param config {{ ttl?: number } | undefined} Configuration object that may contain a TTL value.
*/
getTtl(config) {
const env = this.getEnv();
// Default to maximum ttl if not provided
let useTtl = config?.ttl || env.MAX_STATE_TTL_SECONDS;
// Set TTL to max when over maximum TTL
if (useTtl > env.MAX_STATE_TTL_SECONDS) {
useTtl = env.MAX_STATE_TTL_SECONDS;
}
// Set TTL to min seconds when under minimum TTL
if (useTtl < env.MIN_STATE_TTL_SECONDS) {
useTtl = env.MIN_STATE_TTL_SECONDS;
}
return useTtl;
}
}

/**
* State API implementation using Cloudflare's KV as the backing store. KV is a distributed key-value store that allows
* for storing and retrieving key-value pairs across different worker instances with eventual consistency.
*/
class KvStateApiImpl extends StateApiAbstract {
meshConfig;

constructor(env, meshConfig) {
super(env);
this.meshConfig = meshConfig;
}

/**
* Ensure that the KV namespace is configured in the environment.
* @throws {GraphQLError} when the KV namespace is not configured.
*/
ensureKvConfigured() {
if (!this.getEnv().MESH_KV_NAMESPACE || !this.meshConfig?.state?.enabled) {
throw new GraphQLError(
'Context state is not configured for this mesh. Please check your mesh configuration and try again.',
{
extensions: {
code: 'ERROR_CONTEXT_STATE_NOT_CONFIGURED',
},
},
);
}
}

/**
* Get a value by key.
* @param key {key} Key to retrieve.
* @return {Promise<string | null>}
*/
async get(key) {
this.ensureKvConfigured();
this.validateKey(key);
return this.getEnv().MESH_KV_NAMESPACE.get(key);
}

/**
* Put a key-value pair with optional TTL.
* @param key {string} Key to store.
* @param value {string} Value to store.
* @param config {{ ttl?: number } | undefined} Optional configuration object that may contain a TTL value in seconds.
* @return {Promise<void>}
*/
async put(key, value, config) {
this.ensureKvConfigured();
this.validateKey(key);
this.validateValue(key, value);
const ttl = this.getTtl(config);
return this.getEnv().MESH_KV_NAMESPACE.put(key, value, { expirationTtl: ttl });
}

/**
* Delete a key-value pair.
* @param key {string} Key to delete.
* @return {Promise<void>}
*/
async delete(key) {
this.ensureKvConfigured();
this.validateKey(key);
return this.getEnv().MESH_KV_NAMESPACE.delete(key);
}
}

module.exports = { KvStateApiImpl };
12 changes: 10 additions & 2 deletions src/templates/gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Dependencies
node_modules

# Temporary files
.wrangler
.mesh
tenantFiles
tempfiles
mesh-artifact
meshes
tempfiles
*.txt

# Template files from @adobe/aio-cli-plugin-api-mesh
wrangler.toml
12 changes: 9 additions & 3 deletions src/templates/wrangler.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
# Tenant worker core definition. For platform workers metadata configs are used to set bindings.
name = "tenant-worker-core"
compatibility_date = "2024-06-03"
node_compat = false

find_additional_modules = true
base_dir = ".mesh"
rules = [
{ type = "CommonJS", globs = ["tenantFiles/**/*.js"] }
]

kv_namespaces = [
{ binding = "MESH_KV_NAMESPACE", id = "local-kv" }
]

[[migrations]]
tag = "v1"
[vars]
# State configuration
MAX_STATE_KEY_SIZE_BYTES="512" # 512 bytes
MAX_STATE_VALUE_SIZE_BYTES="1048576" # 1 MiB
MIN_STATE_TTL_SECONDS="60" # 1 minute
MAX_STATE_TTL_SECONDS="604800" # 7 days
32 changes: 32 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,38 @@ function getFilesInMeshConfig(data, meshConfigName) {
filesList.push(filename);
}
}

if (plugin.hooks.afterAll) {
const composer = plugin.hooks.afterAll.composer;
if (composer && !fileURLRegex.test(composer)) {
const [filename] = composer.split('#');
filesList.push(filename);
}
}

if (plugin.hooks.beforeSource) {
Object.values(plugin.hooks.beforeSource).forEach(beforeSourceHooks => {
beforeSourceHooks?.forEach(hook => {
const composer = hook.composer;
if (composer && !fileURLRegex.test(composer)) {
const [filename] = composer.split('#');
filesList.push(filename);
}
});
});
}

if (plugin.hooks.afterSource) {
Object.values(plugin.hooks.afterSource).forEach(afterSourceHooks => {
afterSourceHooks?.forEach(hook => {
const composer = hook.composer;
if (composer && !fileURLRegex.test(composer)) {
const [filename] = composer.split('#');
filesList.push(filename);
}
});
});
}
}
});

Expand Down
8 changes: 6 additions & 2 deletions src/wranglerCli.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { spawn } = require('child_process');
const { readSecretsFile } = require('./serverUtils');
const packageData = require('../package.json');
const { join } = require('node:path');
const fs = require('node:fs');

/**
* Starts the wrangler dev server
Expand All @@ -13,7 +14,10 @@ const { join } = require('node:path');
const start = (command, port, debug, inspectPort) => {
const wranglerPackageNumber = packageData.dependencies.wrangler;
const wranglerVersion = `wrangler@${wranglerPackageNumber.replace(/^[\^~]/, '')}`;
const wranglerToml = join(__dirname, '..', 'wrangler.toml');
// Project wrangler.toml must be used to avoid issues w/ Wrangler. Ensure it is up to date.
const wranglerTomlTemplate = join(__dirname, '..', 'wrangler.toml');
const wranglerToml = join(process.cwd(), 'wrangler.toml');
fs.cpSync(wranglerTomlTemplate, wranglerToml);
const meshDir = '.mesh';
const secrets = readSecretsFile(meshDir);
const entrypoint = join(__dirname, 'worker.js');
Expand All @@ -29,7 +33,7 @@ const start = (command, port, debug, inspectPort) => {
'--show-interactive-dev-session',
'false',
'--var',
`Secret:${JSON.stringify(secrets)}`,
`SECRETS:${JSON.stringify(secrets)}`,
'--port',
port,
'--inspector-port',
Expand Down
11 changes: 9 additions & 2 deletions wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ rules = [
{ type = "CommonJS", globs = ["tenantFiles/**/*.js"] }
]

kv_namespaces = [
{ binding = "MESH_KV_NAMESPACE", id = "local-kv" }
]

[[migrations]]
tag = "v1"
[vars]
# State configuration
MAX_STATE_KEY_SIZE_BYTES="512" # 512 bytes
MAX_STATE_VALUE_SIZE_BYTES="1048576" # 1 MiB
MIN_STATE_TTL_SECONDS="60" # 1 minute
MAX_STATE_TTL_SECONDS="604800" # 7 days
18 changes: 9 additions & 9 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2378,9 +2378,9 @@
tslib "^2.4.0"

"@graphql-tools/utils@^10.8.0":
version "10.8.6"
resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-10.8.6.tgz#69ef29e408a27919108b2b2227fe8b465acf9e5c"
integrity sha512-Alc9Vyg0oOsGhRapfL3xvqh1zV8nKoFUdtLhXX7Ki4nClaIJXckrA86j+uxEuG3ic6j4jlM1nvcWXRn/71AVLQ==
version "10.9.1"
resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-10.9.1.tgz#ff4067256f2080db0c66f4475858f29a8da65ecf"
integrity sha512-B1wwkXk9UvU7LCBkPs8513WxOQ2H8Fo5p8HR1+Id9WmYE5+bd51vqN+MbrqvWczHCH2gwkREgHJN88tE0n1FCw==
dependencies:
"@graphql-typed-document-node/core" "^3.1.1"
"@whatwg-node/promise-helpers" "^1.0.0"
Expand Down Expand Up @@ -5696,9 +5696,9 @@ expect@^29.7.0:
jest-util "^29.7.0"

exsolve@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.4.tgz#7de5c75af82ecd15998328fbf5f2295883be3a39"
integrity sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==
version "1.0.7"
resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.7.tgz#3b74e4c7ca5c5f9a19c3626ca857309fa99f9e9e"
integrity sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==

external-editor@^3.0.3:
version "3.1.0"
Expand Down Expand Up @@ -10315,9 +10315,9 @@ ua-parser-js@^1.0.35:
integrity sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==

ufo@^1.5.4:
version "1.5.4"
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754"
integrity sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==
version "1.6.1"
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.1.tgz#ac2db1d54614d1b22c1d603e3aef44a85d8f146b"
integrity sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==

uglify-js@^3.1.4:
version "3.17.4"
Expand Down
Loading