Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d06b0c5
chore: updated meshbuilder and plugin hooks version that supports aft…
nvyasadobe Jul 17, 2025
73447f0
chore: plugin-hooks version updated for localdev init
nvyasadobe Jul 17, 2025
ff0b824
Merge branch 'develop' of github.com:adobe/aio-cli-plugin-api-mesh in…
nvyasadobe Jul 28, 2025
5f16de6
chore: updated dependency in package json and also sync from develop
nvyasadobe Jul 28, 2025
b29a4c0
feat: updated dependecies and bump alpha cli version
nvyasadobe Aug 8, 2025
24e7d89
Merge pull request #272 from adobe/cext-1442-afterAll-cli
nvyasadobe Aug 8, 2025
96d3262
feat: bumped version as beta in CLI for new hooks release
nvyasadobe Aug 8, 2025
b8774d7
chore: took pull and fix conflicts
nvyasadobe Aug 8, 2025
486bc59
chore: bumped cli version to beta version
nvyasadobe Aug 8, 2025
1b49575
chore: bumped to stable version
nvyasadobe Aug 8, 2025
7f99ea2
chore(local-dev): - Supported hooks/state in localdev.
Aug 8, 2025
bf6bd37
Merge branch 'epic/hooks-context-support' into feature/CEXT-4838-loca…
Aug 8, 2025
020f42a
chore(local-dev): - Supported hooks/state in localdev.
Aug 8, 2025
53b3e33
chore(local-dev): - Replace all instances of the composer.
Aug 8, 2025
07131e5
chore(local-dev): - Return state error when state not configured.
Aug 8, 2025
73d31b5
feature: hooks and context config support (#277)
nvyasadobe Aug 8, 2025
f24c693
Merge branch 'develop' into feature/CEXT-4838-localdev-support
Aug 8, 2025
50de3b5
Merge pull request #278 from adobe/feature/CEXT-4838-localdev-support
brasewel Aug 8, 2025
3c6b765
Merge branch 'main' into release/5.6.0
brasewel Aug 11, 2025
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
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/aio-cli-plugin-api-mesh",
"version": "5.5.0",
"version": "5.6.0",
"description": "Adobe I/O CLI plugin to develop and manage API mesh sources",
"keywords": [
"oclif-plugin"
Expand Down Expand Up @@ -38,13 +38,13 @@
"version": "oclif-dev readme && git add README.md"
},
"dependencies": {
"@adobe-apimesh/mesh-builder": "2.2.0",
"@adobe-apimesh/mesh-builder": "2.3.1",
"@adobe/aio-cli-lib-console": "^5.0.0",
"@adobe/aio-lib-core-config": "^5.0.0",
"@adobe/aio-lib-core-logging": "^3.0.0",
"@adobe/aio-lib-env": "^3.0.0",
"@adobe/aio-lib-ims": "^7.0.1",
"@adobe/plugin-hooks": "0.3.4",
"@adobe/plugin-hooks": "0.4.0",
"@adobe/plugin-on-fetch": "0.1.1",
"@adobe/plugin-source-headers": "^0.0.2",
"@envelop/disable-introspection": "^6.0.0",
Expand Down
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
2 changes: 1 addition & 1 deletion src/templates/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"@graphql-mesh/soap": "0.14.25",
"@graphql-mesh/http": "^0.96.9",
"graphql": "^16.6.0",
"@adobe/plugin-hooks": "0.3.3",
"@adobe/plugin-hooks": "0.4.0",
"@adobe/plugin-on-fetch": "0.1.1",
"eslint-plugin-no-loops": "^0.3.0",
"eslint-plugin-node": "^11.1.0",
Expand Down
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
Loading
Loading