@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.
Files changed (47) hide show
  1. package/.github/workflows/ci.yml +41 -0
  2. package/.github/workflows/release.yml +38 -0
  3. package/CODE_OF_CONDUCT.md +28 -0
  4. package/CONTRIBUTING.md +34 -0
  5. package/LICENSE +21 -0
  6. package/README.md +166 -0
  7. package/clients/typescript/docs/codegen.md +107 -0
  8. package/clients/typescript/eslint.config.js +18 -0
  9. package/clients/typescript/package.json +87 -0
  10. package/clients/typescript/src/api-keys.ts +13 -0
  11. package/clients/typescript/src/cli/codegen-typescript-config.ts +36 -0
  12. package/clients/typescript/src/cli/generate-models.ts +94 -0
  13. package/clients/typescript/src/cli/generate.ts +44 -0
  14. package/clients/typescript/src/cli/import-from-sdk.ts +12 -0
  15. package/clients/typescript/src/cli/load-config.ts +1 -0
  16. package/clients/typescript/src/cli/main.ts +39 -0
  17. package/clients/typescript/src/cli/prettify-generated-types.ts +129 -0
  18. package/clients/typescript/src/cli/run-codegen.ts +58 -0
  19. package/clients/typescript/src/client.ts +152 -0
  20. package/clients/typescript/src/config.ts +41 -0
  21. package/clients/typescript/src/errors.ts +82 -0
  22. package/clients/typescript/src/execute.ts +104 -0
  23. package/clients/typescript/src/fetch-query.ts +123 -0
  24. package/clients/typescript/src/index.ts +41 -0
  25. package/clients/typescript/src/introspection.ts +165 -0
  26. package/clients/typescript/src/load-client.ts +47 -0
  27. package/clients/typescript/src/load-config.ts +64 -0
  28. package/clients/typescript/src/load-models.ts +35 -0
  29. package/clients/typescript/src/models.ts +16 -0
  30. package/clients/typescript/src/request-payload.ts +16 -0
  31. package/clients/typescript/src/resolve-codegen-paths.ts +31 -0
  32. package/clients/typescript/src/select.ts +22 -0
  33. package/clients/typescript/src/types.ts +55 -0
  34. package/clients/typescript/src/write-introspection.ts +13 -0
  35. package/clients/typescript/tests/api-keys.test.ts +14 -0
  36. package/clients/typescript/tests/client.test.ts +221 -0
  37. package/clients/typescript/tests/load-client.test.ts +126 -0
  38. package/clients/typescript/tests/load-config.test.ts +32 -0
  39. package/clients/typescript/tests/prettify-generated-types.test.ts +43 -0
  40. package/clients/typescript/tests/resolve-codegen-paths.test.ts +57 -0
  41. package/clients/typescript/tests/select.test.ts +53 -0
  42. package/clients/typescript/tests/write-introspection.test.ts +30 -0
  43. package/clients/typescript/tsconfig.json +16 -0
  44. package/clients/typescript/tsup.config.ts +32 -0
  45. package/clients/typescript/vitest.config.ts +8 -0
  46. package/package.json +17 -0
  47. package/pnpm-workspace.yaml +2 -0
@@ -0,0 +1,123 @@
1
+ import { buildSelectionSet, type SelectShape } from "./select";
2
+ import type { ModelRegistryEntry } from "./models";
3
+
4
+ export type FetchListArgs = {
5
+ select: SelectShape;
6
+ limit?: number;
7
+ offset?: number;
8
+ locale?: string;
9
+ where?: Record<string, unknown>;
10
+ status?: string;
11
+ statusIn?: string[];
12
+ statusNotIn?: string[];
13
+ [key: string]: unknown;
14
+ };
15
+
16
+ export type FetchOneArgs = {
17
+ select: SelectShape;
18
+ id?: string;
19
+ locale?: string;
20
+ status?: string;
21
+ statusIn?: string[];
22
+ statusNotIn?: string[];
23
+ where?: Record<string, unknown>;
24
+ [key: string]: unknown;
25
+ };
26
+
27
+ const RESERVED_LIST_KEYS = new Set(["select", "signal", "operationName"]);
28
+ const RESERVED_ONE_KEYS = new Set(["select", "signal", "operationName"]);
29
+
30
+ function collectVariables(
31
+ args: Record<string, unknown>,
32
+ reserved: Set<string>,
33
+ model?: ModelRegistryEntry,
34
+ ): { variables: Record<string, unknown>; variableDefs: string[]; argList: string[] } {
35
+ const variables: Record<string, unknown> = {};
36
+ const variableDefs: string[] = [];
37
+ const argList: string[] = [];
38
+
39
+ for (const [key, value] of Object.entries(args)) {
40
+ if (reserved.has(key) || value === undefined) {
41
+ continue;
42
+ }
43
+ const varName = key;
44
+ variables[varName] = value;
45
+ variableDefs.push(`$${varName}: ${graphqlVariableType(key, model)}`);
46
+ argList.push(`${key}: $${varName}`);
47
+ }
48
+
49
+ return { variables, variableDefs, argList };
50
+ }
51
+
52
+ function graphqlVariableType(key: string, model?: ModelRegistryEntry): string {
53
+ if (key === "limit" || key === "offset") {
54
+ return "Int";
55
+ }
56
+ if (key === "id") {
57
+ return "ID";
58
+ }
59
+ if (key === "statusIn" || key === "statusNotIn") {
60
+ return "[EntryStatus!]";
61
+ }
62
+ if (key === "status") {
63
+ return "EntryStatus";
64
+ }
65
+ if (key === "where" && model?.whereInput) {
66
+ return model.whereInput;
67
+ }
68
+ return "String";
69
+ }
70
+
71
+ export function buildFetchListQuery(model: ModelRegistryEntry, args: FetchListArgs): {
72
+ query: string;
73
+ variables: Record<string, unknown>;
74
+ operationName: string;
75
+ resultKey: string;
76
+ } {
77
+ const selection = buildSelectionSet(args.select);
78
+ if (!selection) {
79
+ throw new Error("select must include at least one field");
80
+ }
81
+
82
+ const field = model.isSingleton ? model.singular : model.plural;
83
+ const operationName = `Fetch${model.apiId}List`;
84
+ const { select: _select, ...rest } = args;
85
+ const { variables, variableDefs, argList } = collectVariables(rest, RESERVED_LIST_KEYS, model);
86
+ const fieldArgs = argList.length > 0 ? `(${argList.join(", ")})` : "";
87
+ const varDefs = variableDefs.length > 0 ? `(${variableDefs.join(", ")})` : "";
88
+
89
+ const query = `query ${operationName}${varDefs} {
90
+ ${field}${fieldArgs} {
91
+ ${selection}
92
+ }
93
+ }`;
94
+
95
+ return { query, variables, operationName, resultKey: field };
96
+ }
97
+
98
+ export function buildFetchOneQuery(model: ModelRegistryEntry, args: FetchOneArgs): {
99
+ query: string;
100
+ variables: Record<string, unknown>;
101
+ operationName: string;
102
+ resultKey: string;
103
+ } {
104
+ const selection = buildSelectionSet(args.select);
105
+ if (!selection) {
106
+ throw new Error("select must include at least one field");
107
+ }
108
+
109
+ const field = model.singular;
110
+ const operationName = `Fetch${model.apiId}One`;
111
+ const { select: _select, ...rest } = args;
112
+ const { variables, variableDefs, argList } = collectVariables(rest, RESERVED_ONE_KEYS, model);
113
+ const fieldArgs = argList.length > 0 ? `(${argList.join(", ")})` : "";
114
+ const varDefs = variableDefs.length > 0 ? `(${variableDefs.join(", ")})` : "";
115
+
116
+ const query = `query ${operationName}${varDefs} {
117
+ ${field}${fieldArgs} {
118
+ ${selection}
119
+ }
120
+ }`;
121
+
122
+ return { query, variables, operationName, resultKey: field };
123
+ }
@@ -0,0 +1,41 @@
1
+ export {
2
+ ContismoClient,
3
+ type ContismoClientOptions,
4
+ type FetchOptions,
5
+ type FetchOneOptions,
6
+ } from "./client";
7
+ export { resolveContismoClientConfig } from "./load-client";
8
+ export { findConfigFile, loadContismoConfig } from "./load-config";
9
+ export { loadContismoModels } from "./load-models";
10
+ export type { ModelRegistry, ModelRegistryEntry } from "./models";
11
+ export type { SelectShape } from "./select";
12
+ export type { FetchListArgs, FetchOneArgs } from "./fetch-query";
13
+ export {
14
+ READ_ONLY_API_KEY_PREFIX,
15
+ READ_WRITE_API_KEY_PREFIX,
16
+ isReadOnlyApiKey,
17
+ isReadWriteApiKey,
18
+ } from "./api-keys";
19
+ export {
20
+ ContismoError,
21
+ ContismoGraphQLError,
22
+ ContismoHttpError,
23
+ ContismoErrorCodes,
24
+ isContismoError,
25
+ } from "./errors";
26
+ export {
27
+ INTROSPECTION_QUERY,
28
+ type IntrospectionQueryResult,
29
+ type IntrospectionSchema,
30
+ type IntrospectionType,
31
+ type IntrospectionField,
32
+ type IntrospectionTypeRef,
33
+ } from "./introspection";
34
+ export { writeIntrospectionResult } from "./write-introspection";
35
+ export type {
36
+ ContismoClientConfig,
37
+ RequestOptions,
38
+ GraphQLResponse,
39
+ GraphQLResponseError,
40
+ GraphQLErrorExtensions,
41
+ } from "./types";
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Standard GraphQL introspection query (compatible with GraphiQL and graphql-codegen).
3
+ * @see https://spec.graphql.org/draft/#sec-Introspection
4
+ */
5
+ export const INTROSPECTION_QUERY = /* GraphQL */ `
6
+ query IntrospectionQuery {
7
+ __schema {
8
+ queryType {
9
+ name
10
+ }
11
+ mutationType {
12
+ name
13
+ }
14
+ subscriptionType {
15
+ name
16
+ }
17
+ types {
18
+ ...FullType
19
+ }
20
+ directives {
21
+ name
22
+ description
23
+ locations
24
+ args {
25
+ ...InputValue
26
+ }
27
+ }
28
+ }
29
+ }
30
+
31
+ fragment FullType on __Type {
32
+ kind
33
+ name
34
+ description
35
+ fields(includeDeprecated: true) {
36
+ name
37
+ description
38
+ args {
39
+ ...InputValue
40
+ }
41
+ type {
42
+ ...TypeRef
43
+ }
44
+ isDeprecated
45
+ deprecationReason
46
+ }
47
+ inputFields {
48
+ ...InputValue
49
+ }
50
+ interfaces {
51
+ ...TypeRef
52
+ }
53
+ enumValues(includeDeprecated: true) {
54
+ name
55
+ description
56
+ isDeprecated
57
+ deprecationReason
58
+ }
59
+ possibleTypes {
60
+ ...TypeRef
61
+ }
62
+ }
63
+
64
+ fragment InputValue on __InputValue {
65
+ name
66
+ description
67
+ type {
68
+ ...TypeRef
69
+ }
70
+ defaultValue
71
+ }
72
+
73
+ fragment TypeRef on __Type {
74
+ kind
75
+ name
76
+ ofType {
77
+ kind
78
+ name
79
+ ofType {
80
+ kind
81
+ name
82
+ ofType {
83
+ kind
84
+ name
85
+ ofType {
86
+ kind
87
+ name
88
+ ofType {
89
+ kind
90
+ name
91
+ ofType {
92
+ kind
93
+ name
94
+ ofType {
95
+ kind
96
+ name
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }
104
+ }
105
+ `;
106
+
107
+ /** Subset of the introspection query result used by codegen tools. */
108
+ export type IntrospectionQueryResult = {
109
+ __schema: IntrospectionSchema;
110
+ };
111
+
112
+ export type IntrospectionSchema = {
113
+ queryType: { name: string } | null;
114
+ mutationType: { name: string } | null;
115
+ subscriptionType: { name: string } | null;
116
+ types: readonly IntrospectionType[];
117
+ directives: readonly IntrospectionDirective[];
118
+ };
119
+
120
+ export type IntrospectionType = {
121
+ kind: string;
122
+ name: string | null;
123
+ description?: string | null;
124
+ fields?: readonly IntrospectionField[] | null;
125
+ inputFields?: readonly IntrospectionInputValue[] | null;
126
+ interfaces?: readonly IntrospectionTypeRef[] | null;
127
+ enumValues?: readonly IntrospectionEnumValue[] | null;
128
+ possibleTypes?: readonly IntrospectionTypeRef[] | null;
129
+ };
130
+
131
+ export type IntrospectionField = {
132
+ name: string;
133
+ description?: string | null;
134
+ args: readonly IntrospectionInputValue[];
135
+ type: IntrospectionTypeRef;
136
+ isDeprecated: boolean;
137
+ deprecationReason?: string | null;
138
+ };
139
+
140
+ export type IntrospectionInputValue = {
141
+ name: string;
142
+ description?: string | null;
143
+ type: IntrospectionTypeRef;
144
+ defaultValue?: string | null;
145
+ };
146
+
147
+ export type IntrospectionEnumValue = {
148
+ name: string;
149
+ description?: string | null;
150
+ isDeprecated: boolean;
151
+ deprecationReason?: string | null;
152
+ };
153
+
154
+ export type IntrospectionDirective = {
155
+ name: string;
156
+ description?: string | null;
157
+ locations: readonly string[];
158
+ args: readonly IntrospectionInputValue[];
159
+ };
160
+
161
+ export type IntrospectionTypeRef = {
162
+ kind: string;
163
+ name: string | null;
164
+ ofType?: IntrospectionTypeRef | null;
165
+ };
@@ -0,0 +1,47 @@
1
+ import { findConfigFile, loadContismoConfig } from "./load-config";
2
+ import { loadContismoModels } from "./load-models";
3
+ import type { ContismoClientConfig } from "./types";
4
+
5
+ export type ContismoClientOptions = {
6
+ /** Working directory for config and generated file discovery. Default: `process.cwd()`. */
7
+ cwd?: string;
8
+ /** Path to `contismo.config.*` when it is not in the project root. */
9
+ config?: string;
10
+ } & Partial<Omit<ContismoClientConfig, "models">>;
11
+
12
+ function hasExplicitCredentials(
13
+ options: ContismoClientConfig | ContismoClientOptions,
14
+ ): options is ContismoClientConfig {
15
+ return typeof options.apiKey === "string" && options.apiKey.length > 0;
16
+ }
17
+
18
+ /**
19
+ * Resolve client settings from explicit credentials or `contismo.config.*` plus the generated model registry.
20
+ */
21
+ export function resolveContismoClientConfig(
22
+ options: ContismoClientConfig | ContismoClientOptions = {},
23
+ ): ContismoClientConfig {
24
+ const cwd =
25
+ "cwd" in options && options.cwd !== undefined ? options.cwd : process.cwd();
26
+
27
+ if (hasExplicitCredentials(options)) {
28
+ if (options.models) {
29
+ return options;
30
+ }
31
+ if (findConfigFile(cwd)) {
32
+ const fileConfig = loadContismoConfig(cwd);
33
+ return { ...options, models: loadContismoModels(cwd, fileConfig.codegen) };
34
+ }
35
+ return options;
36
+ }
37
+
38
+ const { cwd: _cwd, config: configPath, ...overrides } = options as ContismoClientOptions;
39
+ const fileConfig = loadContismoConfig(cwd, configPath);
40
+ const models = loadContismoModels(cwd, fileConfig.codegen);
41
+
42
+ return {
43
+ ...fileConfig.client,
44
+ ...overrides,
45
+ models,
46
+ };
47
+ }
@@ -0,0 +1,64 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { createJiti } from "jiti";
4
+ import type { ContismoConfig } from "./config";
5
+
6
+ const CONFIG_NAMES = [
7
+ "contismo.config.ts",
8
+ "contismo.config.mts",
9
+ "contismo.config.js",
10
+ "contismo.config.mjs",
11
+ "contismo.config.cjs",
12
+ "contismo.config.json",
13
+ ] as const;
14
+
15
+ export function findConfigFile(cwd: string): string | null {
16
+ for (const name of CONFIG_NAMES) {
17
+ const path = resolve(cwd, name);
18
+ if (existsSync(path)) {
19
+ return path;
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+
25
+ export function loadContismoConfig(cwd: string, configPath?: string): ContismoConfig {
26
+ const resolved =
27
+ configPath !== undefined ? resolve(cwd, configPath) : findConfigFile(cwd);
28
+
29
+ if (!resolved || !existsSync(resolved)) {
30
+ throw new Error(
31
+ `No Contismo config found in ${cwd}. Create contismo.config.ts:\n\n` +
32
+ ` import { defineConfig } from "@contismo/sdk/config";\n\n` +
33
+ ` export default defineConfig({\n` +
34
+ ` client: { apiKey: "gql_…", endpoint: "https://graphql.contismo.com", project: "…", environment: "production" },\n` +
35
+ ` });\n`,
36
+ );
37
+ }
38
+
39
+ if (resolved.endsWith(".json")) {
40
+ const config = JSON.parse(readFileSync(resolved, "utf8")) as ContismoConfig;
41
+ assertConfig(config, resolved);
42
+ return config;
43
+ }
44
+
45
+ const jiti = createJiti(cwd, { interopDefault: true });
46
+ const loaded = jiti(resolved) as { default?: ContismoConfig } | ContismoConfig;
47
+ const config = (loaded as { default?: ContismoConfig }).default ?? (loaded as ContismoConfig);
48
+ assertConfig(config, resolved);
49
+ return config;
50
+ }
51
+
52
+ function assertConfig(config: unknown, path: string): asserts config is ContismoConfig {
53
+ if (!config || typeof config !== "object" || !("client" in config)) {
54
+ throw new Error(`Invalid config at ${path}: expected { client: { apiKey, endpoint, project, environment } }`);
55
+ }
56
+
57
+ const { client } = config as ContismoConfig;
58
+ const required = ["apiKey", "endpoint", "project", "environment"] as const;
59
+ for (const key of required) {
60
+ if (typeof client[key] !== "string" || client[key].length === 0) {
61
+ throw new Error(`Invalid config at ${path}: client.${key} is required`);
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,35 @@
1
+ import { existsSync } from "node:fs";
2
+ import { createJiti } from "jiti";
3
+ import type { ContismoCodegenConfig } from "./config";
4
+ import type { ModelRegistry } from "./models";
5
+ import { resolveCodegenPaths } from "./resolve-codegen-paths";
6
+
7
+ export function loadContismoModels(cwd: string, codegen?: ContismoCodegenConfig): ModelRegistry {
8
+ const { modelsPath } = resolveCodegenPaths(cwd, codegen);
9
+
10
+ if (!existsSync(modelsPath)) {
11
+ throw new Error(
12
+ `Model registry not found at ${modelsPath}. Run \`contismo generate\` first.`,
13
+ );
14
+ }
15
+
16
+ const jiti = createJiti(cwd, { interopDefault: true });
17
+ const loaded = jiti(modelsPath) as {
18
+ contismoModels?: ModelRegistry;
19
+ default?: ModelRegistry | { contismoModels?: ModelRegistry };
20
+ };
21
+
22
+ const raw =
23
+ loaded.contismoModels ??
24
+ (loaded.default && typeof loaded.default === "object" && "contismoModels" in loaded.default
25
+ ? (loaded.default as { contismoModels?: ModelRegistry }).contismoModels
26
+ : loaded.default);
27
+
28
+ if (!raw || typeof raw !== "object") {
29
+ throw new Error(
30
+ `Expected \`contismoModels\` export in ${modelsPath}. Run \`contismo generate\`.`,
31
+ );
32
+ }
33
+
34
+ return raw as ModelRegistry;
35
+ }
@@ -0,0 +1,16 @@
1
+ export type ModelRegistryEntry = {
2
+ /** Content model API ID (e.g. `Test`). */
3
+ apiId: string;
4
+ /** GraphQL query field for one entry (e.g. `test`). */
5
+ singular: string;
6
+ /** GraphQL query field for many entries (e.g. `tests`). Same as `singular` when singleton. */
7
+ plural: string;
8
+ /** GraphQL object type name (e.g. `Entry_Test`). */
9
+ graphqlType: string;
10
+ /** When true, only the singular field exists on Query. */
11
+ isSingleton?: boolean;
12
+ /** GraphQL input type for `where` on list queries (e.g. `Entry_Test_WhereInput`). */
13
+ whereInput?: string;
14
+ };
15
+
16
+ export type ModelRegistry = Record<string, ModelRegistryEntry>;
@@ -0,0 +1,16 @@
1
+ import type { GraphQLRequestPayload } from "./types";
2
+
3
+ export function buildGraphQLPayload(
4
+ query: string,
5
+ variables?: Record<string, unknown>,
6
+ operationName?: string,
7
+ ): GraphQLRequestPayload {
8
+ const payload: GraphQLRequestPayload = { query };
9
+ if (variables !== undefined) {
10
+ payload.variables = variables;
11
+ }
12
+ if (operationName !== undefined) {
13
+ payload.operationName = operationName;
14
+ }
15
+ return payload;
16
+ }
@@ -0,0 +1,31 @@
1
+ import { dirname, join, resolve } from "node:path";
2
+ import type { ContismoCodegenConfig } from "./config";
3
+
4
+ export type ResolvedCodegenPaths = {
5
+ schemaPath: string;
6
+ outputPath: string;
7
+ modelsPath: string;
8
+ };
9
+
10
+ export function resolveCodegenPaths(cwd: string, codegen?: ContismoCodegenConfig): ResolvedCodegenPaths {
11
+ const schemaPath =
12
+ codegen?.schema !== undefined
13
+ ? resolve(cwd, codegen.schema)
14
+ : join(resolve(cwd, codegen?.schemaDir ?? "contismo"), codegen?.schemaFile ?? "schema.json");
15
+
16
+ const outputDir = resolve(cwd, codegen?.outputDir ?? "src/generated");
17
+
18
+ const outputPath =
19
+ codegen?.output !== undefined
20
+ ? resolve(cwd, codegen.output)
21
+ : join(outputDir, codegen?.outputFile ?? "contismo.ts");
22
+
23
+ const modelsDir = codegen?.output !== undefined ? dirname(outputPath) : outputDir;
24
+
25
+ const modelsPath =
26
+ codegen?.models !== undefined
27
+ ? resolve(cwd, codegen.models)
28
+ : join(modelsDir, codegen?.modelsFile ?? "contismo-models.ts");
29
+
30
+ return { schemaPath, outputPath, modelsPath };
31
+ }
@@ -0,0 +1,22 @@
1
+ export type SelectShape = {
2
+ [field: string]: boolean | SelectShape;
3
+ };
4
+
5
+ export function buildSelectionSet(select: SelectShape): string {
6
+ const parts: string[] = [];
7
+
8
+ for (const [key, value] of Object.entries(select)) {
9
+ if (value === true) {
10
+ parts.push(key);
11
+ continue;
12
+ }
13
+ if (value && typeof value === "object") {
14
+ const nested = buildSelectionSet(value);
15
+ if (nested.length > 0) {
16
+ parts.push(`${key} { ${nested} }`);
17
+ }
18
+ }
19
+ }
20
+
21
+ return parts.join(" ");
22
+ }
@@ -0,0 +1,55 @@
1
+ import type { ModelRegistry } from "./models";
2
+
3
+ export type ContismoClientConfig = {
4
+ /** GraphQL API key (`gql_…` or `gqlw_…`). */
5
+ apiKey: string;
6
+ /** Full GraphQL endpoint URL from Studio settings. */
7
+ endpoint: string;
8
+ /** Project ID from Studio (sent as `X-Project-Id`). */
9
+ project: string;
10
+ /** Environment apiId (sent as `X-Environment`). */
11
+ environment: string;
12
+ /**
13
+ * Model registry from generated `contismo-models.ts`.
14
+ * Loaded automatically when constructing from config; only pass manually for custom setups.
15
+ */
16
+ models?: ModelRegistry;
17
+ /** Custom fetch implementation (defaults to global `fetch`). */
18
+ fetch?: typeof fetch;
19
+ /** Additional headers merged after SDK defaults. */
20
+ headers?: Record<string, string>;
21
+ /** Optional `X-Request-Id` applied to every request when set. */
22
+ requestId?: string;
23
+ };
24
+
25
+ export type RequestOptions = {
26
+ signal?: AbortSignal;
27
+ operationName?: string;
28
+ };
29
+
30
+ export type GraphQLRequestPayload = {
31
+ query: string;
32
+ variables?: Record<string, unknown>;
33
+ operationName?: string;
34
+ };
35
+
36
+ export type GraphQLResponse<TData> = {
37
+ data?: TData;
38
+ errors?: GraphQLResponseError[];
39
+ };
40
+
41
+ export type GraphQLResponseError = {
42
+ message: string;
43
+ extensions?: GraphQLErrorExtensions;
44
+ };
45
+
46
+ export type GraphQLErrorExtensions = {
47
+ code?: string;
48
+ lockedByUserId?: string;
49
+ lockedByName?: string | null;
50
+ limitType?: string;
51
+ currentUsage?: number;
52
+ limit?: number;
53
+ retryAfterSeconds?: number;
54
+ [key: string]: unknown;
55
+ };
@@ -0,0 +1,13 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import type { IntrospectionQueryResult } from "./introspection";
3
+
4
+ /**
5
+ * Writes an introspection result to disk (for offline graphql-codegen runs).
6
+ */
7
+ export async function writeIntrospectionResult(
8
+ result: IntrospectionQueryResult,
9
+ filePath: string,
10
+ ): Promise<void> {
11
+ const payload = { data: result };
12
+ await writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
13
+ }
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { isReadOnlyApiKey, isReadWriteApiKey } from "../src/api-keys";
3
+
4
+ describe("api key helpers", () => {
5
+ it("identifies read-only keys", () => {
6
+ expect(isReadOnlyApiKey("gql_abc")).toBe(true);
7
+ expect(isReadWriteApiKey("gql_abc")).toBe(false);
8
+ });
9
+
10
+ it("identifies read-write keys", () => {
11
+ expect(isReadWriteApiKey("gqlw_abc")).toBe(true);
12
+ expect(isReadOnlyApiKey("gqlw_abc")).toBe(false);
13
+ });
14
+ });