@contismo/sdk 0.1.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/.github/workflows/ci.yml +41 -0
- package/.github/workflows/release.yml +38 -0
- package/CODE_OF_CONDUCT.md +28 -0
- package/CONTRIBUTING.md +34 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/clients/typescript/docs/codegen.md +107 -0
- package/clients/typescript/eslint.config.js +18 -0
- package/clients/typescript/package.json +87 -0
- package/clients/typescript/src/api-keys.ts +13 -0
- package/clients/typescript/src/cli/codegen-typescript-config.ts +36 -0
- package/clients/typescript/src/cli/generate-models.ts +94 -0
- package/clients/typescript/src/cli/generate.ts +44 -0
- package/clients/typescript/src/cli/import-from-sdk.ts +12 -0
- package/clients/typescript/src/cli/load-config.ts +1 -0
- package/clients/typescript/src/cli/main.ts +39 -0
- package/clients/typescript/src/cli/prettify-generated-types.ts +129 -0
- package/clients/typescript/src/cli/run-codegen.ts +58 -0
- package/clients/typescript/src/client.ts +152 -0
- package/clients/typescript/src/config.ts +41 -0
- package/clients/typescript/src/errors.ts +82 -0
- package/clients/typescript/src/execute.ts +104 -0
- package/clients/typescript/src/fetch-query.ts +123 -0
- package/clients/typescript/src/index.ts +41 -0
- package/clients/typescript/src/introspection.ts +165 -0
- package/clients/typescript/src/load-client.ts +47 -0
- package/clients/typescript/src/load-config.ts +64 -0
- package/clients/typescript/src/load-models.ts +35 -0
- package/clients/typescript/src/models.ts +16 -0
- package/clients/typescript/src/request-payload.ts +16 -0
- package/clients/typescript/src/resolve-codegen-paths.ts +31 -0
- package/clients/typescript/src/select.ts +22 -0
- package/clients/typescript/src/types.ts +55 -0
- package/clients/typescript/src/write-introspection.ts +13 -0
- package/clients/typescript/tests/api-keys.test.ts +14 -0
- package/clients/typescript/tests/client.test.ts +221 -0
- package/clients/typescript/tests/load-client.test.ts +126 -0
- package/clients/typescript/tests/load-config.test.ts +32 -0
- package/clients/typescript/tests/prettify-generated-types.test.ts +43 -0
- package/clients/typescript/tests/resolve-codegen-paths.test.ts +57 -0
- package/clients/typescript/tests/select.test.ts +53 -0
- package/clients/typescript/tests/write-introspection.test.ts +30 -0
- package/clients/typescript/tsconfig.json +16 -0
- package/clients/typescript/tsup.config.ts +32 -0
- package/clients/typescript/vitest.config.ts +8 -0
- package/package.json +17 -0
- package/pnpm-workspace.yaml +2 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { ContismoClient } from "../client";
|
|
5
|
+
import { resolveCodegenPaths } from "../resolve-codegen-paths";
|
|
6
|
+
import { writeIntrospectionResult } from "../write-introspection";
|
|
7
|
+
import { loadContismoConfig } from "./load-config";
|
|
8
|
+
import { buildModelRegistry, writeModelsModule } from "./generate-models";
|
|
9
|
+
import { prettifyGeneratedTypes } from "./prettify-generated-types";
|
|
10
|
+
import { runGraphQLCodegen } from "./run-codegen";
|
|
11
|
+
|
|
12
|
+
export type GenerateOptions = {
|
|
13
|
+
cwd?: string;
|
|
14
|
+
config?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function runGenerate(options: GenerateOptions = {}): Promise<void> {
|
|
18
|
+
const cwd = options.cwd ?? process.cwd();
|
|
19
|
+
const config = loadContismoConfig(cwd, options.config);
|
|
20
|
+
|
|
21
|
+
const { schemaPath, outputPath, modelsPath } = resolveCodegenPaths(cwd, config.codegen);
|
|
22
|
+
|
|
23
|
+
console.log("Introspecting schema…");
|
|
24
|
+
const client = new ContismoClient(config.client);
|
|
25
|
+
const introspection = await client.introspect();
|
|
26
|
+
|
|
27
|
+
await mkdir(dirname(schemaPath), { recursive: true });
|
|
28
|
+
await writeIntrospectionResult(introspection, schemaPath);
|
|
29
|
+
console.log(`Wrote ${schemaPath}`);
|
|
30
|
+
|
|
31
|
+
console.log("Generating types…");
|
|
32
|
+
await runGraphQLCodegen(cwd, schemaPath, outputPath, config.codegen);
|
|
33
|
+
|
|
34
|
+
if (config.codegen?.prettify !== false) {
|
|
35
|
+
const raw = await readFile(outputPath, "utf8");
|
|
36
|
+
await writeFile(outputPath, `${prettifyGeneratedTypes(raw)}\n`, "utf8");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(`Wrote ${outputPath}`);
|
|
40
|
+
|
|
41
|
+
const registry = buildModelRegistry(introspection);
|
|
42
|
+
await writeModelsModule(registry, modelsPath);
|
|
43
|
+
console.log(`Wrote ${modelsPath}`);
|
|
44
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
const sdkRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
6
|
+
|
|
7
|
+
/** Resolve and import a dependency bundled with `@contismo/sdk` (works under pnpm). */
|
|
8
|
+
export function importFromSdk<T = unknown>(specifier: string): Promise<T> {
|
|
9
|
+
const require = createRequire(join(sdkRoot, "package.json"));
|
|
10
|
+
const resolved = require.resolve(specifier);
|
|
11
|
+
return import(pathToFileURL(resolved).href) as Promise<T>;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { findConfigFile, loadContismoConfig } from "../load-config";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { runGenerate } from "./generate";
|
|
3
|
+
|
|
4
|
+
const { values, positionals } = parseArgs({
|
|
5
|
+
allowPositionals: true,
|
|
6
|
+
options: {
|
|
7
|
+
config: { type: "string", short: "c" },
|
|
8
|
+
help: { type: "boolean", short: "h" },
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const command = positionals[0];
|
|
13
|
+
|
|
14
|
+
if (values.help || command === "help" || command === undefined) {
|
|
15
|
+
console.log(
|
|
16
|
+
`Contismo CLI
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
contismo generate [--config <path>] Fetch schema and generate TypeScript types
|
|
20
|
+
|
|
21
|
+
Requires contismo.config.ts in your project.
|
|
22
|
+
`,
|
|
23
|
+
);
|
|
24
|
+
process.exit(command === undefined ? 1 : 0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (command === "generate") {
|
|
28
|
+
try {
|
|
29
|
+
await runGenerate(values.config !== undefined ? { config: values.config } : {});
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error(error instanceof Error ? error.message : error);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.error(`Unknown command: ${command}`);
|
|
38
|
+
console.error("Run contismo --help");
|
|
39
|
+
process.exit(1);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/** Maps GraphQL scalar names to TypeScript types used in generated output. */
|
|
2
|
+
export const PRETTIFY_SCALAR_TYPES: Record<string, string> = {
|
|
3
|
+
ID: "string",
|
|
4
|
+
String: "string",
|
|
5
|
+
Boolean: "boolean",
|
|
6
|
+
Int: "number",
|
|
7
|
+
Float: "number",
|
|
8
|
+
DateTime: "string",
|
|
9
|
+
Date: "string",
|
|
10
|
+
JSON: "unknown",
|
|
11
|
+
Time: "string",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const BOILERPLATE_TYPES = [
|
|
15
|
+
"Maybe",
|
|
16
|
+
"InputMaybe",
|
|
17
|
+
"Exact",
|
|
18
|
+
"MakeOptional",
|
|
19
|
+
"MakeMaybe",
|
|
20
|
+
"MakeEmpty",
|
|
21
|
+
"Incremental",
|
|
22
|
+
"Scalars",
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Simplifies graphql-codegen output for Contismo consumers:
|
|
27
|
+
* - `Maybe<Scalars['DateTime']['output']>` → `string | null`
|
|
28
|
+
* - `Maybe<RichText>` → `RichText | null`
|
|
29
|
+
* - removes unused codegen utility types
|
|
30
|
+
*/
|
|
31
|
+
export function prettifyGeneratedTypes(source: string): string {
|
|
32
|
+
let output = source;
|
|
33
|
+
|
|
34
|
+
for (const [scalar, tsType] of Object.entries(PRETTIFY_SCALAR_TYPES)) {
|
|
35
|
+
const quoted = scalar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
36
|
+
output = output.replace(
|
|
37
|
+
new RegExp(`Maybe<Scalars\\['${quoted}'\\]\\['output'\\]>`, "g"),
|
|
38
|
+
`${tsType} | null`,
|
|
39
|
+
);
|
|
40
|
+
output = output.replace(
|
|
41
|
+
new RegExp(`Scalars\\['${quoted}'\\]\\['output'\\]`, "g"),
|
|
42
|
+
tsType,
|
|
43
|
+
);
|
|
44
|
+
output = output.replace(
|
|
45
|
+
new RegExp(`InputMaybe<Scalars\\['${quoted}'\\]\\['input'\\]>`, "g"),
|
|
46
|
+
`${tsType} | null`,
|
|
47
|
+
);
|
|
48
|
+
output = output.replace(
|
|
49
|
+
new RegExp(`Scalars\\['${quoted}'\\]\\['input'\\]`, "g"),
|
|
50
|
+
tsType,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
output = output
|
|
55
|
+
.split("\n")
|
|
56
|
+
.map((line) => {
|
|
57
|
+
if (line.startsWith("export type ")) {
|
|
58
|
+
return line;
|
|
59
|
+
}
|
|
60
|
+
return line
|
|
61
|
+
.replace(/\?: Maybe<([^>]+)>/g, "?: $1 | null")
|
|
62
|
+
.replace(/: Maybe<([^>]+)>/g, ": $1 | null")
|
|
63
|
+
.replace(/InputMaybe<([^>]+)>/g, "$1 | null");
|
|
64
|
+
})
|
|
65
|
+
.join("\n");
|
|
66
|
+
|
|
67
|
+
for (const typeName of BOILERPLATE_TYPES) {
|
|
68
|
+
output = removeExportTypeBlock(output, typeName);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
output = output.replace(
|
|
72
|
+
/\/\*\* All built-in and custom scalars, mapped to their actual values \*\/\n/g,
|
|
73
|
+
"",
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return output.replace(/\n{3,}/g, "\n\n").trimStart();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function removeExportTypeBlock(source: string, typeName: string): string {
|
|
80
|
+
const marker = `export type ${typeName}`;
|
|
81
|
+
const start = source.indexOf(marker);
|
|
82
|
+
if (start === -1) {
|
|
83
|
+
return source;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const assignIndex = source.indexOf("=", start);
|
|
87
|
+
if (assignIndex === -1) {
|
|
88
|
+
return source;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let index = assignIndex + 1;
|
|
92
|
+
while (index < source.length && /\s/.test(source[index] ?? "")) {
|
|
93
|
+
index++;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (source[index] === "{") {
|
|
97
|
+
let depth = 0;
|
|
98
|
+
let end = index;
|
|
99
|
+
for (; end < source.length; end++) {
|
|
100
|
+
const char = source[end];
|
|
101
|
+
if (char === "{") {
|
|
102
|
+
depth++;
|
|
103
|
+
} else if (char === "}") {
|
|
104
|
+
depth--;
|
|
105
|
+
if (depth === 0) {
|
|
106
|
+
end++;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (source[end] === ";") {
|
|
112
|
+
end++;
|
|
113
|
+
}
|
|
114
|
+
if (source[end] === "\n") {
|
|
115
|
+
end++;
|
|
116
|
+
}
|
|
117
|
+
return source.slice(0, start) + source.slice(end);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const end = source.indexOf(";", assignIndex);
|
|
121
|
+
if (end === -1) {
|
|
122
|
+
return source;
|
|
123
|
+
}
|
|
124
|
+
let sliceEnd = end + 1;
|
|
125
|
+
if (source[sliceEnd] === "\n") {
|
|
126
|
+
sliceEnd++;
|
|
127
|
+
}
|
|
128
|
+
return source.slice(0, start) + source.slice(sliceEnd);
|
|
129
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import type { ContismoCodegenConfig } from "../config";
|
|
4
|
+
import { buildTypescriptCodegenConfig } from "./codegen-typescript-config";
|
|
5
|
+
import { importFromSdk } from "./import-from-sdk";
|
|
6
|
+
|
|
7
|
+
type CodegenModule = {
|
|
8
|
+
generate: (
|
|
9
|
+
config: unknown,
|
|
10
|
+
saveToFile?: boolean,
|
|
11
|
+
) => Promise<unknown[]>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export async function runGraphQLCodegen(
|
|
15
|
+
cwd: string,
|
|
16
|
+
schemaPath: string,
|
|
17
|
+
outputPath: string,
|
|
18
|
+
codegen?: ContismoCodegenConfig,
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
const documents = codegen?.documents;
|
|
21
|
+
const hasDocuments = documents !== undefined && documents.length > 0;
|
|
22
|
+
|
|
23
|
+
let generate: CodegenModule["generate"];
|
|
24
|
+
try {
|
|
25
|
+
const cli = await importFromSdk<CodegenModule>("@graphql-codegen/cli");
|
|
26
|
+
generate = cli.generate;
|
|
27
|
+
await importFromSdk("@graphql-codegen/typescript");
|
|
28
|
+
if (hasDocuments) {
|
|
29
|
+
await importFromSdk("@graphql-codegen/typescript-operations");
|
|
30
|
+
await importFromSdk("@graphql-codegen/typed-document-node");
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"Failed to load the code generator bundled with @contismo/sdk. Reinstall @contismo/sdk and try again.",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
39
|
+
|
|
40
|
+
const plugins = hasDocuments
|
|
41
|
+
? ["typescript", "typescript-operations", "typed-document-node"]
|
|
42
|
+
: ["typescript"];
|
|
43
|
+
|
|
44
|
+
await generate(
|
|
45
|
+
{
|
|
46
|
+
cwd,
|
|
47
|
+
schema: schemaPath,
|
|
48
|
+
...(hasDocuments ? { documents } : {}),
|
|
49
|
+
generates: {
|
|
50
|
+
[outputPath]: {
|
|
51
|
+
plugins,
|
|
52
|
+
config: buildTypescriptCodegenConfig(codegen),
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
true,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { isReadOnlyApiKey } from "./api-keys";
|
|
2
|
+
import { ContismoErrorCodes, ContismoGraphQLError } from "./errors";
|
|
3
|
+
import { executeGraphQL } from "./execute";
|
|
4
|
+
import {
|
|
5
|
+
buildFetchListQuery,
|
|
6
|
+
buildFetchOneQuery,
|
|
7
|
+
type FetchListArgs,
|
|
8
|
+
type FetchOneArgs,
|
|
9
|
+
} from "./fetch-query";
|
|
10
|
+
import type { ModelRegistry, ModelRegistryEntry } from "./models";
|
|
11
|
+
import { buildGraphQLPayload } from "./request-payload";
|
|
12
|
+
import { INTROSPECTION_QUERY, type IntrospectionQueryResult } from "./introspection";
|
|
13
|
+
import { resolveContismoClientConfig, type ContismoClientOptions } from "./load-client";
|
|
14
|
+
import type { ContismoClientConfig, RequestOptions } from "./types";
|
|
15
|
+
|
|
16
|
+
export type { ContismoClientOptions };
|
|
17
|
+
|
|
18
|
+
export type FetchOptions = FetchListArgs & RequestOptions;
|
|
19
|
+
export type FetchOneOptions = FetchOneArgs & RequestOptions;
|
|
20
|
+
|
|
21
|
+
export class ContismoClient {
|
|
22
|
+
private readonly config: ContismoClientConfig;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Pass full `apiKey`, `endpoint`, `project`, and `environment` to skip config discovery.
|
|
26
|
+
* With no credentials (or only `cwd` / `config`), loads `contismo.config.*` and the generated model registry.
|
|
27
|
+
*/
|
|
28
|
+
constructor(options: ContismoClientConfig | ContismoClientOptions = {}) {
|
|
29
|
+
this.config = resolveContismoClientConfig(options);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Run a GraphQL query.
|
|
34
|
+
*/
|
|
35
|
+
query<TData>(
|
|
36
|
+
document: string,
|
|
37
|
+
variables?: Record<string, unknown>,
|
|
38
|
+
options?: RequestOptions,
|
|
39
|
+
): Promise<TData> {
|
|
40
|
+
return executeGraphQL<TData>(this.config, buildGraphQLPayload(document, variables), options);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Run a GraphQL mutation. Rejects before the network when using a read-only (`gql_`) API key.
|
|
45
|
+
*/
|
|
46
|
+
mutate<TData>(
|
|
47
|
+
document: string,
|
|
48
|
+
variables?: Record<string, unknown>,
|
|
49
|
+
options?: RequestOptions,
|
|
50
|
+
): Promise<TData> {
|
|
51
|
+
if (isReadOnlyApiKey(this.config.apiKey)) {
|
|
52
|
+
throw new ContismoGraphQLError("Read-only API keys cannot perform mutations", [
|
|
53
|
+
{ message: "Read-only API keys cannot perform mutations", extensions: { code: ContismoErrorCodes.FORBIDDEN } },
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return executeGraphQL<TData>(this.config, buildGraphQLPayload(document, variables), options);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Download the project GraphQL schema (used by `contismo generate` for TypeScript types). */
|
|
61
|
+
introspect(options?: RequestOptions): Promise<IntrospectionQueryResult> {
|
|
62
|
+
return executeGraphQL<IntrospectionQueryResult>(
|
|
63
|
+
this.config,
|
|
64
|
+
{
|
|
65
|
+
query: INTROSPECTION_QUERY,
|
|
66
|
+
operationName: "IntrospectionQuery",
|
|
67
|
+
},
|
|
68
|
+
{ ...options, operationName: "IntrospectionQuery" },
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Fetch all entries for a content model using a Prisma-style `select` object.
|
|
74
|
+
* Requires a model registry (loaded automatically from config unless you pass `models`).
|
|
75
|
+
*/
|
|
76
|
+
async fetch<T = unknown>(model: string, options: FetchOptions): Promise<T[]> {
|
|
77
|
+
const entry = this.resolveModel(model);
|
|
78
|
+
const { signal, operationName, ...fetchArgs } = options;
|
|
79
|
+
const built = buildFetchListQuery(entry, fetchArgs);
|
|
80
|
+
const requestOptions: RequestOptions = {
|
|
81
|
+
operationName: operationName ?? built.operationName,
|
|
82
|
+
};
|
|
83
|
+
if (signal !== undefined) {
|
|
84
|
+
requestOptions.signal = signal;
|
|
85
|
+
}
|
|
86
|
+
const data = await executeGraphQL<Record<string, T[] | T | null>>(
|
|
87
|
+
this.config,
|
|
88
|
+
buildGraphQLPayload(built.query, built.variables, built.operationName),
|
|
89
|
+
requestOptions,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const result = data[built.resultKey];
|
|
93
|
+
if (entry.isSingleton) {
|
|
94
|
+
return result == null ? [] : [result as T];
|
|
95
|
+
}
|
|
96
|
+
return (result ?? []) as T[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Fetch one entry by id (or unique filters) using a `select` object.
|
|
101
|
+
*/
|
|
102
|
+
async fetchOne<T = unknown>(model: string, options: FetchOneOptions): Promise<T | null> {
|
|
103
|
+
const entry = this.resolveModel(model);
|
|
104
|
+
const { signal, operationName, ...fetchArgs } = options;
|
|
105
|
+
const built = buildFetchOneQuery(entry, fetchArgs);
|
|
106
|
+
const requestOptions: RequestOptions = {
|
|
107
|
+
operationName: operationName ?? built.operationName,
|
|
108
|
+
};
|
|
109
|
+
if (signal !== undefined) {
|
|
110
|
+
requestOptions.signal = signal;
|
|
111
|
+
}
|
|
112
|
+
const data = await executeGraphQL<Record<string, T | null>>(
|
|
113
|
+
this.config,
|
|
114
|
+
buildGraphQLPayload(built.query, built.variables, built.operationName),
|
|
115
|
+
requestOptions,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return data[built.resultKey] ?? null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Low-level GraphQL request with full control over the document and operation name.
|
|
123
|
+
*/
|
|
124
|
+
rawRequest<TData>(
|
|
125
|
+
document: string,
|
|
126
|
+
variables?: Record<string, unknown>,
|
|
127
|
+
options?: RequestOptions,
|
|
128
|
+
): Promise<TData> {
|
|
129
|
+
return executeGraphQL<TData>(
|
|
130
|
+
this.config,
|
|
131
|
+
buildGraphQLPayload(document, variables, options?.operationName),
|
|
132
|
+
options,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private resolveModel(model: string): ModelRegistryEntry {
|
|
137
|
+
const registry = this.config.models;
|
|
138
|
+
if (!registry) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
"fetch requires a model registry. Run `contismo generate`, then construct the client from config (e.g. `new ContismoClient()`).",
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const entry = registry[model];
|
|
145
|
+
if (!entry) {
|
|
146
|
+
const known = Object.keys(registry).join(", ");
|
|
147
|
+
throw new Error(`Unknown model "${model}". Known models: ${known || "(none)"}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return entry;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ContismoClientConfig } from "./types";
|
|
2
|
+
|
|
3
|
+
export type ContismoCodegenConfig = {
|
|
4
|
+
/** Directory for introspection JSON. Default: `./contismo` (file: `schema.json`). */
|
|
5
|
+
schemaDir?: string;
|
|
6
|
+
/** Schema file name inside `schemaDir`. Default: `schema.json`. */
|
|
7
|
+
schemaFile?: string;
|
|
8
|
+
/** Full path to introspection JSON; overrides `schemaDir` / `schemaFile`. */
|
|
9
|
+
schema?: string;
|
|
10
|
+
/** Directory for generated TypeScript. Default: `./src/generated` (file: `contismo.ts`). */
|
|
11
|
+
outputDir?: string;
|
|
12
|
+
/** Generated file name inside `outputDir`. Default: `contismo.ts`. */
|
|
13
|
+
outputFile?: string;
|
|
14
|
+
/** Full path to generated TypeScript; overrides `outputDir` / `outputFile`. */
|
|
15
|
+
output?: string;
|
|
16
|
+
/** Model registry file name inside `outputDir`. Default: `contismo-models.ts`. */
|
|
17
|
+
modelsFile?: string;
|
|
18
|
+
/** Full path to generated model registry; default: `<outputDir>/contismo-models.ts`. */
|
|
19
|
+
models?: string;
|
|
20
|
+
/** Document globs for typed operations. Omit for schema types only. */
|
|
21
|
+
documents?: string[];
|
|
22
|
+
/**
|
|
23
|
+
* Overrides for `@graphql-codegen/typescript` (merged with Contismo defaults:
|
|
24
|
+
* inlined scalars, no `__typename`, enums as string unions).
|
|
25
|
+
*/
|
|
26
|
+
typescript?: {
|
|
27
|
+
scalars?: Record<string, string>;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
};
|
|
30
|
+
/** Post-process types for readable fields (`string | null` vs `Maybe<Scalars[…]>`). Default: `true`. */
|
|
31
|
+
prettify?: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ContismoConfig = {
|
|
35
|
+
client: ContismoClientConfig;
|
|
36
|
+
codegen?: ContismoCodegenConfig;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function defineConfig(config: ContismoConfig): ContismoConfig {
|
|
40
|
+
return config;
|
|
41
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { GraphQLResponseError } from "./types";
|
|
2
|
+
|
|
3
|
+
export const ContismoErrorCodes = {
|
|
4
|
+
NOT_FOUND: "NOT_FOUND",
|
|
5
|
+
FORBIDDEN: "FORBIDDEN",
|
|
6
|
+
CONFLICT: "CONFLICT",
|
|
7
|
+
BAD_REQUEST: "BAD_REQUEST",
|
|
8
|
+
ENTRY_EDIT_LOCK_CONFLICT: "ENTRY_EDIT_LOCK_CONFLICT",
|
|
9
|
+
PLAN_LIMIT_EXCEEDED: "PLAN_LIMIT_EXCEEDED",
|
|
10
|
+
RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
|
|
11
|
+
TAKEOVER_REQUEST_COOLDOWN: "TAKEOVER_REQUEST_COOLDOWN",
|
|
12
|
+
INTERNAL_SERVER_ERROR: "INTERNAL_SERVER_ERROR",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export type ContismoErrorCode = (typeof ContismoErrorCodes)[keyof typeof ContismoErrorCodes] | string;
|
|
16
|
+
|
|
17
|
+
export class ContismoError extends Error {
|
|
18
|
+
readonly code: ContismoErrorCode;
|
|
19
|
+
readonly httpStatus?: number;
|
|
20
|
+
|
|
21
|
+
constructor(message: string, options: { code: ContismoErrorCode; httpStatus?: number }) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "ContismoError";
|
|
24
|
+
this.code = options.code;
|
|
25
|
+
if (options.httpStatus !== undefined) {
|
|
26
|
+
this.httpStatus = options.httpStatus;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class ContismoGraphQLError extends ContismoError {
|
|
32
|
+
readonly errors: GraphQLResponseError[];
|
|
33
|
+
readonly lockedByUserId?: string;
|
|
34
|
+
readonly lockedByName?: string | null;
|
|
35
|
+
readonly limitType?: string;
|
|
36
|
+
readonly retryAfterSeconds?: number;
|
|
37
|
+
|
|
38
|
+
constructor(message: string, errors: GraphQLResponseError[], httpStatus?: number) {
|
|
39
|
+
const primary = errors[0];
|
|
40
|
+
const extensions = primary?.extensions;
|
|
41
|
+
const code = (extensions?.code as ContismoErrorCode | undefined) ?? ContismoErrorCodes.INTERNAL_SERVER_ERROR;
|
|
42
|
+
|
|
43
|
+
super(message, httpStatus === undefined ? { code } : { code, httpStatus });
|
|
44
|
+
this.name = "ContismoGraphQLError";
|
|
45
|
+
this.errors = errors;
|
|
46
|
+
|
|
47
|
+
if (typeof extensions?.lockedByUserId === "string") {
|
|
48
|
+
this.lockedByUserId = extensions.lockedByUserId;
|
|
49
|
+
}
|
|
50
|
+
if (extensions && "lockedByName" in extensions) {
|
|
51
|
+
const name = extensions.lockedByName;
|
|
52
|
+
if (name === null || typeof name === "string") {
|
|
53
|
+
this.lockedByName = name;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (typeof extensions?.limitType === "string") {
|
|
57
|
+
this.limitType = extensions.limitType;
|
|
58
|
+
}
|
|
59
|
+
if (typeof extensions?.retryAfterSeconds === "number" && Number.isFinite(extensions.retryAfterSeconds)) {
|
|
60
|
+
this.retryAfterSeconds = extensions.retryAfterSeconds;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class ContismoHttpError extends ContismoError {
|
|
66
|
+
readonly body: unknown;
|
|
67
|
+
|
|
68
|
+
constructor(message: string, httpStatus: number, code: ContismoErrorCode, body: unknown) {
|
|
69
|
+
super(message, { code, httpStatus });
|
|
70
|
+
this.name = "ContismoHttpError";
|
|
71
|
+
this.body = body;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isContismoError(error: unknown): error is ContismoError {
|
|
76
|
+
return error instanceof ContismoError;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function parseGraphQLErrors(errors: GraphQLResponseError[], httpStatus?: number): ContismoGraphQLError {
|
|
80
|
+
const message = errors.map((e) => e.message).join("\n") || "GraphQL request failed";
|
|
81
|
+
return new ContismoGraphQLError(message, errors, httpStatus);
|
|
82
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { ContismoErrorCodes, ContismoGraphQLError, ContismoHttpError, parseGraphQLErrors } from "./errors";
|
|
2
|
+
import type {
|
|
3
|
+
ContismoClientConfig,
|
|
4
|
+
GraphQLRequestPayload,
|
|
5
|
+
GraphQLResponse,
|
|
6
|
+
RequestOptions,
|
|
7
|
+
} from "./types";
|
|
8
|
+
|
|
9
|
+
export function buildHeaders(config: ContismoClientConfig): Headers {
|
|
10
|
+
const headers = new Headers({
|
|
11
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
12
|
+
"Content-Type": "application/json",
|
|
13
|
+
"X-Project-Id": config.project,
|
|
14
|
+
"X-Environment": config.environment,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
if (config.requestId) {
|
|
18
|
+
headers.set("X-Request-Id", config.requestId);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (config.headers) {
|
|
22
|
+
for (const [key, value] of Object.entries(config.headers)) {
|
|
23
|
+
headers.set(key, value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return headers;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function executeGraphQL<TData>(
|
|
31
|
+
config: ContismoClientConfig,
|
|
32
|
+
payload: GraphQLRequestPayload,
|
|
33
|
+
options?: RequestOptions,
|
|
34
|
+
): Promise<TData> {
|
|
35
|
+
const fetchFn = config.fetch ?? globalThis.fetch;
|
|
36
|
+
const headers = buildHeaders(config);
|
|
37
|
+
|
|
38
|
+
const operationName = payload.operationName ?? options?.operationName;
|
|
39
|
+
const requestBody: Record<string, unknown> = { query: payload.query };
|
|
40
|
+
if (payload.variables !== undefined) {
|
|
41
|
+
requestBody.variables = payload.variables;
|
|
42
|
+
}
|
|
43
|
+
if (operationName !== undefined) {
|
|
44
|
+
requestBody.operationName = operationName;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const requestInit: RequestInit = {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers,
|
|
50
|
+
body: JSON.stringify(requestBody),
|
|
51
|
+
};
|
|
52
|
+
if (options?.signal !== undefined) {
|
|
53
|
+
requestInit.signal = options.signal;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const response = await fetchFn(config.endpoint, requestInit);
|
|
57
|
+
|
|
58
|
+
const httpStatus = response.status;
|
|
59
|
+
let body: unknown;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
body = await response.json();
|
|
63
|
+
} catch {
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
throw new ContismoHttpError(
|
|
66
|
+
`Request failed with status ${httpStatus}`,
|
|
67
|
+
httpStatus,
|
|
68
|
+
ContismoErrorCodes.INTERNAL_SERVER_ERROR,
|
|
69
|
+
null,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
throw new ContismoHttpError("Invalid JSON response", httpStatus, ContismoErrorCodes.INTERNAL_SERVER_ERROR, null);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
const graphqlBody = body as GraphQLResponse<TData>;
|
|
77
|
+
if (graphqlBody.errors?.length) {
|
|
78
|
+
throw parseGraphQLErrors(graphqlBody.errors, httpStatus);
|
|
79
|
+
}
|
|
80
|
+
throw new ContismoHttpError(
|
|
81
|
+
`Request failed with status ${httpStatus}`,
|
|
82
|
+
httpStatus,
|
|
83
|
+
ContismoErrorCodes.INTERNAL_SERVER_ERROR,
|
|
84
|
+
body,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const graphqlBody = body as GraphQLResponse<TData>;
|
|
89
|
+
|
|
90
|
+
if (graphqlBody.errors?.length) {
|
|
91
|
+
throw parseGraphQLErrors(graphqlBody.errors, httpStatus);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (graphqlBody.data === undefined) {
|
|
95
|
+
throw new ContismoHttpError(
|
|
96
|
+
"GraphQL response missing data",
|
|
97
|
+
httpStatus,
|
|
98
|
+
ContismoErrorCodes.INTERNAL_SERVER_ERROR,
|
|
99
|
+
body,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return graphqlBody.data;
|
|
104
|
+
}
|