wgc 0.79.5 → 0.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +13 -4
- package/dist/src/commands/router/commands/compose.js +171 -89
- package/dist/src/commands/router/commands/compose.js.map +1 -1
- package/dist/src/commands/router/index.js +5 -1
- package/dist/src/commands/router/index.js.map +1 -1
- package/dist/src/commands/router/plugin/commands/build.d.ts +4 -0
- package/dist/src/commands/router/plugin/commands/build.js +76 -0
- package/dist/src/commands/router/plugin/commands/build.js.map +1 -0
- package/dist/src/commands/router/plugin/commands/init.d.ts +4 -0
- package/dist/src/commands/router/plugin/commands/init.js +98 -0
- package/dist/src/commands/router/plugin/commands/init.js.map +1 -0
- package/dist/src/commands/router/plugin/commands/test.d.ts +4 -0
- package/dist/src/commands/router/plugin/commands/test.js +62 -0
- package/dist/src/commands/router/plugin/commands/test.js.map +1 -0
- package/dist/src/commands/router/plugin/helper.d.ts +10 -0
- package/dist/src/commands/router/plugin/helper.js +47 -0
- package/dist/src/commands/router/plugin/helper.js.map +1 -0
- package/dist/src/commands/router/plugin/index.d.ts +4 -0
- package/dist/src/commands/router/plugin/index.js +13 -0
- package/dist/src/commands/router/plugin/index.js.map +1 -0
- package/dist/src/commands/router/plugin/templates/go-plugin.d.ts +5 -0
- package/dist/src/commands/router/plugin/templates/go-plugin.js +342 -0
- package/dist/src/commands/router/plugin/templates/go-plugin.js.map +1 -0
- package/dist/src/commands/router/plugin/toolchain.d.ts +36 -0
- package/dist/src/commands/router/plugin/toolchain.js +413 -0
- package/dist/src/commands/router/plugin/toolchain.js.map +1 -0
- package/dist/src/core/config.d.ts +1 -0
- package/dist/src/core/config.js +1 -0
- package/dist/src/core/config.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -7
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { access, mkdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { Command, program } from 'commander';
|
|
5
|
+
import { resolve } from 'pathe';
|
|
6
|
+
import pc from 'picocolors';
|
|
7
|
+
import pupa from 'pupa';
|
|
8
|
+
import Spinner from 'ora';
|
|
9
|
+
import { compileGraphQLToMapping, compileGraphQLToProto } from '@wundergraph/protographic';
|
|
10
|
+
import { camelCase, upperFirst } from 'lodash-es';
|
|
11
|
+
import { goMod, mainGo, mainGoTest, readme, schema } from '../templates/go-plugin.js';
|
|
12
|
+
import { renderResultTree } from '../helper.js';
|
|
13
|
+
export default (opts) => {
|
|
14
|
+
const command = new Command('init');
|
|
15
|
+
command.description('Scaffold a new gRPC router plugin');
|
|
16
|
+
command.argument('name', 'Name of the plugin');
|
|
17
|
+
command.option('-d, --directory <directory>', 'Directory to create the plugin in', '.');
|
|
18
|
+
command.option('-l, --language <language>', 'Programming language to use for the plugin', 'go');
|
|
19
|
+
command.action(async (name, options) => {
|
|
20
|
+
const startTime = performance.now();
|
|
21
|
+
const pluginDir = resolve(process.cwd(), options.directory, name);
|
|
22
|
+
name = upperFirst(camelCase(name));
|
|
23
|
+
const serviceName = name + 'Service';
|
|
24
|
+
// Check if a directory exists
|
|
25
|
+
try {
|
|
26
|
+
await access(pluginDir);
|
|
27
|
+
program.error(pc.red(`Plugin ${name} already exists in ${pluginDir}`));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Directory doesn't exist, we can proceed
|
|
31
|
+
}
|
|
32
|
+
const spinner = Spinner({ text: 'Creating plugin...' });
|
|
33
|
+
// Create a temporary directory
|
|
34
|
+
const tempDir = resolve(tmpdir(), `cosmo-plugin-${randomUUID()}`);
|
|
35
|
+
spinner.start();
|
|
36
|
+
try {
|
|
37
|
+
spinner.text = 'Creating directories...';
|
|
38
|
+
await mkdir(tempDir, { recursive: true });
|
|
39
|
+
const srcDir = resolve(tempDir, 'src');
|
|
40
|
+
await mkdir(srcDir, { recursive: true });
|
|
41
|
+
const generatedDir = resolve(tempDir, 'generated');
|
|
42
|
+
await mkdir(generatedDir, { recursive: true });
|
|
43
|
+
spinner.text = 'Checkout templates...';
|
|
44
|
+
if (options.language.toLowerCase() !== 'go') {
|
|
45
|
+
spinner.fail(pc.yellow(`Language '${options.language}' is not supported yet. Using 'go' instead.`));
|
|
46
|
+
options.language = 'go';
|
|
47
|
+
}
|
|
48
|
+
await writeFile(resolve(tempDir, 'README.md'), pupa(readme, { name }));
|
|
49
|
+
await writeFile(resolve(srcDir, 'schema.graphql'), pupa(schema, { name }));
|
|
50
|
+
spinner.text = 'Generating mapping and proto files...';
|
|
51
|
+
const mapping = compileGraphQLToMapping(schema, serviceName);
|
|
52
|
+
await writeFile(resolve(generatedDir, 'mapping.json'), JSON.stringify(mapping, null, 2));
|
|
53
|
+
const goModulePath = 'github.com/wundergraph/cosmo/plugin';
|
|
54
|
+
const proto = compileGraphQLToProto(schema, {
|
|
55
|
+
serviceName,
|
|
56
|
+
packageName: 'service',
|
|
57
|
+
goPackage: goModulePath,
|
|
58
|
+
});
|
|
59
|
+
await writeFile(resolve(generatedDir, 'service.proto'), proto.proto);
|
|
60
|
+
await writeFile(resolve(generatedDir, 'service.proto.lock.json'), JSON.stringify(proto.lockData, null, 2));
|
|
61
|
+
await writeFile(resolve(srcDir, 'main.go'), pupa(mainGo, { serviceName }));
|
|
62
|
+
await writeFile(resolve(srcDir, 'main_test.go'), pupa(mainGoTest, { serviceName }));
|
|
63
|
+
// go mod init
|
|
64
|
+
await writeFile(resolve(tempDir, 'go.mod'), pupa(goMod, { modulePath: goModulePath }));
|
|
65
|
+
await mkdir(resolve(process.cwd(), options.directory), { recursive: true });
|
|
66
|
+
await rename(tempDir, pluginDir);
|
|
67
|
+
const endTime = performance.now();
|
|
68
|
+
const elapsedTimeMs = endTime - startTime;
|
|
69
|
+
const formattedTime = elapsedTimeMs > 1000 ? `${(elapsedTimeMs / 1000).toFixed(2)}s` : `${Math.round(elapsedTimeMs)}ms`;
|
|
70
|
+
renderResultTree(spinner, 'Plugin scaffolded!', true, name, {
|
|
71
|
+
language: options.language,
|
|
72
|
+
time: formattedTime,
|
|
73
|
+
location: pluginDir,
|
|
74
|
+
});
|
|
75
|
+
console.log('');
|
|
76
|
+
console.log(` Checkout the ${pc.bold(pc.italic('README.md'))} file for instructions on how to build and run your plugin.`);
|
|
77
|
+
console.log(` Go to https://cosmo-docs.wundergraph.com/router/plugins to learn more about it.`);
|
|
78
|
+
console.log('');
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
// Clean up the temp directory in case of error
|
|
82
|
+
try {
|
|
83
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Ignore cleanup errors
|
|
87
|
+
}
|
|
88
|
+
renderResultTree(spinner, 'Plugin scaffolding', false, name, {
|
|
89
|
+
error: error.message,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
spinner.stop();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
return command;
|
|
97
|
+
};
|
|
98
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../../../../../src/commands/router/plugin/commands/init.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { resolve } from 'pathe';
|
|
5
|
+
import Spinner from 'ora';
|
|
6
|
+
import { checkAndInstallTools, runGoTests } from '../toolchain.js';
|
|
7
|
+
import { renderResultTree } from '../helper.js';
|
|
8
|
+
export default (opts) => {
|
|
9
|
+
const command = new Command('test');
|
|
10
|
+
command.description('Run tests for a gRPC router plugin');
|
|
11
|
+
command.argument('[directory]', 'Directory of the plugin', '.');
|
|
12
|
+
command.option('--skip-tools-installation', 'Skip tool installation', false);
|
|
13
|
+
command.option('--force-tools-installation', 'Force tools installation regardless of version check or confirmation', false);
|
|
14
|
+
command.action(async (directory, options) => {
|
|
15
|
+
const startTime = performance.now();
|
|
16
|
+
const pluginDir = resolve(directory);
|
|
17
|
+
const spinner = Spinner({ text: 'Running tests...' });
|
|
18
|
+
const pluginName = path.basename(pluginDir);
|
|
19
|
+
try {
|
|
20
|
+
spinner.start();
|
|
21
|
+
// Check and install tools if needed
|
|
22
|
+
if (!options.skipToolsInstallation) {
|
|
23
|
+
await checkAndInstallTools(options.forceToolsInstallation);
|
|
24
|
+
}
|
|
25
|
+
const srcDir = resolve(pluginDir, 'src');
|
|
26
|
+
spinner.text = 'Running tests...';
|
|
27
|
+
try {
|
|
28
|
+
const { failed } = await runGoTests(srcDir, spinner, false);
|
|
29
|
+
// Calculate elapsed time
|
|
30
|
+
const endTime = performance.now();
|
|
31
|
+
const elapsedTimeMs = endTime - startTime;
|
|
32
|
+
const formattedTime = elapsedTimeMs > 1000 ? `${(elapsedTimeMs / 1000).toFixed(2)}s` : `${Math.round(elapsedTimeMs)}ms`;
|
|
33
|
+
// Common details for both success and failure
|
|
34
|
+
const details = {
|
|
35
|
+
source: srcDir,
|
|
36
|
+
env: `${os.platform()} ${os.arch()}`,
|
|
37
|
+
time: formattedTime,
|
|
38
|
+
};
|
|
39
|
+
const title = failed ? 'Tests failed!' : 'Tests completed successfully!';
|
|
40
|
+
renderResultTree(spinner, title, !failed, pluginName, details);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
renderResultTree(spinner, 'Tests failed!', false, pluginName, {
|
|
44
|
+
source: srcDir,
|
|
45
|
+
env: `${os.platform()} ${os.arch()}`,
|
|
46
|
+
error: error.message,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
renderResultTree(spinner, 'Failed to run tests!', false, pluginName, {
|
|
52
|
+
env: `${os.platform()} ${os.arch()}`,
|
|
53
|
+
error: error.message,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
spinner.stop();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
return command;
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test.js","sourceRoot":"","sources":["../../../../../../src/commands/router/plugin/commands/test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import Spinner from 'ora';
|
|
2
|
+
/**
|
|
3
|
+
* Renders a tree-formatted result display
|
|
4
|
+
* @param spinner The spinner instance
|
|
5
|
+
* @param title The title to display
|
|
6
|
+
* @param success Whether the operation was successful
|
|
7
|
+
* @param name The name of the item (e.g. plugin name)
|
|
8
|
+
* @param details Key-value pairs of details to display
|
|
9
|
+
*/
|
|
10
|
+
export declare function renderResultTree(spinner: ReturnType<typeof Spinner>, title: string, success: boolean, name: string, details: Record<string, string>): void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
/**
|
|
3
|
+
* Renders a tree-formatted result display
|
|
4
|
+
* @param spinner The spinner instance
|
|
5
|
+
* @param title The title to display
|
|
6
|
+
* @param success Whether the operation was successful
|
|
7
|
+
* @param name The name of the item (e.g. plugin name)
|
|
8
|
+
* @param details Key-value pairs of details to display
|
|
9
|
+
*/
|
|
10
|
+
export function renderResultTree(spinner, title, success, name, details) {
|
|
11
|
+
const state = success ? pc.green('success') : pc.red('failed');
|
|
12
|
+
const symbol = success ? pc.green('[●]') : pc.red('[●]');
|
|
13
|
+
spinner.stopAndPersist({
|
|
14
|
+
symbol,
|
|
15
|
+
text: pc.bold(title),
|
|
16
|
+
});
|
|
17
|
+
// Build the tree with consistent formatting
|
|
18
|
+
let output = ` ${pc.dim('│')}`;
|
|
19
|
+
// Add the name and state first (these are always present)
|
|
20
|
+
output += `\n ${pc.dim('├────────── name')}: ${name}`;
|
|
21
|
+
output += `\n ${pc.dim('├───────── state')}: ${state}`;
|
|
22
|
+
// Dynamically generate key formatters based on actual keys
|
|
23
|
+
const keys = Object.keys(details);
|
|
24
|
+
const keyFormatters = {};
|
|
25
|
+
// Generate dynamic formatters for each key
|
|
26
|
+
for (const key of [...keys, 'name', 'state']) {
|
|
27
|
+
// Calculate the number of dashes needed to align all values
|
|
28
|
+
const dashCount = 14 - key.length;
|
|
29
|
+
keyFormatters[key] = '─'.repeat(dashCount) + ' ' + key;
|
|
30
|
+
}
|
|
31
|
+
// Apply fixed formatters for the standard keys
|
|
32
|
+
keyFormatters.name = '────────── name';
|
|
33
|
+
keyFormatters.state = '───────── state';
|
|
34
|
+
// Add all the other details except the last one
|
|
35
|
+
for (const key of keys.slice(0, -1)) {
|
|
36
|
+
const formattedKey = keyFormatters[key];
|
|
37
|
+
output += `\n ${pc.dim('├' + formattedKey)}: ${details[key]}`;
|
|
38
|
+
}
|
|
39
|
+
// Add the last detail with the corner character
|
|
40
|
+
if (keys.length > 0) {
|
|
41
|
+
const lastKey = keys.at(-1);
|
|
42
|
+
const formattedKey = keyFormatters[lastKey];
|
|
43
|
+
output += `\n ${pc.dim('└' + formattedKey)}: ${details[lastKey]}`;
|
|
44
|
+
}
|
|
45
|
+
console.log(output);
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=helper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helper.js","sourceRoot":"","sources":["../../../../../src/commands/router/plugin/helper.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAC9B,OAAmC,EACnC,KAAa,EACb,OAAgB,EAChB,IAAY,EACZ,OAA+B;IAE/B,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC/D,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAEzD,OAAO,CAAC,cAAc,CAAC;QACrB,MAAM;QACN,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC;KACrB,CAAC,CAAC;IAEH,4CAA4C;IAC5C,IAAI,MAAM,GAAG,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;IAE/B,0DAA0D;IAC1D,MAAM,IAAI,MAAM,EAAE,CAAC,GAAG,CAAC,kBAAkB,CAAC,KAAK,IAAI,EAAE,CAAC;IACtD,MAAM,IAAI,MAAM,EAAE,CAAC,GAAG,CAAC,kBAAkB,CAAC,KAAK,KAAK,EAAE,CAAC;IAEvD,2DAA2D;IAC3D,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAClC,MAAM,aAAa,GAA2B,EAAE,CAAC;IAEjD,2CAA2C;IAC3C,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;QAC7C,4DAA4D;QAC5D,MAAM,SAAS,GAAG,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC;QAClC,aAAa,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,GAAG,GAAG,GAAG,CAAC;IACzD,CAAC;IAED,+CAA+C;IAC/C,aAAa,CAAC,IAAI,GAAG,iBAAiB,CAAC;IACvC,aAAa,CAAC,KAAK,GAAG,iBAAiB,CAAC;IAExC,gDAAgD;IAChD,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpC,MAAM,YAAY,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;QACxC,MAAM,IAAI,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,GAAG,YAAY,CAAC,KAAK,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;IAChE,CAAC;IAED,gDAAgD;IAChD,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,MAAM,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5B,MAAM,YAAY,GAAG,aAAa,CAAC,OAAiB,CAAC,CAAC;QACtD,MAAM,IAAI,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,GAAG,YAAY,CAAC,KAAK,OAAO,CAAC,OAAiB,CAAC,EAAE,CAAC;IAC9E,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import InitPluginCommand from './commands/init.js';
|
|
3
|
+
import BuildPluginCommand from './commands/build.js';
|
|
4
|
+
import TestPluginCommand from './commands/test.js';
|
|
5
|
+
export default (opts) => {
|
|
6
|
+
const command = new Command('plugin');
|
|
7
|
+
command.description('Provides commands for creating and maintaining router plugins');
|
|
8
|
+
command.addCommand(InitPluginCommand(opts));
|
|
9
|
+
command.addCommand(BuildPluginCommand(opts));
|
|
10
|
+
command.addCommand(TestPluginCommand(opts));
|
|
11
|
+
return command;
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/commands/router/plugin/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,iBAAiB,MAAM,oBAAoB,CAAC;AACnD,OAAO,kBAAkB,MAAM,qBAAqB,CAAC;AACrD,OAAO,iBAAiB,MAAM,oBAAoB,CAAC;AAEnD,eAAe,CAAC,IAAwB,EAAE,EAAE;IAC1C,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;IACtC,OAAO,CAAC,WAAW,CAAC,+DAA+D,CAAC,CAAC;IACrF,OAAO,CAAC,UAAU,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5C,OAAO,CAAC,UAAU,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC;IAC7C,OAAO,CAAC,UAAU,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC;IAE5C,OAAO,OAAO,CAAC;AACjB,CAAC,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const goMod = "\nmodule {modulePath}\n\ngo 1.24.1\n\nrequire (\n github.com/stretchr/testify v1.10.0\n github.com/wundergraph/cosmo/router-plugin v0.0.0-20250519132817-0768029ce9e5\n google.golang.org/grpc v1.68.1\n google.golang.org/protobuf v1.36.5\n)\n";
|
|
2
|
+
export declare const mainGo = "package main\n\nimport (\n \"context\"\n \"log\"\n \"strconv\"\n\n service \"github.com/wundergraph/cosmo/plugin/generated\"\n\n routerplugin \"github.com/wundergraph/cosmo/router-plugin\"\n \"google.golang.org/grpc\"\n)\n\nfunc main() {\n pl, err := routerplugin.NewRouterPlugin(func(s *grpc.Server) {\n s.RegisterService(&service.{serviceName}_ServiceDesc, &{serviceName}{\n nextID: 1,\n })\n })\n\n if err != nil {\n log.Fatalf(\"failed to create router plugin: %v\", err)\n }\n\n pl.Serve()\n}\n\ntype {serviceName} struct {\n service.Unimplemented{serviceName}Server\n nextID int\n}\n\nfunc (s *{serviceName}) QueryHello(ctx context.Context, req *service.QueryHelloRequest) (*service.QueryHelloResponse, error) {\n response := &service.QueryHelloResponse{\n Hello: &service.World{\n Id: strconv.Itoa(s.nextID),\n Name: req.Name,\n },\n }\n s.nextID++\n return response, nil\n}\n";
|
|
3
|
+
export declare const mainGoTest = "package main\n\nimport (\n \"context\"\n \"net\"\n \"testing\"\n\n \"github.com/stretchr/testify/assert\"\n \"github.com/stretchr/testify/require\"\n service \"github.com/wundergraph/cosmo/plugin/generated\"\n \"google.golang.org/grpc\"\n \"google.golang.org/grpc/credentials/insecure\"\n \"google.golang.org/grpc/test/bufconn\"\n)\n\nconst bufSize = 1024 * 1024\n\n// testService is a wrapper that holds the gRPC test components\ntype testService struct {\n grpcConn *grpc.ClientConn\n client service.{serviceName}Client\n cleanup func()\n}\n\n// setupTestService creates a local gRPC server for testing\nfunc setupTestService(t *testing.T) *testService {\n // Create a buffer for gRPC connections\n lis := bufconn.Listen(bufSize)\n\n // Create a new gRPC server\n grpcServer := grpc.NewServer()\n\n // Register our service\n service.Register{serviceName}Server(grpcServer, &{serviceName}{\n nextID: 1,\n })\n\n // Start the server\n go func() {\n if err := grpcServer.Serve(lis); err != nil {\n t.Fatalf(\"failed to serve: %v\", err)\n }\n }()\n\n // Create a client connection\n dialer := func(context.Context, string) (net.Conn, error) {\n return lis.Dial()\n }\n conn, err := grpc.Dial(\n \"passthrough:///bufnet\",\n grpc.WithContextDialer(dialer),\n grpc.WithTransportCredentials(insecure.NewCredentials()),\n )\n require.NoError(t, err)\n\n // Create the service client\n client := service.New{serviceName}Client(conn)\n\n // Return cleanup function\n cleanup := func() {\n conn.Close()\n grpcServer.Stop()\n }\n\n return &testService{\n grpcConn: conn,\n client: client,\n cleanup: cleanup,\n }\n}\n\nfunc TestQueryHello(t *testing.T) {\n // Set up basic service\n svc := setupTestService(t)\n defer svc.cleanup()\n\n tests := []struct {\n name string\n userName string\n wantId string\n wantName string\n wantErr bool\n }{\n {\n name: \"valid hello\",\n userName: \"Alice\",\n wantId: \"1\",\n wantName: \"Alice\",\n wantErr: false,\n },\n {\n name: \"empty name\",\n userName: \"\",\n wantId: \"2\",\n wantName: \"\", // Empty name should be preserved\n wantErr: false,\n },\n {\n name: \"special characters\",\n userName: \"John & Jane\",\n wantId: \"3\",\n wantName: \"John & Jane\",\n wantErr: false,\n },\n }\n\n for _, tt := range tests {\n t.Run(tt.name, func(t *testing.T) {\n req := &service.QueryHelloRequest{\n Name: tt.userName,\n }\n\n resp, err := svc.client.QueryHello(context.Background(), req)\n if tt.wantErr {\n assert.Error(t, err)\n return\n }\n\n assert.NoError(t, err)\n assert.NotNil(t, resp.Hello)\n assert.Equal(t, tt.wantId, resp.Hello.Id)\n assert.Equal(t, tt.wantName, resp.Hello.Name)\n })\n }\n}\n\nfunc TestSequentialIDs(t *testing.T) {\n // Set up basic service\n svc := setupTestService(t)\n defer svc.cleanup()\n\n // The first request should get ID \"1\"\n firstReq := &service.QueryHelloRequest{Name: \"First\"}\n firstResp, err := svc.client.QueryHello(context.Background(), firstReq)\n require.NoError(t, err)\n assert.Equal(t, \"1\", firstResp.Hello.Id)\n\n // The second request should get ID \"2\"\n secondReq := &service.QueryHelloRequest{Name: \"Second\"}\n secondResp, err := svc.client.QueryHello(context.Background(), secondReq)\n require.NoError(t, err)\n assert.Equal(t, \"2\", secondResp.Hello.Id)\n\n // The third request should get ID \"3\"\n thirdReq := &service.QueryHelloRequest{Name: \"Third\"}\n thirdResp, err := svc.client.QueryHello(context.Background(), thirdReq)\n require.NoError(t, err)\n assert.Equal(t, \"3\", thirdResp.Hello.Id)\n}\n";
|
|
4
|
+
export declare const readme = "# {name} Plugin - Cosmo gRPC Subgraph Example\n\nThis repository contains a simple Cosmo gRPC subgraph plugin that showcases how to design APIs with GraphQL but implement them using gRPC methods instead of traditional resolvers.\n\n## What is this demo about?\n\nThis demo illustrates a key pattern in Cosmo subgraph development:\n- **Design with GraphQL**: Define your API using GraphQL schema\n- **Implement with gRPC**: Instead of writing GraphQL resolvers, implement gRPC service methods\n- **Bridge the gap**: The Cosmo router connects GraphQL operations to your gRPC implementations\n- **Test-Driven Development**: Test your gRPC service implementation with gRPC client and server without external dependencies\n\nThe plugin demonstrates:\n- How GraphQL types and operations map to gRPC service methods\n- Simple \"Hello World\" implementation\n- Proper structure for a Cosmo gRPC subgraph plugin\n- How to test your gRPC service implementation with gRPC client and server without external dependencies\n\n## Plugin Structure\n\n- `src/` - Contains the plugin source code\n - `main.go` - The gRPC service implementation with methods that replace GraphQL resolvers\n - `main_test.go` - The gRPC service implementation with methods that replace GraphQL resolvers\n - `schema.graphql` - The GraphQL schema defining the API contract\n- `generated/` - Contains generated code from the plugin schema\n- `bin/` - Contains compiled binaries of the plugin\n\n## GraphQL to gRPC Mapping\n\nThe plugin shows how GraphQL operations map to gRPC methods:\n\n| GraphQL Operation | gRPC Method |\n|-------------------|-------------|\n| `query { hello }` | `QueryHello()` |\n\n## GraphQL Schema\n\n```graphql\ntype World {\n id: ID!\n name: String!\n}\n\ntype Query {\n hello(name: String!): World!\n}\n```\n\n## Getting Started\n\n1. **Build the plugin**\n\n ```\n wgc router plugin build <plugin-directory>\n ```\n\n2. **Compose your supergraph with your gRPC subgraph**\n\n config.yaml\n ```yaml\n subgraphs:\n - name: <plugin-name>\n plugin:\n version: 0.0.1\n directory: <plugin-directory>/<plugin-name>\n ```\n\n3. **Build the federated graph**\n\n ```bash\n wgc router compose config.yaml\n ```\n\n4. **Test the plugin**\n\n ```bash\n wgc router plugin test <plugin-directory>/<plugin-name>\n ```\n or\n ```bash\n go test src -v\n ```\n if you have the Go toolchain already installed.\n\n5. **Start the router**\n\n ```yaml\n execution_config:\n file:\n path: ./config.yaml\n plugins:\n - <plugin-directory>\n ```\n\n6. **Query the hello endpoint**\n\n Once running, you can perform GraphQL operations like:\n \n ```graphql\n # Hello query\n query {\n hello(name: \"World\") {\n id\n name\n }\n }\n ```\n\n## Further Steps\n\n- Change the plugin code in `src/main.go` and rebuild the plugin\n- Change the GraphQL schema in `src/schema.graphql` and rebuild the plugin\n\n## Learn More\n\nFor more information about Cosmo and building subgraph plugins, visit the [Cosmo documentation](https://cosmo-docs.wundergraph.com).";
|
|
5
|
+
export declare const schema = "type World {\n \"\"\"\n The ID of the world\n \"\"\"\n id: ID!\n \"\"\"\n The name of the world\n \"\"\"\n name: String!\n}\n\ntype Query {\n \"\"\"\n The hello query\n \"\"\"\n hello(name: String!): World!\n}\n";
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// We store the templates in code to avoid dealing with file system issues when
|
|
2
|
+
// building for bun and transpiling TypeScript.
|
|
3
|
+
export const goMod = `
|
|
4
|
+
module {modulePath}
|
|
5
|
+
|
|
6
|
+
go 1.24.1
|
|
7
|
+
|
|
8
|
+
require (
|
|
9
|
+
github.com/stretchr/testify v1.10.0
|
|
10
|
+
github.com/wundergraph/cosmo/router-plugin v0.0.0-20250519132817-0768029ce9e5
|
|
11
|
+
google.golang.org/grpc v1.68.1
|
|
12
|
+
google.golang.org/protobuf v1.36.5
|
|
13
|
+
)
|
|
14
|
+
`;
|
|
15
|
+
export const mainGo = `package main
|
|
16
|
+
|
|
17
|
+
import (
|
|
18
|
+
"context"
|
|
19
|
+
"log"
|
|
20
|
+
"strconv"
|
|
21
|
+
|
|
22
|
+
service "github.com/wundergraph/cosmo/plugin/generated"
|
|
23
|
+
|
|
24
|
+
routerplugin "github.com/wundergraph/cosmo/router-plugin"
|
|
25
|
+
"google.golang.org/grpc"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
func main() {
|
|
29
|
+
pl, err := routerplugin.NewRouterPlugin(func(s *grpc.Server) {
|
|
30
|
+
s.RegisterService(&service.{serviceName}_ServiceDesc, &{serviceName}{
|
|
31
|
+
nextID: 1,
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
if err != nil {
|
|
36
|
+
log.Fatalf("failed to create router plugin: %v", err)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
pl.Serve()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type {serviceName} struct {
|
|
43
|
+
service.Unimplemented{serviceName}Server
|
|
44
|
+
nextID int
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func (s *{serviceName}) QueryHello(ctx context.Context, req *service.QueryHelloRequest) (*service.QueryHelloResponse, error) {
|
|
48
|
+
response := &service.QueryHelloResponse{
|
|
49
|
+
Hello: &service.World{
|
|
50
|
+
Id: strconv.Itoa(s.nextID),
|
|
51
|
+
Name: req.Name,
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
s.nextID++
|
|
55
|
+
return response, nil
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
export const mainGoTest = `package main
|
|
59
|
+
|
|
60
|
+
import (
|
|
61
|
+
"context"
|
|
62
|
+
"net"
|
|
63
|
+
"testing"
|
|
64
|
+
|
|
65
|
+
"github.com/stretchr/testify/assert"
|
|
66
|
+
"github.com/stretchr/testify/require"
|
|
67
|
+
service "github.com/wundergraph/cosmo/plugin/generated"
|
|
68
|
+
"google.golang.org/grpc"
|
|
69
|
+
"google.golang.org/grpc/credentials/insecure"
|
|
70
|
+
"google.golang.org/grpc/test/bufconn"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
const bufSize = 1024 * 1024
|
|
74
|
+
|
|
75
|
+
// testService is a wrapper that holds the gRPC test components
|
|
76
|
+
type testService struct {
|
|
77
|
+
grpcConn *grpc.ClientConn
|
|
78
|
+
client service.{serviceName}Client
|
|
79
|
+
cleanup func()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// setupTestService creates a local gRPC server for testing
|
|
83
|
+
func setupTestService(t *testing.T) *testService {
|
|
84
|
+
// Create a buffer for gRPC connections
|
|
85
|
+
lis := bufconn.Listen(bufSize)
|
|
86
|
+
|
|
87
|
+
// Create a new gRPC server
|
|
88
|
+
grpcServer := grpc.NewServer()
|
|
89
|
+
|
|
90
|
+
// Register our service
|
|
91
|
+
service.Register{serviceName}Server(grpcServer, &{serviceName}{
|
|
92
|
+
nextID: 1,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Start the server
|
|
96
|
+
go func() {
|
|
97
|
+
if err := grpcServer.Serve(lis); err != nil {
|
|
98
|
+
t.Fatalf("failed to serve: %v", err)
|
|
99
|
+
}
|
|
100
|
+
}()
|
|
101
|
+
|
|
102
|
+
// Create a client connection
|
|
103
|
+
dialer := func(context.Context, string) (net.Conn, error) {
|
|
104
|
+
return lis.Dial()
|
|
105
|
+
}
|
|
106
|
+
conn, err := grpc.Dial(
|
|
107
|
+
"passthrough:///bufnet",
|
|
108
|
+
grpc.WithContextDialer(dialer),
|
|
109
|
+
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
110
|
+
)
|
|
111
|
+
require.NoError(t, err)
|
|
112
|
+
|
|
113
|
+
// Create the service client
|
|
114
|
+
client := service.New{serviceName}Client(conn)
|
|
115
|
+
|
|
116
|
+
// Return cleanup function
|
|
117
|
+
cleanup := func() {
|
|
118
|
+
conn.Close()
|
|
119
|
+
grpcServer.Stop()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return &testService{
|
|
123
|
+
grpcConn: conn,
|
|
124
|
+
client: client,
|
|
125
|
+
cleanup: cleanup,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func TestQueryHello(t *testing.T) {
|
|
130
|
+
// Set up basic service
|
|
131
|
+
svc := setupTestService(t)
|
|
132
|
+
defer svc.cleanup()
|
|
133
|
+
|
|
134
|
+
tests := []struct {
|
|
135
|
+
name string
|
|
136
|
+
userName string
|
|
137
|
+
wantId string
|
|
138
|
+
wantName string
|
|
139
|
+
wantErr bool
|
|
140
|
+
}{
|
|
141
|
+
{
|
|
142
|
+
name: "valid hello",
|
|
143
|
+
userName: "Alice",
|
|
144
|
+
wantId: "1",
|
|
145
|
+
wantName: "Alice",
|
|
146
|
+
wantErr: false,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "empty name",
|
|
150
|
+
userName: "",
|
|
151
|
+
wantId: "2",
|
|
152
|
+
wantName: "", // Empty name should be preserved
|
|
153
|
+
wantErr: false,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: "special characters",
|
|
157
|
+
userName: "John & Jane",
|
|
158
|
+
wantId: "3",
|
|
159
|
+
wantName: "John & Jane",
|
|
160
|
+
wantErr: false,
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for _, tt := range tests {
|
|
165
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
166
|
+
req := &service.QueryHelloRequest{
|
|
167
|
+
Name: tt.userName,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
resp, err := svc.client.QueryHello(context.Background(), req)
|
|
171
|
+
if tt.wantErr {
|
|
172
|
+
assert.Error(t, err)
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
assert.NoError(t, err)
|
|
177
|
+
assert.NotNil(t, resp.Hello)
|
|
178
|
+
assert.Equal(t, tt.wantId, resp.Hello.Id)
|
|
179
|
+
assert.Equal(t, tt.wantName, resp.Hello.Name)
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
func TestSequentialIDs(t *testing.T) {
|
|
185
|
+
// Set up basic service
|
|
186
|
+
svc := setupTestService(t)
|
|
187
|
+
defer svc.cleanup()
|
|
188
|
+
|
|
189
|
+
// The first request should get ID "1"
|
|
190
|
+
firstReq := &service.QueryHelloRequest{Name: "First"}
|
|
191
|
+
firstResp, err := svc.client.QueryHello(context.Background(), firstReq)
|
|
192
|
+
require.NoError(t, err)
|
|
193
|
+
assert.Equal(t, "1", firstResp.Hello.Id)
|
|
194
|
+
|
|
195
|
+
// The second request should get ID "2"
|
|
196
|
+
secondReq := &service.QueryHelloRequest{Name: "Second"}
|
|
197
|
+
secondResp, err := svc.client.QueryHello(context.Background(), secondReq)
|
|
198
|
+
require.NoError(t, err)
|
|
199
|
+
assert.Equal(t, "2", secondResp.Hello.Id)
|
|
200
|
+
|
|
201
|
+
// The third request should get ID "3"
|
|
202
|
+
thirdReq := &service.QueryHelloRequest{Name: "Third"}
|
|
203
|
+
thirdResp, err := svc.client.QueryHello(context.Background(), thirdReq)
|
|
204
|
+
require.NoError(t, err)
|
|
205
|
+
assert.Equal(t, "3", thirdResp.Hello.Id)
|
|
206
|
+
}
|
|
207
|
+
`;
|
|
208
|
+
export const readme = `# {name} Plugin - Cosmo gRPC Subgraph Example
|
|
209
|
+
|
|
210
|
+
This repository contains a simple Cosmo gRPC subgraph plugin that showcases how to design APIs with GraphQL but implement them using gRPC methods instead of traditional resolvers.
|
|
211
|
+
|
|
212
|
+
## What is this demo about?
|
|
213
|
+
|
|
214
|
+
This demo illustrates a key pattern in Cosmo subgraph development:
|
|
215
|
+
- **Design with GraphQL**: Define your API using GraphQL schema
|
|
216
|
+
- **Implement with gRPC**: Instead of writing GraphQL resolvers, implement gRPC service methods
|
|
217
|
+
- **Bridge the gap**: The Cosmo router connects GraphQL operations to your gRPC implementations
|
|
218
|
+
- **Test-Driven Development**: Test your gRPC service implementation with gRPC client and server without external dependencies
|
|
219
|
+
|
|
220
|
+
The plugin demonstrates:
|
|
221
|
+
- How GraphQL types and operations map to gRPC service methods
|
|
222
|
+
- Simple "Hello World" implementation
|
|
223
|
+
- Proper structure for a Cosmo gRPC subgraph plugin
|
|
224
|
+
- How to test your gRPC service implementation with gRPC client and server without external dependencies
|
|
225
|
+
|
|
226
|
+
## Plugin Structure
|
|
227
|
+
|
|
228
|
+
- \`src/\` - Contains the plugin source code
|
|
229
|
+
- \`main.go\` - The gRPC service implementation with methods that replace GraphQL resolvers
|
|
230
|
+
- \`main_test.go\` - The gRPC service implementation with methods that replace GraphQL resolvers
|
|
231
|
+
- \`schema.graphql\` - The GraphQL schema defining the API contract
|
|
232
|
+
- \`generated/\` - Contains generated code from the plugin schema
|
|
233
|
+
- \`bin/\` - Contains compiled binaries of the plugin
|
|
234
|
+
|
|
235
|
+
## GraphQL to gRPC Mapping
|
|
236
|
+
|
|
237
|
+
The plugin shows how GraphQL operations map to gRPC methods:
|
|
238
|
+
|
|
239
|
+
| GraphQL Operation | gRPC Method |
|
|
240
|
+
|-------------------|-------------|
|
|
241
|
+
| \`query { hello }\` | \`QueryHello()\` |
|
|
242
|
+
|
|
243
|
+
## GraphQL Schema
|
|
244
|
+
|
|
245
|
+
\`\`\`graphql
|
|
246
|
+
type World {
|
|
247
|
+
id: ID!
|
|
248
|
+
name: String!
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
type Query {
|
|
252
|
+
hello(name: String!): World!
|
|
253
|
+
}
|
|
254
|
+
\`\`\`
|
|
255
|
+
|
|
256
|
+
## Getting Started
|
|
257
|
+
|
|
258
|
+
1. **Build the plugin**
|
|
259
|
+
|
|
260
|
+
\`\`\`
|
|
261
|
+
wgc router plugin build <plugin-directory>
|
|
262
|
+
\`\`\`
|
|
263
|
+
|
|
264
|
+
2. **Compose your supergraph with your gRPC subgraph**
|
|
265
|
+
|
|
266
|
+
config.yaml
|
|
267
|
+
\`\`\`yaml
|
|
268
|
+
subgraphs:
|
|
269
|
+
- name: <plugin-name>
|
|
270
|
+
plugin:
|
|
271
|
+
version: 0.0.1
|
|
272
|
+
directory: <plugin-directory>/<plugin-name>
|
|
273
|
+
\`\`\`
|
|
274
|
+
|
|
275
|
+
3. **Build the federated graph**
|
|
276
|
+
|
|
277
|
+
\`\`\`bash
|
|
278
|
+
wgc router compose config.yaml
|
|
279
|
+
\`\`\`
|
|
280
|
+
|
|
281
|
+
4. **Test the plugin**
|
|
282
|
+
|
|
283
|
+
\`\`\`bash
|
|
284
|
+
wgc router plugin test <plugin-directory>/<plugin-name>
|
|
285
|
+
\`\`\`
|
|
286
|
+
or
|
|
287
|
+
\`\`\`bash
|
|
288
|
+
go test src -v
|
|
289
|
+
\`\`\`
|
|
290
|
+
if you have the Go toolchain already installed.
|
|
291
|
+
|
|
292
|
+
5. **Start the router**
|
|
293
|
+
|
|
294
|
+
\`\`\`yaml
|
|
295
|
+
execution_config:
|
|
296
|
+
file:
|
|
297
|
+
path: ./config.yaml
|
|
298
|
+
plugins:
|
|
299
|
+
- <plugin-directory>
|
|
300
|
+
\`\`\`
|
|
301
|
+
|
|
302
|
+
6. **Query the hello endpoint**
|
|
303
|
+
|
|
304
|
+
Once running, you can perform GraphQL operations like:
|
|
305
|
+
|
|
306
|
+
\`\`\`graphql
|
|
307
|
+
# Hello query
|
|
308
|
+
query {
|
|
309
|
+
hello(name: "World") {
|
|
310
|
+
id
|
|
311
|
+
name
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
\`\`\`
|
|
315
|
+
|
|
316
|
+
## Further Steps
|
|
317
|
+
|
|
318
|
+
- Change the plugin code in \`src/main.go\` and rebuild the plugin
|
|
319
|
+
- Change the GraphQL schema in \`src/schema.graphql\` and rebuild the plugin
|
|
320
|
+
|
|
321
|
+
## Learn More
|
|
322
|
+
|
|
323
|
+
For more information about Cosmo and building subgraph plugins, visit the [Cosmo documentation](https://cosmo-docs.wundergraph.com).`;
|
|
324
|
+
export const schema = `type World {
|
|
325
|
+
"""
|
|
326
|
+
The ID of the world
|
|
327
|
+
"""
|
|
328
|
+
id: ID!
|
|
329
|
+
"""
|
|
330
|
+
The name of the world
|
|
331
|
+
"""
|
|
332
|
+
name: String!
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
type Query {
|
|
336
|
+
"""
|
|
337
|
+
The hello query
|
|
338
|
+
"""
|
|
339
|
+
hello(name: String!): World!
|
|
340
|
+
}
|
|
341
|
+
`;
|
|
342
|
+
//# sourceMappingURL=go-plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"go-plugin.js","sourceRoot":"","sources":["../../../../../../src/commands/router/plugin/templates/go-plugin.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,+CAA+C;AAE/C,MAAM,CAAC,MAAM,KAAK,GAAG;;;;;;;;;;;CAWpB,CAAC;AAEF,MAAM,CAAC,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0CrB,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqJzB,CAAC;AAEF,MAAM,CAAC,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qIAmH+G,CAAC;AAEtI,MAAM,CAAC,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;CAiBrB,CAAC"}
|