@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,41 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ build:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: pnpm/action-setup@v4
20
+ with:
21
+ version: 9
22
+
23
+ - uses: actions/setup-node@v4
24
+ with:
25
+ node-version: 22
26
+ cache: pnpm
27
+
28
+ - run: pnpm install --frozen-lockfile
29
+ working-directory: .
30
+
31
+ - run: pnpm typecheck
32
+ working-directory: .
33
+
34
+ - run: pnpm lint
35
+ working-directory: .
36
+
37
+ - run: pnpm test
38
+ working-directory: .
39
+
40
+ - run: pnpm build
41
+ working-directory: .
@@ -0,0 +1,38 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ concurrency:
9
+ group: ${{ github.workflow }}-release
10
+ cancel-in-progress: false
11
+
12
+ jobs:
13
+ publish:
14
+ runs-on: ubuntu-latest
15
+ permissions:
16
+ contents: read
17
+ id-token: write
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - uses: pnpm/action-setup@v4
22
+ with:
23
+ version: 9
24
+
25
+ - uses: actions/setup-node@v4
26
+ with:
27
+ node-version: 22
28
+ cache: pnpm
29
+ registry-url: https://registry.npmjs.org
30
+
31
+ - run: pnpm install --frozen-lockfile
32
+
33
+ - run: pnpm build
34
+
35
+ - name: Publish to npm
36
+ run: pnpm --filter @contismo/sdk publish --no-git-checks --access public
37
+ env:
38
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1,28 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our pledge
4
+
5
+ We pledge to make participation in our community a harassment-free experience for everyone.
6
+
7
+ ## Our standards
8
+
9
+ Examples of behavior that contributes to a positive environment:
10
+
11
+ - Using welcoming and inclusive language
12
+ - Being respectful of differing viewpoints and experiences
13
+ - Gracefully accepting constructive criticism
14
+ - Focusing on what is best for the community
15
+
16
+ Examples of unacceptable behavior:
17
+
18
+ - Trolling, insulting or derogatory comments, and personal or political attacks
19
+ - Public or private harassment
20
+ - Publishing others' private information without explicit permission
21
+
22
+ ## Enforcement
23
+
24
+ Project maintainers may remove, edit, or reject comments, commits, code, and other contributions that are not aligned to this Code of Conduct.
25
+
26
+ ## Attribution
27
+
28
+ This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1.
@@ -0,0 +1,34 @@
1
+ # Contributing
2
+
3
+ Thank you for contributing to the Contismo SDK.
4
+
5
+ ## Setup
6
+
7
+ Requirements: Node.js 20+, pnpm 9.
8
+
9
+ ```bash
10
+ pnpm install
11
+ ```
12
+
13
+ ## Commands
14
+
15
+ | Command | Description |
16
+ | ------- | ----------- |
17
+ | `pnpm test` | Run unit tests |
18
+ | `pnpm build` | Build `@contismo/sdk` |
19
+ | `pnpm typecheck` | Typecheck the TypeScript client |
20
+ | `pnpm lint` | Lint the TypeScript client |
21
+
22
+ ## Releases
23
+
24
+ Bump the version in `clients/typescript/package.json`, tag the commit (e.g. `v0.1.0`), and push the tag. The release workflow publishes `@contismo/sdk` to npm.
25
+
26
+ ## Pull requests
27
+
28
+ - Keep changes focused and well-tested.
29
+ - Match existing code style (`pnpm lint`).
30
+ - Update documentation when changing public API behavior.
31
+
32
+ ## Live API tests
33
+
34
+ Optional integration tests against a real Contismo project are not run in CI. To test locally, construct a `ContismoClient` with your own endpoint, environment, and API key in a scratch script or test — do not commit secrets.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Contismo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # Contismo SDK
2
+
3
+ Official client libraries for the [Contismo](https://contismo.com) GraphQL Content API.
4
+
5
+ ## TypeScript
6
+
7
+ Install:
8
+
9
+ ```bash
10
+ pnpm add @contismo/sdk
11
+ ```
12
+
13
+ Quick start (with `contismo.config.ts` in your project root):
14
+
15
+ ```typescript
16
+ import { ContismoClient } from "@contismo/sdk";
17
+
18
+ const client = new ContismoClient();
19
+
20
+ const posts = await client.fetch("BlogPost", {
21
+ select: { _id: true, title: true },
22
+ limit: 10,
23
+ });
24
+ ```
25
+
26
+ Use `new ContismoClient({ config: "./path/to/contismo.config.ts" })` when the config file is elsewhere, or `{ cwd: "…" }` when the process cwd differs. Credentials and codegen paths come from the config file; the generated model registry is loaded automatically.
27
+
28
+ For explicit credentials without a config file:
29
+
30
+ ```typescript
31
+ const client = new ContismoClient({
32
+ apiKey: "gql_…",
33
+ endpoint: "https://graphql.contismo.com",
34
+ project: "your-project-id",
35
+ environment: "production",
36
+ });
37
+ ```
38
+
39
+ ### API keys
40
+
41
+ | Key prefix | Access |
42
+ | ---------- | ------------------------------------- |
43
+ | `gql_` | Queries and introspection only |
44
+ | `gqlw_` | Queries, mutations, and introspection |
45
+
46
+ Pass the key as `apiKey` in the client constructor with `Authorization: Bearer` applied automatically.
47
+
48
+ ### Required configuration
49
+
50
+ | Option | Description |
51
+ | ------------- | ----------------------------------------------------- |
52
+ | `apiKey` | GraphQL API key from Studio (**Settings → API keys**) |
53
+ | `endpoint` | GraphQL URL (`https://graphql.contismo.com`) |
54
+ | `project` | Project ID from Studio (sent as `X-Project-Id`) |
55
+ | `environment` | Environment apiId (sent as `X-Environment`) |
56
+
57
+ ### Mutations
58
+
59
+ Use a read/write key (`gqlw_…`) and `client.mutate()`:
60
+
61
+ ```typescript
62
+ const client = new ContismoClient({
63
+ apiKey: "gqlw_…",
64
+ endpoint: "https://graphql.contismo.com",
65
+ project: "your-project-id",
66
+ environment: "production",
67
+ });
68
+
69
+ await client.mutate(
70
+ `
71
+ mutation CreatePost($input: BlogPostCreateInput!) {
72
+ createBlogPost(input: $input) { _id }
73
+ }
74
+ `,
75
+ { input: { title: "Hello" } },
76
+ );
77
+ ```
78
+
79
+ ### Generate types
80
+
81
+ Add `contismo.config.ts`:
82
+
83
+ ```typescript
84
+ import { defineConfig } from "@contismo/sdk/config";
85
+
86
+ export default defineConfig({
87
+ client: {
88
+ apiKey: "gql_…",
89
+ endpoint: "https://graphql.contismo.com",
90
+ project: "your-project-id",
91
+ environment: "production",
92
+ },
93
+ });
94
+ ```
95
+
96
+ Run:
97
+
98
+ ```bash
99
+ pnpm contismo generate
100
+ ```
101
+
102
+ Writes `contismo/schema.json`, `src/generated/contismo.ts`, and `contismo-models.ts` by default (`schemaDir`, `outputDir`, and file names are configurable). See [clients/typescript/docs/codegen.md](clients/typescript/docs/codegen.md) for options.
103
+
104
+ ### Select-based reads
105
+
106
+ After `contismo generate`, use `fetch` / `fetchOne` with a Prisma-style `select` — no hand-written query:
107
+
108
+ ```typescript
109
+ import type { Entry_BlogPost } from "./generated/contismo";
110
+
111
+ const client = new ContismoClient();
112
+
113
+ const posts = await client.fetch<Entry_BlogPost>("BlogPost", {
114
+ select: {
115
+ _id: true,
116
+ title: true,
117
+ author: { name: true },
118
+ },
119
+ limit: 10,
120
+ });
121
+
122
+ const post = await client.fetchOne<Entry_BlogPost>("BlogPost", {
123
+ id: "…",
124
+ select: { _id: true, title: true },
125
+ });
126
+ ```
127
+
128
+ The first argument is the content model API id (`"BlogPost"`, `"Test"`, …). Nested objects in `select` become nested GraphQL selections; `true` marks a scalar leaf.
129
+
130
+ ### Introspection
131
+
132
+ ```typescript
133
+ const schema = await client.introspect();
134
+ ```
135
+
136
+ ### Errors
137
+
138
+ ```typescript
139
+ import { ContismoClient, isContismoError, ContismoGraphQLError } from "@contismo/sdk";
140
+
141
+ try {
142
+ await client.query(`query { blogPost(id: "missing") { _id } }`);
143
+ } catch (error) {
144
+ if (isContismoError(error) && error instanceof ContismoGraphQLError) {
145
+ console.error(error.code, error.message);
146
+ }
147
+ }
148
+ ```
149
+
150
+ ## Other languages
151
+
152
+ Additional clients will live under `clients/<language>/` when available.
153
+
154
+ ## Development
155
+
156
+ ```bash
157
+ pnpm install
158
+ pnpm test
159
+ pnpm build
160
+ ```
161
+
162
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
163
+
164
+ ## License
165
+
166
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,107 @@
1
+ # TypeScript types from your schema
2
+
3
+ ## Quick path: `contismo generate`
4
+
5
+ 1. Create `contismo.config.ts` in your project root:
6
+
7
+ ```typescript
8
+ import { defineConfig } from "@contismo/sdk/config";
9
+
10
+ export default defineConfig({
11
+ client: {
12
+ apiKey: "gql_…",
13
+ endpoint: "https://graphql.contismo.com",
14
+ project: "your-project-id",
15
+ environment: "production",
16
+ },
17
+ codegen: {
18
+ outputDir: "./src/generated",
19
+ outputFile: "contismo.ts",
20
+ schemaDir: "./contismo",
21
+ // Or full paths: schema: "./contismo/schema.json", output: "./src/generated/contismo.ts"
22
+ // documents: ["src/**/*.graphql"], // optional: typed operations
23
+ },
24
+ });
25
+ ```
26
+
27
+ 2. Add a script (optional):
28
+
29
+ ```json
30
+ {
31
+ "scripts": {
32
+ "generate": "contismo generate"
33
+ }
34
+ }
35
+ ```
36
+
37
+ 3. Run:
38
+
39
+ ```bash
40
+ pnpm generate
41
+ ```
42
+
43
+ This pulls your live GraphQL schema and generates TypeScript types from it, similar to `prisma generate`.
44
+
45
+ Generated entry types use readable fields (`string | null`, `EntryStatus | null`) instead of `Maybe<Scalars['DateTime']['output']>`. Set `codegen.prettify: false` to keep raw graphql-codegen output.
46
+
47
+ ## Manual workflow
48
+
49
+ ### Fetch the schema
50
+
51
+ ```typescript
52
+ import { ContismoClient, writeIntrospectionResult } from "@contismo/sdk";
53
+
54
+ const client = new ContismoClient({
55
+ apiKey: "gql_…",
56
+ endpoint: "https://graphql.contismo.com",
57
+ project: "your-project-id",
58
+ environment: "production",
59
+ });
60
+
61
+ const schema = await client.introspect();
62
+ await writeIntrospectionResult(schema, "./contismo/schema.json");
63
+ ```
64
+
65
+ ### Configure codegen yourself
66
+
67
+ See [GraphQL Code Generator](https://the-guild.dev/graphql/codegen) docs if you prefer a custom `codegen.ts`.
68
+
69
+ ### Select-based reads (no hand-written query)
70
+
71
+ `contismo generate` writes `contismo-models.ts`. `new ContismoClient()` reads `contismo.config.*` and loads that registry automatically (paths follow your `codegen` settings).
72
+
73
+ ```typescript
74
+ import { ContismoClient } from "@contismo/sdk";
75
+ import type { Entry_BlogPost } from "./generated/contismo";
76
+
77
+ const client = new ContismoClient();
78
+ // Custom config path: new ContismoClient({ config: "./config/contismo.config.ts" });
79
+
80
+ const posts = await client.fetch<Entry_BlogPost>("BlogPost", {
81
+ select: {
82
+ _id: true,
83
+ title: true,
84
+ author: { name: true, avatar: { url: true } },
85
+ },
86
+ limit: 10,
87
+ });
88
+
89
+ const post = await client.fetchOne<Entry_BlogPost>("BlogPost", {
90
+ id: "…",
91
+ select: { _id: true, title: true },
92
+ });
93
+ ```
94
+
95
+ Use the content model **API id** (e.g. `"BlogPost"`, `"Test"`) as the first argument. Nested `select` objects mirror GraphQL field selection (`true` = scalar leaf, nested object = sub-selection).
96
+
97
+ ### Raw GraphQL (optional)
98
+
99
+ ```typescript
100
+ import type { Query } from "./generated/contismo";
101
+
102
+ const data = await client.query<Pick<Query, "blogPosts">>(`
103
+ query { blogPosts { _id title } }
104
+ `);
105
+ ```
106
+
107
+ When using `documents` in config, you can use `TypedDocumentNode` outputs with `client.query` / `client.mutate` as usual.
@@ -0,0 +1,18 @@
1
+ import eslint from "@eslint/js";
2
+ import globals from "globals";
3
+ import tseslint from "typescript-eslint";
4
+
5
+ export default tseslint.config(
6
+ eslint.configs.recommended,
7
+ ...tseslint.configs.recommended,
8
+ {
9
+ languageOptions: {
10
+ globals: {
11
+ ...globals.node,
12
+ },
13
+ },
14
+ },
15
+ {
16
+ ignores: ["dist/**"],
17
+ },
18
+ );
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "@contismo/ts-client",
3
+ "version": "0.1.0",
4
+ "description": "Official TypeScript client for the Contismo GraphQL Content Delivery API",
5
+ "license": "MIT",
6
+ "author": "Contismo",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/contismo/cms-sdk.git",
10
+ "directory": "clients/typescript"
11
+ },
12
+ "homepage": "https://github.com/contismo/cms-sdk#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/contismo/cms-sdk/issues"
15
+ },
16
+ "keywords": [
17
+ "contismo",
18
+ "cms",
19
+ "graphql",
20
+ "headless-cms",
21
+ "content-delivery"
22
+ ],
23
+ "type": "module",
24
+ "sideEffects": false,
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "bin": {
29
+ "contismo": "./dist/cli/main.js"
30
+ },
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "main": "./dist/index.cjs",
35
+ "module": "./dist/index.js",
36
+ "types": "./dist/index.d.ts",
37
+ "exports": {
38
+ ".": {
39
+ "import": {
40
+ "types": "./dist/index.d.ts",
41
+ "default": "./dist/index.js"
42
+ },
43
+ "require": {
44
+ "types": "./dist/index.d.cts",
45
+ "default": "./dist/index.cjs"
46
+ }
47
+ },
48
+ "./config": {
49
+ "import": {
50
+ "types": "./dist/config.d.ts",
51
+ "default": "./dist/config.js"
52
+ },
53
+ "require": {
54
+ "types": "./dist/config.d.cts",
55
+ "default": "./dist/config.cjs"
56
+ }
57
+ }
58
+ },
59
+ "publishConfig": {
60
+ "access": "public"
61
+ },
62
+ "scripts": {
63
+ "build": "tsup",
64
+ "dev": "tsup --watch",
65
+ "typecheck": "tsc --noEmit",
66
+ "lint": "eslint src tests",
67
+ "test": "vitest run",
68
+ "test:watch": "vitest"
69
+ },
70
+ "dependencies": {
71
+ "@graphql-codegen/cli": "^5.0.3",
72
+ "@graphql-codegen/typescript": "^4.1.2",
73
+ "@graphql-codegen/typescript-operations": "^4.4.0",
74
+ "@graphql-codegen/typed-document-node": "^5.0.4",
75
+ "jiti": "^2.4.2"
76
+ },
77
+ "devDependencies": {
78
+ "@eslint/js": "^9.16.0",
79
+ "@types/node": "^22.10.1",
80
+ "eslint": "^9.16.0",
81
+ "globals": "^15.13.0",
82
+ "tsup": "^8.3.5",
83
+ "typescript": "^5.7.2",
84
+ "typescript-eslint": "^8.18.0",
85
+ "vitest": "^2.1.8"
86
+ }
87
+ }
@@ -0,0 +1,13 @@
1
+ /** Read-only GraphQL API key prefix (`gql_`). */
2
+ export const READ_ONLY_API_KEY_PREFIX = "gql_" as const;
3
+
4
+ /** Read/write GraphQL API key prefix (`gqlw_`). */
5
+ export const READ_WRITE_API_KEY_PREFIX = "gqlw_" as const;
6
+
7
+ export function isReadOnlyApiKey(apiKey: string): boolean {
8
+ return apiKey.startsWith(READ_ONLY_API_KEY_PREFIX) && !apiKey.startsWith(READ_WRITE_API_KEY_PREFIX);
9
+ }
10
+
11
+ export function isReadWriteApiKey(apiKey: string): boolean {
12
+ return apiKey.startsWith(READ_WRITE_API_KEY_PREFIX);
13
+ }
@@ -0,0 +1,36 @@
1
+ import type { ContismoCodegenConfig } from "../config";
2
+
3
+ /** Default `@graphql-codegen/typescript` options for readable Contismo types. */
4
+ export const DEFAULT_TYPESCRIPT_CODEGEN_CONFIG = {
5
+ preResolveTypes: true,
6
+ skipTypename: true,
7
+ enumsAsTypes: true,
8
+ useTypeImports: true,
9
+ strictScalars: true,
10
+ defaultScalarType: "string",
11
+ maybeValue: "T | null",
12
+ scalars: {
13
+ ID: "string",
14
+ String: "string",
15
+ Boolean: "boolean",
16
+ Int: "number",
17
+ Float: "number",
18
+ DateTime: "string",
19
+ Date: "string",
20
+ JSON: "unknown",
21
+ Time: "string",
22
+ },
23
+ } as const satisfies Record<string, unknown>;
24
+
25
+ export function buildTypescriptCodegenConfig(
26
+ codegen?: ContismoCodegenConfig,
27
+ ): Record<string, unknown> {
28
+ return {
29
+ ...DEFAULT_TYPESCRIPT_CODEGEN_CONFIG,
30
+ ...codegen?.typescript,
31
+ scalars: {
32
+ ...DEFAULT_TYPESCRIPT_CODEGEN_CONFIG.scalars,
33
+ ...codegen?.typescript?.scalars,
34
+ },
35
+ };
36
+ }
@@ -0,0 +1,94 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import type { IntrospectionQueryResult, IntrospectionType, IntrospectionTypeRef } from "../introspection";
3
+ import type { ModelRegistry } from "../models";
4
+
5
+ function unwrapNamedType(ref: IntrospectionTypeRef): { name: string; isList: boolean } | null {
6
+ let current: IntrospectionTypeRef | null | undefined = ref;
7
+ let isList = false;
8
+
9
+ while (current) {
10
+ if (current.kind === "LIST") {
11
+ isList = true;
12
+ current = current.ofType;
13
+ } else if (current.kind === "NON_NULL") {
14
+ current = current.ofType;
15
+ } else if (current.name) {
16
+ return { name: current.name, isList };
17
+ } else {
18
+ break;
19
+ }
20
+ }
21
+
22
+ return null;
23
+ }
24
+
25
+ export function buildModelRegistry(introspection: IntrospectionQueryResult): ModelRegistry {
26
+ const queryType = introspection.__schema.types.find((t) => t.name === "Query");
27
+ if (!queryType?.fields) {
28
+ return {};
29
+ }
30
+
31
+ const byGraphqlType = new Map<
32
+ string,
33
+ { singular?: string; plural?: string; whereInput?: string }
34
+ >();
35
+
36
+ for (const field of queryType.fields) {
37
+ const unwrapped = unwrapNamedType(field.type);
38
+ if (!unwrapped?.name.startsWith("Entry_")) {
39
+ continue;
40
+ }
41
+
42
+ const graphqlType = unwrapped.name;
43
+ const existing = byGraphqlType.get(graphqlType) ?? {};
44
+
45
+ if (unwrapped.isList) {
46
+ existing.plural = field.name;
47
+ const whereArg = field.args?.find((arg) => arg.name === "where");
48
+ const whereType = whereArg ? unwrapNamedType(whereArg.type)?.name : undefined;
49
+ if (whereType) {
50
+ existing.whereInput = whereType;
51
+ }
52
+ } else {
53
+ existing.singular = field.name;
54
+ }
55
+
56
+ byGraphqlType.set(graphqlType, existing);
57
+ }
58
+
59
+ const registry: ModelRegistry = {};
60
+
61
+ for (const [graphqlType, fields] of byGraphqlType) {
62
+ const singular = fields.singular ?? fields.plural;
63
+ const plural = fields.plural ?? fields.singular;
64
+ if (!singular || !plural) {
65
+ continue;
66
+ }
67
+
68
+ const isSingleton = fields.plural === undefined;
69
+
70
+ registry[graphqlType.slice("Entry_".length)] = {
71
+ apiId: graphqlType.slice("Entry_".length),
72
+ singular,
73
+ plural,
74
+ graphqlType,
75
+ ...(isSingleton ? { isSingleton: true } : {}),
76
+ ...(fields.whereInput ? { whereInput: fields.whereInput } : {}),
77
+ };
78
+ }
79
+
80
+ return registry;
81
+ }
82
+
83
+ export async function writeModelsModule(registry: ModelRegistry, filePath: string): Promise<void> {
84
+ const content = `/* eslint-disable */
85
+ /** Generated by \`contismo generate\` — do not edit manually. */
86
+ import type { ModelRegistry } from "@contismo/sdk";
87
+
88
+ export const contismoModels = ${JSON.stringify(registry, null, 2)} as const satisfies ModelRegistry;
89
+
90
+ export type ContismoModelName = keyof typeof contismoModels;
91
+ `;
92
+
93
+ await writeFile(filePath, content, "utf8");
94
+ }