diff --git a/pgpm/core/package.json b/pgpm/core/package.json index 2fb22a24e..6d15e54aa 100644 --- a/pgpm/core/package.json +++ b/pgpm/core/package.json @@ -49,6 +49,7 @@ }, "dependencies": { "@pgpmjs/env": "workspace:^", + "@pgpmjs/migrate-client": "workspace:^", "@pgpmjs/logger": "workspace:^", "@pgpmjs/server-utils": "workspace:^", "@pgpmjs/types": "workspace:^", diff --git a/pgpm/core/src/export/export-graphql.ts b/pgpm/core/src/export/export-graphql.ts index 47958c1ca..7928522b0 100644 --- a/pgpm/core/src/export/export-graphql.ts +++ b/pgpm/core/src/export/export-graphql.ts @@ -13,6 +13,7 @@ import { sync as glob } from 'glob'; import path from 'path'; import { Inquirerer } from 'inquirerer'; +import { createClient as createMigrateClient } from '@pgpmjs/migrate-client'; import { PgpmPackage } from '../core/class/pgpm'; import { PgpmRow, SqlWriteOptions, writePgpmFiles, writePgpmPlan } from '../files'; @@ -309,16 +310,47 @@ export const exportGraphQL = async ({ if (migrateEndpoint) { console.log(`Fetching sql_actions from ${migrateEndpoint}...`); - const migrateClient = new GraphQLClient({ endpoint: migrateEndpoint, token }); + const migrateDb = createMigrateClient({ + endpoint: migrateEndpoint, + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }); try { - const rawRows = await migrateClient.fetchAllNodes( - 'sqlActions', - 'id\ndatabaseId\nname\ndeploy\nrevert\nverify\ncontent\ndeps\naction\nactionId\nactorId\npayload', - { databaseId } - ); + const allNodes: Record[] = []; + let hasNextPage = true; + let afterCursor: string | undefined; + + while (hasNextPage) { + const result = await migrateDb.sqlAction.findMany({ + select: { + id: true, + databaseId: true, + name: true, + deploy: true, + revert: true, + verify: true, + content: true, + deps: true, + action: true, + actionId: true, + actorId: true, + payload: true, + }, + where: { databaseId: { equalTo: databaseId } }, + first: 100, + ...(afterCursor ? { after: afterCursor } : {}), + }).unwrap(); + + allNodes.push( + ...result.sqlActions.nodes.map(node => + graphqlRowToPostgresRow(node as Record) + ) + ); + hasNextPage = result.sqlActions.pageInfo.hasNextPage; + afterCursor = result.sqlActions.pageInfo.endCursor ?? undefined; + } - sqlActionRows = rawRows.map(graphqlRowToPostgresRow); + sqlActionRows = allNodes; console.log(` Found ${sqlActionRows.length} sql_actions`); } catch (err) { console.warn(` Warning: Could not fetch sql_actions: ${err instanceof Error ? err.message : err}`); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e37f2085a..690fa11a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2043,6 +2043,9 @@ importers: '@pgpmjs/logger': specifier: workspace:^ version: link:../logger/dist + '@pgpmjs/migrate-client': + specifier: workspace:^ + version: link:../../sdk/migrate-client/dist '@pgpmjs/server-utils': specifier: workspace:^ version: link:../../packages/server-utils/dist @@ -2575,6 +2578,38 @@ importers: version: 5.9.3 publishDirectory: dist + sdk/migrate-client: + dependencies: + '@0no-co/graphql.web': + specifier: ^1.1.2 + version: 1.2.0(graphql@16.13.0) + '@constructive-io/graphql-types': + specifier: workspace:^ + version: link:../../graphql/types/dist + gql-ast: + specifier: workspace:^ + version: link:../../graphql/gql-ast/dist + graphql: + specifier: 16.13.0 + version: 16.13.0 + devDependencies: + '@constructive-io/graphql-codegen': + specifier: workspace:^ + version: link:../../graphql/codegen/dist + '@types/node': + specifier: ^22.19.11 + version: 22.19.11 + makage: + specifier: ^0.1.12 + version: 0.1.12 + tsx: + specifier: ^4.19.0 + version: 4.21.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + publishDirectory: dist + uploads/content-type-stream: dependencies: etag-hash: diff --git a/sdk/migrate-client/README.md b/sdk/migrate-client/README.md new file mode 100644 index 000000000..0ef71a486 --- /dev/null +++ b/sdk/migrate-client/README.md @@ -0,0 +1,32 @@ +# @pgpmjs/migrate-client + +Typed GraphQL ORM client for the Constructive Migrate API (`db_migrate` schema). + +Generated from `migrate.graphql` via `@constructive-io/graphql-codegen`. + +## Usage + +```typescript +import { createClient } from '@pgpmjs/migrate-client'; + +const db = createClient({ + endpoint: 'https://migrate.example.com/graphql', + headers: { Authorization: 'Bearer ' }, +}); + +// Fetch all sql_actions for a database +const result = await db.sqlAction.findMany({ + select: { id: true, name: true, deploy: true, revert: true, verify: true, content: true }, + where: { databaseId: { equalTo: '' } }, +}).unwrap(); + +console.log(result.sqlActions.nodes); +``` + +## Regeneration + +```bash +pnpm generate +``` + +This runs codegen against `schemas/migrate.graphql` and outputs to `src/`. diff --git a/sdk/migrate-client/package.json b/sdk/migrate-client/package.json new file mode 100644 index 000000000..bb1461639 --- /dev/null +++ b/sdk/migrate-client/package.json @@ -0,0 +1,54 @@ +{ + "name": "@pgpmjs/migrate-client", + "version": "0.0.1", + "author": "Constructive ", + "description": "Typed GraphQL ORM client for the Constructive Migrate API (db_migrate schema)", + "main": "index.js", + "module": "esm/index.js", + "types": "index.d.ts", + "homepage": "https://github.com/constructive-io/constructive", + "license": "MIT", + "publishConfig": { + "access": "public", + "directory": "dist" + }, + "repository": { + "type": "git", + "url": "https://github.com/constructive-io/constructive" + }, + "bugs": { + "url": "https://github.com/constructive-io/constructive/issues" + }, + "scripts": { + "clean": "makage clean", + "prepack": "npm run build", + "build": "makage build", + "build:dev": "makage build --dev", + "generate": "tsx scripts/generate.ts", + "lint": "eslint . --fix", + "test": "jest --passWithNoTests", + "test:watch": "jest --watch" + }, + "keywords": [ + "graphql", + "sdk", + "orm", + "constructive", + "migrate", + "db_migrate", + "pgpm" + ], + "dependencies": { + "@0no-co/graphql.web": "^1.1.2", + "@constructive-io/graphql-types": "workspace:^", + "gql-ast": "workspace:^", + "graphql": "^16.13.0" + }, + "devDependencies": { + "@constructive-io/graphql-codegen": "workspace:^", + "@types/node": "^22.19.11", + "makage": "^0.1.12", + "tsx": "^4.19.0", + "typescript": "^5.9.3" + } +} diff --git a/sdk/migrate-client/schemas/migrate.graphql b/sdk/migrate-client/schemas/migrate.graphql new file mode 100644 index 000000000..7c68b87c2 --- /dev/null +++ b/sdk/migrate-client/schemas/migrate.graphql @@ -0,0 +1,1281 @@ +"""The root query type which gives access points into the data universe.""" +type Query { + """Reads and enables pagination through a set of `MigrateFile`.""" + migrateFiles( + """Only read the first `n` values of the set.""" + first: Int + + """Only read the last `n` values of the set.""" + last: Int + + """ + Skip the first `n` values from our `after` cursor, an alternative to cursor + based pagination. May not be used with `last`. + """ + offset: Int + + """Read all values in the set before (above) this cursor.""" + before: Cursor + + """Read all values in the set after (below) this cursor.""" + after: Cursor + + """ + A filter to be used in determining which values should be returned by the collection. + """ + where: MigrateFileFilter + + """The method to use when ordering `MigrateFile`.""" + orderBy: [MigrateFileOrderBy!] = [PRIMARY_KEY_ASC] + ): MigrateFileConnection + + """Reads and enables pagination through a set of `SqlAction`.""" + sqlActions( + """Only read the first `n` values of the set.""" + first: Int + + """Only read the last `n` values of the set.""" + last: Int + + """ + Skip the first `n` values from our `after` cursor, an alternative to cursor + based pagination. May not be used with `last`. + """ + offset: Int + + """Read all values in the set before (above) this cursor.""" + before: Cursor + + """Read all values in the set after (below) this cursor.""" + after: Cursor + + """ + A filter to be used in determining which values should be returned by the collection. + """ + where: SqlActionFilter + + """The method to use when ordering `SqlAction`.""" + orderBy: [SqlActionOrderBy!] = [PRIMARY_KEY_ASC] + ): SqlActionConnection + + """ + Metadata about the database schema, including tables, fields, indexes, and constraints. Useful for code generation tools. + """ + _meta: MetaSchema +} + +"""A connection to a list of `MigrateFile` values.""" +type MigrateFileConnection { + """A list of `MigrateFile` objects.""" + nodes: [MigrateFile]! + + """ + A list of edges which contains the `MigrateFile` and cursor to aid in pagination. + """ + edges: [MigrateFileEdge]! + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """The count of *all* `MigrateFile` you could get from the connection.""" + totalCount: Int! +} + +type MigrateFile { + id: UUID! + databaseId: UUID + upload: ConstructiveInternalTypeUpload +} + +""" +A universally unique identifier as defined by [RFC 4122](https://tools.ietf.org/html/rfc4122). +""" +scalar UUID + +scalar ConstructiveInternalTypeUpload + +"""A `MigrateFile` edge in the connection.""" +type MigrateFileEdge { + """A cursor for use in pagination.""" + cursor: Cursor + + """The `MigrateFile` at the end of the edge.""" + node: MigrateFile +} + +"""A location in a connection that can be used for resuming pagination.""" +scalar Cursor + +"""Information about pagination in a connection.""" +type PageInfo { + """When paginating forwards, are there more items?""" + hasNextPage: Boolean! + + """When paginating backwards, are there more items?""" + hasPreviousPage: Boolean! + + """When paginating backwards, the cursor to continue.""" + startCursor: Cursor + + """When paginating forwards, the cursor to continue.""" + endCursor: Cursor +} + +""" +A filter to be used against `MigrateFile` object types. All fields are combined with a logical ‘and.’ +""" +input MigrateFileFilter { + """Filter by the object’s `id` field.""" + id: UUIDFilter + + """Filter by the object’s `databaseId` field.""" + databaseId: UUIDFilter + + """Filter by the object’s `upload` field.""" + upload: ConstructiveInternalTypeUploadFilter + + """Checks for all expressions in this list.""" + and: [MigrateFileFilter!] + + """Checks for any expressions in this list.""" + or: [MigrateFileFilter!] + + """Negates the expression.""" + not: MigrateFileFilter +} + +""" +A filter to be used against UUID fields. All fields are combined with a logical ‘and.’ +""" +input UUIDFilter { + """ + Is null (if `true` is specified) or is not null (if `false` is specified). + """ + isNull: Boolean + + """Equal to the specified value.""" + equalTo: UUID + + """Not equal to the specified value.""" + notEqualTo: UUID + + """ + Not equal to the specified value, treating null like an ordinary value. + """ + distinctFrom: UUID + + """Equal to the specified value, treating null like an ordinary value.""" + notDistinctFrom: UUID + + """Included in the specified list.""" + in: [UUID!] + + """Not included in the specified list.""" + notIn: [UUID!] + + """Less than the specified value.""" + lessThan: UUID + + """Less than or equal to the specified value.""" + lessThanOrEqualTo: UUID + + """Greater than the specified value.""" + greaterThan: UUID + + """Greater than or equal to the specified value.""" + greaterThanOrEqualTo: UUID +} + +""" +A filter to be used against ConstructiveInternalTypeUpload fields. All fields are combined with a logical ‘and.’ +""" +input ConstructiveInternalTypeUploadFilter { + """ + Is null (if `true` is specified) or is not null (if `false` is specified). + """ + isNull: Boolean + + """Equal to the specified value.""" + equalTo: ConstructiveInternalTypeUpload + + """Not equal to the specified value.""" + notEqualTo: ConstructiveInternalTypeUpload + + """ + Not equal to the specified value, treating null like an ordinary value. + """ + distinctFrom: ConstructiveInternalTypeUpload + + """Equal to the specified value, treating null like an ordinary value.""" + notDistinctFrom: ConstructiveInternalTypeUpload + + """Included in the specified list.""" + in: [ConstructiveInternalTypeUpload!] + + """Not included in the specified list.""" + notIn: [ConstructiveInternalTypeUpload!] + + """Less than the specified value.""" + lessThan: ConstructiveInternalTypeUpload + + """Less than or equal to the specified value.""" + lessThanOrEqualTo: ConstructiveInternalTypeUpload + + """Greater than the specified value.""" + greaterThan: ConstructiveInternalTypeUpload + + """Greater than or equal to the specified value.""" + greaterThanOrEqualTo: ConstructiveInternalTypeUpload + + """Contains the specified JSON.""" + contains: ConstructiveInternalTypeUpload + + """Contains the specified key.""" + containsKey: String + + """Contains all of the specified keys.""" + containsAllKeys: [String!] + + """Contains any of the specified keys.""" + containsAnyKeys: [String!] + + """Contained by the specified JSON.""" + containedBy: ConstructiveInternalTypeUpload +} + +"""Methods to use when ordering `MigrateFile`.""" +enum MigrateFileOrderBy { + NATURAL + PRIMARY_KEY_ASC + PRIMARY_KEY_DESC + ID_ASC + ID_DESC +} + +"""A connection to a list of `SqlAction` values.""" +type SqlActionConnection { + """A list of `SqlAction` objects.""" + nodes: [SqlAction]! + + """ + A list of edges which contains the `SqlAction` and cursor to aid in pagination. + """ + edges: [SqlActionEdge]! + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """The count of *all* `SqlAction` you could get from the connection.""" + totalCount: Int! +} + +type SqlAction { + id: Int! + name: String + databaseId: UUID + deploy: String + deps: [String] + payload: JSON + content: String + revert: String + verify: String + createdAt: Datetime + action: String + actionId: UUID + actorId: UUID + + """ + TRGM similarity when searching `name`. Returns null when no trgm search filter is active. + """ + nameTrgmSimilarity: Float + + """ + TRGM similarity when searching `deploy`. Returns null when no trgm search filter is active. + """ + deployTrgmSimilarity: Float + + """ + TRGM similarity when searching `content`. Returns null when no trgm search filter is active. + """ + contentTrgmSimilarity: Float + + """ + TRGM similarity when searching `revert`. Returns null when no trgm search filter is active. + """ + revertTrgmSimilarity: Float + + """ + TRGM similarity when searching `verify`. Returns null when no trgm search filter is active. + """ + verifyTrgmSimilarity: Float + + """ + TRGM similarity when searching `action`. Returns null when no trgm search filter is active. + """ + actionTrgmSimilarity: Float + + """ + Composite search relevance score (0..1, higher = more relevant). Computed by normalizing and averaging all active search signals. Returns null when no search filters are active. + """ + searchScore: Float +} + +""" +Represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). +""" +scalar JSON + +""" +A point in time as described by the [ISO +8601](https://en.wikipedia.org/wiki/ISO_8601) and, if it has a timezone, [RFC +3339](https://datatracker.ietf.org/doc/html/rfc3339) standards. Input values +that do not conform to both ISO 8601 and RFC 3339 may be coerced, which may lead +to unexpected results. +""" +scalar Datetime + +"""A `SqlAction` edge in the connection.""" +type SqlActionEdge { + """A cursor for use in pagination.""" + cursor: Cursor + + """The `SqlAction` at the end of the edge.""" + node: SqlAction +} + +""" +A filter to be used against `SqlAction` object types. All fields are combined with a logical ‘and.’ +""" +input SqlActionFilter { + """Filter by the object’s `id` field.""" + id: IntFilter + + """Filter by the object’s `name` field.""" + name: StringFilter + + """Filter by the object’s `databaseId` field.""" + databaseId: UUIDFilter + + """Filter by the object’s `deploy` field.""" + deploy: StringFilter + + """Filter by the object’s `deps` field.""" + deps: StringListFilter + + """Filter by the object’s `content` field.""" + content: StringFilter + + """Filter by the object’s `revert` field.""" + revert: StringFilter + + """Filter by the object’s `verify` field.""" + verify: StringFilter + + """Filter by the object’s `createdAt` field.""" + createdAt: DatetimeFilter + + """Filter by the object’s `action` field.""" + action: StringFilter + + """Filter by the object’s `actionId` field.""" + actionId: UUIDFilter + + """Filter by the object’s `actorId` field.""" + actorId: UUIDFilter + + """Checks for all expressions in this list.""" + and: [SqlActionFilter!] + + """Checks for any expressions in this list.""" + or: [SqlActionFilter!] + + """Negates the expression.""" + not: SqlActionFilter + + """TRGM search on the `name` column.""" + trgmName: TrgmSearchInput + + """TRGM search on the `deploy` column.""" + trgmDeploy: TrgmSearchInput + + """TRGM search on the `content` column.""" + trgmContent: TrgmSearchInput + + """TRGM search on the `revert` column.""" + trgmRevert: TrgmSearchInput + + """TRGM search on the `verify` column.""" + trgmVerify: TrgmSearchInput + + """TRGM search on the `action` column.""" + trgmAction: TrgmSearchInput + + """ + Composite full-text search. Provide a search string and it will be dispatched + to all text-compatible search algorithms (tsvector, BM25, pg_trgm) + simultaneously. Rows matching ANY algorithm are returned. All matching score + fields are populated. + """ + fullTextSearch: String +} + +""" +A filter to be used against Int fields. All fields are combined with a logical ‘and.’ +""" +input IntFilter { + """ + Is null (if `true` is specified) or is not null (if `false` is specified). + """ + isNull: Boolean + + """Equal to the specified value.""" + equalTo: Int + + """Not equal to the specified value.""" + notEqualTo: Int + + """ + Not equal to the specified value, treating null like an ordinary value. + """ + distinctFrom: Int + + """Equal to the specified value, treating null like an ordinary value.""" + notDistinctFrom: Int + + """Included in the specified list.""" + in: [Int!] + + """Not included in the specified list.""" + notIn: [Int!] + + """Less than the specified value.""" + lessThan: Int + + """Less than or equal to the specified value.""" + lessThanOrEqualTo: Int + + """Greater than the specified value.""" + greaterThan: Int + + """Greater than or equal to the specified value.""" + greaterThanOrEqualTo: Int +} + +""" +A filter to be used against String fields. All fields are combined with a logical ‘and.’ +""" +input StringFilter { + """ + Is null (if `true` is specified) or is not null (if `false` is specified). + """ + isNull: Boolean + + """Equal to the specified value.""" + equalTo: String + + """Not equal to the specified value.""" + notEqualTo: String + + """ + Not equal to the specified value, treating null like an ordinary value. + """ + distinctFrom: String + + """Equal to the specified value, treating null like an ordinary value.""" + notDistinctFrom: String + + """Included in the specified list.""" + in: [String!] + + """Not included in the specified list.""" + notIn: [String!] + + """Less than the specified value.""" + lessThan: String + + """Less than or equal to the specified value.""" + lessThanOrEqualTo: String + + """Greater than the specified value.""" + greaterThan: String + + """Greater than or equal to the specified value.""" + greaterThanOrEqualTo: String + + """Contains the specified string (case-sensitive).""" + includes: String + + """Does not contain the specified string (case-sensitive).""" + notIncludes: String + + """Contains the specified string (case-insensitive).""" + includesInsensitive: String + + """Does not contain the specified string (case-insensitive).""" + notIncludesInsensitive: String + + """Starts with the specified string (case-sensitive).""" + startsWith: String + + """Does not start with the specified string (case-sensitive).""" + notStartsWith: String + + """Starts with the specified string (case-insensitive).""" + startsWithInsensitive: String + + """Does not start with the specified string (case-insensitive).""" + notStartsWithInsensitive: String + + """Ends with the specified string (case-sensitive).""" + endsWith: String + + """Does not end with the specified string (case-sensitive).""" + notEndsWith: String + + """Ends with the specified string (case-insensitive).""" + endsWithInsensitive: String + + """Does not end with the specified string (case-insensitive).""" + notEndsWithInsensitive: String + + """ + Matches the specified pattern (case-sensitive). An underscore (_) matches any single character; a percent sign (%) matches any sequence of zero or more characters. + """ + like: String + + """ + Does not match the specified pattern (case-sensitive). An underscore (_) matches any single character; a percent sign (%) matches any sequence of zero or more characters. + """ + notLike: String + + """ + Matches the specified pattern (case-insensitive). An underscore (_) matches any single character; a percent sign (%) matches any sequence of zero or more characters. + """ + likeInsensitive: String + + """ + Does not match the specified pattern (case-insensitive). An underscore (_) matches any single character; a percent sign (%) matches any sequence of zero or more characters. + """ + notLikeInsensitive: String + + """Equal to the specified value (case-insensitive).""" + equalToInsensitive: String + + """Not equal to the specified value (case-insensitive).""" + notEqualToInsensitive: String + + """ + Not equal to the specified value, treating null like an ordinary value (case-insensitive). + """ + distinctFromInsensitive: String + + """ + Equal to the specified value, treating null like an ordinary value (case-insensitive). + """ + notDistinctFromInsensitive: String + + """Included in the specified list (case-insensitive).""" + inInsensitive: [String!] + + """Not included in the specified list (case-insensitive).""" + notInInsensitive: [String!] + + """Less than the specified value (case-insensitive).""" + lessThanInsensitive: String + + """Less than or equal to the specified value (case-insensitive).""" + lessThanOrEqualToInsensitive: String + + """Greater than the specified value (case-insensitive).""" + greaterThanInsensitive: String + + """Greater than or equal to the specified value (case-insensitive).""" + greaterThanOrEqualToInsensitive: String + + """ + Fuzzy matches using pg_trgm trigram similarity. Tolerates typos and misspellings. + """ + similarTo: TrgmSearchInput + + """ + Fuzzy matches using pg_trgm word_similarity. Finds the best matching substring within the column value. + """ + wordSimilarTo: TrgmSearchInput +} + +""" +Input for pg_trgm fuzzy text matching. Provide a search value and optional similarity threshold. +""" +input TrgmSearchInput { + """The text to fuzzy-match against. Typos and misspellings are tolerated.""" + value: String! + + """ + Minimum similarity threshold (0.0 to 1.0). Higher = stricter matching. Default is 0.3. + """ + threshold: Float +} + +""" +A filter to be used against String List fields. All fields are combined with a logical ‘and.’ +""" +input StringListFilter { + """ + Is null (if `true` is specified) or is not null (if `false` is specified). + """ + isNull: Boolean + + """Equal to the specified value.""" + equalTo: [String] + + """Not equal to the specified value.""" + notEqualTo: [String] + + """ + Not equal to the specified value, treating null like an ordinary value. + """ + distinctFrom: [String] + + """Equal to the specified value, treating null like an ordinary value.""" + notDistinctFrom: [String] + + """Less than the specified value.""" + lessThan: [String] + + """Less than or equal to the specified value.""" + lessThanOrEqualTo: [String] + + """Greater than the specified value.""" + greaterThan: [String] + + """Greater than or equal to the specified value.""" + greaterThanOrEqualTo: [String] + + """Contains the specified list of values.""" + contains: [String] + + """Contained by the specified list of values.""" + containedBy: [String] + + """Overlaps the specified list of values.""" + overlaps: [String] + + """Any array item is equal to the specified value.""" + anyEqualTo: String + + """Any array item is not equal to the specified value.""" + anyNotEqualTo: String + + """Any array item is less than the specified value.""" + anyLessThan: String + + """Any array item is less than or equal to the specified value.""" + anyLessThanOrEqualTo: String + + """Any array item is greater than the specified value.""" + anyGreaterThan: String + + """Any array item is greater than or equal to the specified value.""" + anyGreaterThanOrEqualTo: String +} + +""" +A filter to be used against Datetime fields. All fields are combined with a logical ‘and.’ +""" +input DatetimeFilter { + """ + Is null (if `true` is specified) or is not null (if `false` is specified). + """ + isNull: Boolean + + """Equal to the specified value.""" + equalTo: Datetime + + """Not equal to the specified value.""" + notEqualTo: Datetime + + """ + Not equal to the specified value, treating null like an ordinary value. + """ + distinctFrom: Datetime + + """Equal to the specified value, treating null like an ordinary value.""" + notDistinctFrom: Datetime + + """Included in the specified list.""" + in: [Datetime!] + + """Not included in the specified list.""" + notIn: [Datetime!] + + """Less than the specified value.""" + lessThan: Datetime + + """Less than or equal to the specified value.""" + lessThanOrEqualTo: Datetime + + """Greater than the specified value.""" + greaterThan: Datetime + + """Greater than or equal to the specified value.""" + greaterThanOrEqualTo: Datetime +} + +"""Methods to use when ordering `SqlAction`.""" +enum SqlActionOrderBy { + NATURAL + PRIMARY_KEY_ASC + PRIMARY_KEY_DESC + ID_ASC + ID_DESC + DATABASE_ID_ASC + DATABASE_ID_DESC + DEPLOY_ASC + DEPLOY_DESC + NAME_TRGM_SIMILARITY_ASC + NAME_TRGM_SIMILARITY_DESC + DEPLOY_TRGM_SIMILARITY_ASC + DEPLOY_TRGM_SIMILARITY_DESC + CONTENT_TRGM_SIMILARITY_ASC + CONTENT_TRGM_SIMILARITY_DESC + REVERT_TRGM_SIMILARITY_ASC + REVERT_TRGM_SIMILARITY_DESC + VERIFY_TRGM_SIMILARITY_ASC + VERIFY_TRGM_SIMILARITY_DESC + ACTION_TRGM_SIMILARITY_ASC + ACTION_TRGM_SIMILARITY_DESC + SEARCH_SCORE_ASC + SEARCH_SCORE_DESC +} + +"""Root meta schema type""" +type MetaSchema { + tables: [MetaTable!]! +} + +"""Information about a database table""" +type MetaTable { + name: String! + schemaName: String! + fields: [MetaField!]! + indexes: [MetaIndex!]! + constraints: MetaConstraints! + foreignKeyConstraints: [MetaForeignKeyConstraint!]! + primaryKeyConstraints: [MetaPrimaryKeyConstraint!]! + uniqueConstraints: [MetaUniqueConstraint!]! + relations: MetaRelations! + inflection: MetaInflection! + query: MetaQuery! +} + +"""Information about a table field/column""" +type MetaField { + name: String! + type: MetaType! + isNotNull: Boolean! + hasDefault: Boolean! +} + +"""Information about a PostgreSQL type""" +type MetaType { + pgType: String! + gqlType: String! + isArray: Boolean! + isNotNull: Boolean + hasDefault: Boolean +} + +"""Information about a database index""" +type MetaIndex { + name: String! + isUnique: Boolean! + isPrimary: Boolean! + columns: [String!]! + fields: [MetaField!] +} + +"""Table constraints""" +type MetaConstraints { + primaryKey: MetaPrimaryKeyConstraint + unique: [MetaUniqueConstraint!]! + foreignKey: [MetaForeignKeyConstraint!]! +} + +"""Information about a primary key constraint""" +type MetaPrimaryKeyConstraint { + name: String! + fields: [MetaField!]! +} + +"""Information about a unique constraint""" +type MetaUniqueConstraint { + name: String! + fields: [MetaField!]! +} + +"""Information about a foreign key constraint""" +type MetaForeignKeyConstraint { + name: String! + fields: [MetaField!]! + referencedTable: String! + referencedFields: [String!]! + refFields: [MetaField!] + refTable: MetaRefTable +} + +"""Reference to a related table""" +type MetaRefTable { + name: String! +} + +"""Table relations""" +type MetaRelations { + belongsTo: [MetaBelongsToRelation!]! + has: [MetaHasRelation!]! + hasOne: [MetaHasRelation!]! + hasMany: [MetaHasRelation!]! + manyToMany: [MetaManyToManyRelation!]! +} + +"""A belongs-to (forward FK) relation""" +type MetaBelongsToRelation { + fieldName: String + isUnique: Boolean! + type: String + keys: [MetaField!]! + references: MetaRefTable! +} + +"""A has-one or has-many (reverse FK) relation""" +type MetaHasRelation { + fieldName: String + isUnique: Boolean! + type: String + keys: [MetaField!]! + referencedBy: MetaRefTable! +} + +"""A many-to-many relation via junction table""" +type MetaManyToManyRelation { + fieldName: String + type: String + junctionTable: MetaRefTable! + junctionLeftConstraint: MetaForeignKeyConstraint! + junctionLeftKeyAttributes: [MetaField!]! + junctionRightConstraint: MetaForeignKeyConstraint! + junctionRightKeyAttributes: [MetaField!]! + leftKeyAttributes: [MetaField!]! + rightKeyAttributes: [MetaField!]! + rightTable: MetaRefTable! +} + +"""Table inflection names""" +type MetaInflection { + tableType: String! + allRows: String! + connection: String! + edge: String! + filterType: String + orderByType: String! + conditionType: String! + patchType: String + createInputType: String! + createPayloadType: String! + updatePayloadType: String + deletePayloadType: String! +} + +"""Table query/mutation names""" +type MetaQuery { + all: String! + one: String + create: String + update: String + delete: String +} + +""" +The root mutation type which contains root level fields which mutate data. +""" +type Mutation { + executeSql( + """ + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + """ + input: ExecuteSqlInput! + ): ExecuteSqlPayload + runMigration( + """ + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + """ + input: RunMigrationInput! + ): RunMigrationPayload + + """Creates a single `MigrateFile`.""" + createMigrateFile( + """ + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + """ + input: CreateMigrateFileInput! + ): CreateMigrateFilePayload + + """Creates a single `SqlAction`.""" + createSqlAction( + """ + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + """ + input: CreateSqlActionInput! + ): CreateSqlActionPayload + + """Updates a single `MigrateFile` using a unique key and a patch.""" + updateMigrateFile( + """ + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + """ + input: UpdateMigrateFileInput! + ): UpdateMigrateFilePayload + + """Updates a single `SqlAction` using a unique key and a patch.""" + updateSqlAction( + """ + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + """ + input: UpdateSqlActionInput! + ): UpdateSqlActionPayload + + """Deletes a single `MigrateFile` using a unique key.""" + deleteMigrateFile( + """ + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + """ + input: DeleteMigrateFileInput! + ): DeleteMigrateFilePayload + + """Deletes a single `SqlAction` using a unique key.""" + deleteSqlAction( + """ + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + """ + input: DeleteSqlActionInput! + ): DeleteSqlActionPayload +} + +"""The output of our `executeSql` mutation.""" +type ExecuteSqlPayload { + """ + The exact same `clientMutationId` that was provided in the mutation input, + unchanged and unused. May be used by a client to track mutations. + """ + clientMutationId: String + + """ + Our root query field type. Allows us to run any query from our mutation payload. + """ + query: Query +} + +"""All input for the `executeSql` mutation.""" +input ExecuteSqlInput { + """ + An arbitrary string value with no semantic meaning. Will be included in the + payload verbatim. May be used to track mutations by the client. + """ + clientMutationId: String + stmt: String +} + +"""The output of our `runMigration` mutation.""" +type RunMigrationPayload { + """ + The exact same `clientMutationId` that was provided in the mutation input, + unchanged and unused. May be used by a client to track mutations. + """ + clientMutationId: String + + """ + Our root query field type. Allows us to run any query from our mutation payload. + """ + query: Query +} + +"""All input for the `runMigration` mutation.""" +input RunMigrationInput { + """ + An arbitrary string value with no semantic meaning. Will be included in the + payload verbatim. May be used to track mutations by the client. + """ + clientMutationId: String + databaseId: UUID + migration: Int + kind: String +} + +"""The output of our create `MigrateFile` mutation.""" +type CreateMigrateFilePayload { + """ + The exact same `clientMutationId` that was provided in the mutation input, + unchanged and unused. May be used by a client to track mutations. + """ + clientMutationId: String + + """The `MigrateFile` that was created by this mutation.""" + migrateFile: MigrateFile + + """ + Our root query field type. Allows us to run any query from our mutation payload. + """ + query: Query + + """An edge for our `MigrateFile`. May be used by Relay 1.""" + migrateFileEdge( + """The method to use when ordering `MigrateFile`.""" + orderBy: [MigrateFileOrderBy!]! = [PRIMARY_KEY_ASC] + ): MigrateFileEdge +} + +"""All input for the create `MigrateFile` mutation.""" +input CreateMigrateFileInput { + """ + An arbitrary string value with no semantic meaning. Will be included in the + payload verbatim. May be used to track mutations by the client. + """ + clientMutationId: String + + """The `MigrateFile` to be created by this mutation.""" + migrateFile: MigrateFileInput! +} + +"""An input for mutations affecting `MigrateFile`""" +input MigrateFileInput { + id: UUID + databaseId: UUID + upload: ConstructiveInternalTypeUpload +} + +"""The output of our create `SqlAction` mutation.""" +type CreateSqlActionPayload { + """ + The exact same `clientMutationId` that was provided in the mutation input, + unchanged and unused. May be used by a client to track mutations. + """ + clientMutationId: String + + """The `SqlAction` that was created by this mutation.""" + sqlAction: SqlAction + + """ + Our root query field type. Allows us to run any query from our mutation payload. + """ + query: Query + + """An edge for our `SqlAction`. May be used by Relay 1.""" + sqlActionEdge( + """The method to use when ordering `SqlAction`.""" + orderBy: [SqlActionOrderBy!]! = [PRIMARY_KEY_ASC] + ): SqlActionEdge +} + +"""All input for the create `SqlAction` mutation.""" +input CreateSqlActionInput { + """ + An arbitrary string value with no semantic meaning. Will be included in the + payload verbatim. May be used to track mutations by the client. + """ + clientMutationId: String + + """The `SqlAction` to be created by this mutation.""" + sqlAction: SqlActionInput! +} + +"""An input for mutations affecting `SqlAction`""" +input SqlActionInput { + id: Int + name: String + databaseId: UUID + deploy: String + deps: [String] + payload: JSON + content: String + revert: String + verify: String + createdAt: Datetime + action: String + actionId: UUID + actorId: UUID +} + +"""The output of our update `MigrateFile` mutation.""" +type UpdateMigrateFilePayload { + """ + The exact same `clientMutationId` that was provided in the mutation input, + unchanged and unused. May be used by a client to track mutations. + """ + clientMutationId: String + + """The `MigrateFile` that was updated by this mutation.""" + migrateFile: MigrateFile + + """ + Our root query field type. Allows us to run any query from our mutation payload. + """ + query: Query + + """An edge for our `MigrateFile`. May be used by Relay 1.""" + migrateFileEdge( + """The method to use when ordering `MigrateFile`.""" + orderBy: [MigrateFileOrderBy!]! = [PRIMARY_KEY_ASC] + ): MigrateFileEdge +} + +"""All input for the `updateMigrateFile` mutation.""" +input UpdateMigrateFileInput { + """ + An arbitrary string value with no semantic meaning. Will be included in the + payload verbatim. May be used to track mutations by the client. + """ + clientMutationId: String + id: UUID! + + """ + An object where the defined keys will be set on the `MigrateFile` being updated. + """ + migrateFilePatch: MigrateFilePatch! +} + +""" +Represents an update to a `MigrateFile`. Fields that are set will be updated. +""" +input MigrateFilePatch { + id: UUID + databaseId: UUID + upload: ConstructiveInternalTypeUpload + + """File upload for the `upload` field.""" + uploadUpload: Upload +} + +"""The `Upload` scalar type represents a file upload.""" +scalar Upload + +"""The output of our update `SqlAction` mutation.""" +type UpdateSqlActionPayload { + """ + The exact same `clientMutationId` that was provided in the mutation input, + unchanged and unused. May be used by a client to track mutations. + """ + clientMutationId: String + + """The `SqlAction` that was updated by this mutation.""" + sqlAction: SqlAction + + """ + Our root query field type. Allows us to run any query from our mutation payload. + """ + query: Query + + """An edge for our `SqlAction`. May be used by Relay 1.""" + sqlActionEdge( + """The method to use when ordering `SqlAction`.""" + orderBy: [SqlActionOrderBy!]! = [PRIMARY_KEY_ASC] + ): SqlActionEdge +} + +"""All input for the `updateSqlAction` mutation.""" +input UpdateSqlActionInput { + """ + An arbitrary string value with no semantic meaning. Will be included in the + payload verbatim. May be used to track mutations by the client. + """ + clientMutationId: String + id: Int! + + """ + An object where the defined keys will be set on the `SqlAction` being updated. + """ + sqlActionPatch: SqlActionPatch! +} + +""" +Represents an update to a `SqlAction`. Fields that are set will be updated. +""" +input SqlActionPatch { + id: Int + name: String + databaseId: UUID + deploy: String + deps: [String] + payload: JSON + content: String + revert: String + verify: String + createdAt: Datetime + action: String + actionId: UUID + actorId: UUID +} + +"""The output of our delete `MigrateFile` mutation.""" +type DeleteMigrateFilePayload { + """ + The exact same `clientMutationId` that was provided in the mutation input, + unchanged and unused. May be used by a client to track mutations. + """ + clientMutationId: String + + """The `MigrateFile` that was deleted by this mutation.""" + migrateFile: MigrateFile + + """ + Our root query field type. Allows us to run any query from our mutation payload. + """ + query: Query + + """An edge for our `MigrateFile`. May be used by Relay 1.""" + migrateFileEdge( + """The method to use when ordering `MigrateFile`.""" + orderBy: [MigrateFileOrderBy!]! = [PRIMARY_KEY_ASC] + ): MigrateFileEdge +} + +"""All input for the `deleteMigrateFile` mutation.""" +input DeleteMigrateFileInput { + """ + An arbitrary string value with no semantic meaning. Will be included in the + payload verbatim. May be used to track mutations by the client. + """ + clientMutationId: String + id: UUID! +} + +"""The output of our delete `SqlAction` mutation.""" +type DeleteSqlActionPayload { + """ + The exact same `clientMutationId` that was provided in the mutation input, + unchanged and unused. May be used by a client to track mutations. + """ + clientMutationId: String + + """The `SqlAction` that was deleted by this mutation.""" + sqlAction: SqlAction + + """ + Our root query field type. Allows us to run any query from our mutation payload. + """ + query: Query + + """An edge for our `SqlAction`. May be used by Relay 1.""" + sqlActionEdge( + """The method to use when ordering `SqlAction`.""" + orderBy: [SqlActionOrderBy!]! = [PRIMARY_KEY_ASC] + ): SqlActionEdge +} + +"""All input for the `deleteSqlAction` mutation.""" +input DeleteSqlActionInput { + """ + An arbitrary string value with no semantic meaning. Will be included in the + payload verbatim. May be used to track mutations by the client. + """ + clientMutationId: String + id: Int! +} \ No newline at end of file diff --git a/sdk/migrate-client/scripts/generate.ts b/sdk/migrate-client/scripts/generate.ts new file mode 100644 index 000000000..a53a99358 --- /dev/null +++ b/sdk/migrate-client/scripts/generate.ts @@ -0,0 +1,64 @@ +import { + generateMulti, + expandSchemaDirToMultiTarget, +} from '@constructive-io/graphql-codegen'; + +const SCHEMA_DIR = './schemas'; + +async function main() { + console.log('Generating migrate-client ORM from schema files...'); + console.log(`Schema directory: ${SCHEMA_DIR}`); + + const baseConfig = { + schemaDir: SCHEMA_DIR, + output: './src', + orm: true, + reactQuery: false, + verbose: true, + docs: { + agents: false, + mcp: false, + skills: false, + } + }; + + const expanded = expandSchemaDirToMultiTarget(baseConfig); + if (!expanded) { + console.error('No .graphql files found in schema directory.'); + process.exit(1); + } + + console.log(`Found targets: ${Object.keys(expanded).join(', ')}`); + + const { results } = await generateMulti({ + configs: expanded, + }); + + let realError = false; + + for (const { name, result } of results) { + if (result.success) { + console.log(`[${name}] ${result.message}`); + if (result.tables?.length) { + console.log(` Tables: ${result.tables.join(', ')}`); + } + } else if (result.message?.includes('No tables found')) { + console.log(`[${name}] SKIP: no tables (empty schema)`); + } else { + console.error(`[${name}] ERROR: ${result.message}`); + realError = true; + } + } + + if (realError) { + console.error('\nGeneration failed'); + process.exit(1); + } + + console.log('\nMigrate-client ORM generation completed successfully!'); +} + +main().catch((err) => { + console.error('Fatal error:', err); + process.exit(1); +}); diff --git a/sdk/migrate-client/src/index.ts b/sdk/migrate-client/src/index.ts new file mode 100644 index 000000000..b9518c5ae --- /dev/null +++ b/sdk/migrate-client/src/index.ts @@ -0,0 +1 @@ +export * from './orm'; diff --git a/sdk/migrate-client/src/orm/README.md b/sdk/migrate-client/src/orm/README.md new file mode 100644 index 000000000..53a4e5510 --- /dev/null +++ b/sdk/migrate-client/src/orm/README.md @@ -0,0 +1,148 @@ +# ORM Client + +

+ +

+ + + +## Setup + +```typescript +import { createClient } from './orm'; + +const db = createClient({ + endpoint: 'https://api.example.com/graphql', + headers: { Authorization: 'Bearer ' }, +}); +``` + +## Models + +| Model | Operations | +|-------|------------| +| `migrateFile` | findMany, findOne, create, update, delete | +| `sqlAction` | findMany, findOne, create, update, delete | + +## Table Operations + +### `db.migrateFile` + +CRUD operations for MigrateFile records. + +**Fields:** + +| Field | Type | Editable | +|-------|------|----------| +| `id` | UUID | No | +| `databaseId` | UUID | Yes | +| `upload` | ConstructiveInternalTypeUpload | Yes | + +**Operations:** + +```typescript +// List all migrateFile records +const items = await db.migrateFile.findMany({ select: { id: true, databaseId: true, upload: true } }).execute(); + +// Get one by id +const item = await db.migrateFile.findOne({ id: '', select: { id: true, databaseId: true, upload: true } }).execute(); + +// Create +const created = await db.migrateFile.create({ data: { databaseId: '', upload: '' }, select: { id: true } }).execute(); + +// Update +const updated = await db.migrateFile.update({ where: { id: '' }, data: { databaseId: '' }, select: { id: true } }).execute(); + +// Delete +const deleted = await db.migrateFile.delete({ where: { id: '' } }).execute(); +``` + +### `db.sqlAction` + +CRUD operations for SqlAction records. + +**Fields:** + +| Field | Type | Editable | +|-------|------|----------| +| `id` | Int | No | +| `name` | String | Yes | +| `databaseId` | UUID | Yes | +| `deploy` | String | Yes | +| `deps` | String | Yes | +| `payload` | JSON | Yes | +| `content` | String | Yes | +| `revert` | String | Yes | +| `verify` | String | Yes | +| `createdAt` | Datetime | No | +| `action` | String | Yes | +| `actionId` | UUID | Yes | +| `actorId` | UUID | Yes | +| `nameTrgmSimilarity` | Float | Yes | +| `deployTrgmSimilarity` | Float | Yes | +| `contentTrgmSimilarity` | Float | Yes | +| `revertTrgmSimilarity` | Float | Yes | +| `verifyTrgmSimilarity` | Float | Yes | +| `actionTrgmSimilarity` | Float | Yes | +| `searchScore` | Float | Yes | + +**Operations:** + +```typescript +// List all sqlAction records +const items = await db.sqlAction.findMany({ select: { id: true, name: true, databaseId: true, deploy: true, deps: true, payload: true, content: true, revert: true, verify: true, createdAt: true, action: true, actionId: true, actorId: true, nameTrgmSimilarity: true, deployTrgmSimilarity: true, contentTrgmSimilarity: true, revertTrgmSimilarity: true, verifyTrgmSimilarity: true, actionTrgmSimilarity: true, searchScore: true } }).execute(); + +// Get one by id +const item = await db.sqlAction.findOne({ id: '', select: { id: true, name: true, databaseId: true, deploy: true, deps: true, payload: true, content: true, revert: true, verify: true, createdAt: true, action: true, actionId: true, actorId: true, nameTrgmSimilarity: true, deployTrgmSimilarity: true, contentTrgmSimilarity: true, revertTrgmSimilarity: true, verifyTrgmSimilarity: true, actionTrgmSimilarity: true, searchScore: true } }).execute(); + +// Create +const created = await db.sqlAction.create({ data: { name: '', databaseId: '', deploy: '', deps: '', payload: '', content: '', revert: '', verify: '', action: '', actionId: '', actorId: '', nameTrgmSimilarity: '', deployTrgmSimilarity: '', contentTrgmSimilarity: '', revertTrgmSimilarity: '', verifyTrgmSimilarity: '', actionTrgmSimilarity: '', searchScore: '' }, select: { id: true } }).execute(); + +// Update +const updated = await db.sqlAction.update({ where: { id: '' }, data: { name: '' }, select: { id: true } }).execute(); + +// Delete +const deleted = await db.sqlAction.delete({ where: { id: '' } }).execute(); +``` + +## Custom Operations + +### `db.mutation.executeSql` + +executeSql + +- **Type:** mutation +- **Arguments:** + + | Argument | Type | + |----------|------| + | `input` | ExecuteSqlInput (required) | + +```typescript +const result = await db.mutation.executeSql({ input: '' }).execute(); +``` + +### `db.mutation.runMigration` + +runMigration + +- **Type:** mutation +- **Arguments:** + + | Argument | Type | + |----------|------| + | `input` | RunMigrationInput (required) | + +```typescript +const result = await db.mutation.runMigration({ input: '' }).execute(); +``` + +--- + +Built by the [Constructive](https://constructive.io) team. + +## Disclaimer + +AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND. + +No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value. diff --git a/sdk/migrate-client/src/orm/client.ts b/sdk/migrate-client/src/orm/client.ts new file mode 100644 index 000000000..c0f12c466 --- /dev/null +++ b/sdk/migrate-client/src/orm/client.ts @@ -0,0 +1,137 @@ +/** + * ORM Client - Runtime GraphQL executor + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ +import type { GraphQLAdapter, GraphQLError, QueryResult } from '@constructive-io/graphql-types'; + +export type { GraphQLAdapter, GraphQLError, QueryResult } from '@constructive-io/graphql-types'; + +/** + * Default adapter that uses fetch for HTTP requests. + * This is used when no custom adapter is provided. + */ +export class FetchAdapter implements GraphQLAdapter { + private headers: Record; + + constructor( + private endpoint: string, + headers?: Record + ) { + this.headers = headers ?? {}; + } + + async execute(document: string, variables?: Record): Promise> { + const response = await fetch(this.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...this.headers, + }, + body: JSON.stringify({ + query: document, + variables: variables ?? {}, + }), + }); + + if (!response.ok) { + return { + ok: false, + data: null, + errors: [{ message: `HTTP ${response.status}: ${response.statusText}` }], + }; + } + + const json = (await response.json()) as { + data?: T; + errors?: GraphQLError[]; + }; + + if (json.errors && json.errors.length > 0) { + return { + ok: false, + data: null, + errors: json.errors, + }; + } + + return { + ok: true, + data: json.data as T, + errors: undefined, + }; + } + + setHeaders(headers: Record): void { + this.headers = { ...this.headers, ...headers }; + } + + getEndpoint(): string { + return this.endpoint; + } +} + +/** + * Configuration for creating an ORM client. + * Either provide endpoint (and optional headers) for HTTP requests, + * or provide a custom adapter for alternative execution strategies. + */ +export interface OrmClientConfig { + /** GraphQL endpoint URL (required if adapter not provided) */ + endpoint?: string; + /** Default headers for HTTP requests (only used with endpoint) */ + headers?: Record; + /** Custom adapter for GraphQL execution (overrides endpoint/headers) */ + adapter?: GraphQLAdapter; +} + +/** + * Error thrown when GraphQL request fails + */ +export class GraphQLRequestError extends Error { + constructor( + public readonly errors: GraphQLError[], + public readonly data: unknown = null + ) { + const messages = errors.map((e) => e.message).join('; '); + super(`GraphQL Error: ${messages}`); + this.name = 'GraphQLRequestError'; + } +} + +export class OrmClient { + private adapter: GraphQLAdapter; + + constructor(config: OrmClientConfig) { + if (config.adapter) { + this.adapter = config.adapter; + } else if (config.endpoint) { + this.adapter = new FetchAdapter(config.endpoint, config.headers); + } else { + throw new Error('OrmClientConfig requires either an endpoint or a custom adapter'); + } + } + + async execute(document: string, variables?: Record): Promise> { + return this.adapter.execute(document, variables); + } + + /** + * Set headers for requests. + * Only works if the adapter supports headers. + */ + setHeaders(headers: Record): void { + if (this.adapter.setHeaders) { + this.adapter.setHeaders(headers); + } + } + + /** + * Get the endpoint URL. + * Returns empty string if the adapter doesn't have an endpoint. + */ + getEndpoint(): string { + return this.adapter.getEndpoint?.() ?? ''; + } +} diff --git a/sdk/migrate-client/src/orm/index.ts b/sdk/migrate-client/src/orm/index.ts new file mode 100644 index 000000000..b3a9b2715 --- /dev/null +++ b/sdk/migrate-client/src/orm/index.ts @@ -0,0 +1,47 @@ +/** + * ORM Client - createClient factory + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ +import { OrmClient } from './client'; +import type { OrmClientConfig } from './client'; +import { MigrateFileModel } from './models/migrateFile'; +import { SqlActionModel } from './models/sqlAction'; +import { createMutationOperations } from './mutation'; +export type { OrmClientConfig, QueryResult, GraphQLError, GraphQLAdapter } from './client'; +export { GraphQLRequestError } from './client'; +export { QueryBuilder } from './query-builder'; +export * from './select-types'; +export * from './models'; +export { createMutationOperations } from './mutation'; +/** + * Create an ORM client instance + * + * @example + * ```typescript + * const db = createClient({ + * endpoint: 'https://api.example.com/graphql', + * headers: { Authorization: 'Bearer token' }, + * }); + * + * // Query users + * const users = await db.user.findMany({ + * select: { id: true, name: true }, + * first: 10, + * }).execute(); + * + * // Create a user + * const newUser = await db.user.create({ + * data: { name: 'John', email: 'john@example.com' }, + * select: { id: true }, + * }).execute(); + * ``` + */ +export function createClient(config: OrmClientConfig) { + const client = new OrmClient(config); + return { + migrateFile: new MigrateFileModel(client), + sqlAction: new SqlActionModel(client), + mutation: createMutationOperations(client), + }; +} diff --git a/sdk/migrate-client/src/orm/input-types.ts b/sdk/migrate-client/src/orm/input-types.ts new file mode 100644 index 000000000..ada837d16 --- /dev/null +++ b/sdk/migrate-client/src/orm/input-types.ts @@ -0,0 +1,610 @@ +/** + * GraphQL types for ORM client + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ +// ============ Scalar Filter Types ============ +export interface StringFilter { + isNull?: boolean; + equalTo?: string; + notEqualTo?: string; + distinctFrom?: string; + notDistinctFrom?: string; + in?: string[]; + notIn?: string[]; + lessThan?: string; + lessThanOrEqualTo?: string; + greaterThan?: string; + greaterThanOrEqualTo?: string; + includes?: string; + notIncludes?: string; + includesInsensitive?: string; + notIncludesInsensitive?: string; + startsWith?: string; + notStartsWith?: string; + startsWithInsensitive?: string; + notStartsWithInsensitive?: string; + endsWith?: string; + notEndsWith?: string; + endsWithInsensitive?: string; + notEndsWithInsensitive?: string; + like?: string; + notLike?: string; + likeInsensitive?: string; + notLikeInsensitive?: string; +} +export interface IntFilter { + isNull?: boolean; + equalTo?: number; + notEqualTo?: number; + distinctFrom?: number; + notDistinctFrom?: number; + in?: number[]; + notIn?: number[]; + lessThan?: number; + lessThanOrEqualTo?: number; + greaterThan?: number; + greaterThanOrEqualTo?: number; +} +export interface FloatFilter { + isNull?: boolean; + equalTo?: number; + notEqualTo?: number; + distinctFrom?: number; + notDistinctFrom?: number; + in?: number[]; + notIn?: number[]; + lessThan?: number; + lessThanOrEqualTo?: number; + greaterThan?: number; + greaterThanOrEqualTo?: number; +} +export interface BooleanFilter { + isNull?: boolean; + equalTo?: boolean; + notEqualTo?: boolean; +} +export interface UUIDFilter { + isNull?: boolean; + equalTo?: string; + notEqualTo?: string; + distinctFrom?: string; + notDistinctFrom?: string; + in?: string[]; + notIn?: string[]; +} +export interface DatetimeFilter { + isNull?: boolean; + equalTo?: string; + notEqualTo?: string; + distinctFrom?: string; + notDistinctFrom?: string; + in?: string[]; + notIn?: string[]; + lessThan?: string; + lessThanOrEqualTo?: string; + greaterThan?: string; + greaterThanOrEqualTo?: string; +} +export interface DateFilter { + isNull?: boolean; + equalTo?: string; + notEqualTo?: string; + distinctFrom?: string; + notDistinctFrom?: string; + in?: string[]; + notIn?: string[]; + lessThan?: string; + lessThanOrEqualTo?: string; + greaterThan?: string; + greaterThanOrEqualTo?: string; +} +export interface JSONFilter { + isNull?: boolean; + equalTo?: Record; + notEqualTo?: Record; + distinctFrom?: Record; + notDistinctFrom?: Record; + contains?: Record; + containedBy?: Record; + containsKey?: string; + containsAllKeys?: string[]; + containsAnyKeys?: string[]; +} +export interface BigIntFilter { + isNull?: boolean; + equalTo?: string; + notEqualTo?: string; + distinctFrom?: string; + notDistinctFrom?: string; + in?: string[]; + notIn?: string[]; + lessThan?: string; + lessThanOrEqualTo?: string; + greaterThan?: string; + greaterThanOrEqualTo?: string; +} +export interface BigFloatFilter { + isNull?: boolean; + equalTo?: string; + notEqualTo?: string; + distinctFrom?: string; + notDistinctFrom?: string; + in?: string[]; + notIn?: string[]; + lessThan?: string; + lessThanOrEqualTo?: string; + greaterThan?: string; + greaterThanOrEqualTo?: string; +} +export interface BitStringFilter { + isNull?: boolean; + equalTo?: string; + notEqualTo?: string; +} +export interface InternetAddressFilter { + isNull?: boolean; + equalTo?: string; + notEqualTo?: string; + distinctFrom?: string; + notDistinctFrom?: string; + in?: string[]; + notIn?: string[]; + lessThan?: string; + lessThanOrEqualTo?: string; + greaterThan?: string; + greaterThanOrEqualTo?: string; + contains?: string; + containsOrEqualTo?: string; + containedBy?: string; + containedByOrEqualTo?: string; + containsOrContainedBy?: string; +} +export interface FullTextFilter { + matches?: string; +} +export interface VectorFilter { + isNull?: boolean; + equalTo?: number[]; + notEqualTo?: number[]; + distinctFrom?: number[]; + notDistinctFrom?: number[]; +} +export interface StringListFilter { + isNull?: boolean; + equalTo?: string[]; + notEqualTo?: string[]; + distinctFrom?: string[]; + notDistinctFrom?: string[]; + lessThan?: string[]; + lessThanOrEqualTo?: string[]; + greaterThan?: string[]; + greaterThanOrEqualTo?: string[]; + contains?: string[]; + containedBy?: string[]; + overlaps?: string[]; + anyEqualTo?: string; + anyNotEqualTo?: string; + anyLessThan?: string; + anyLessThanOrEqualTo?: string; + anyGreaterThan?: string; + anyGreaterThanOrEqualTo?: string; +} +export interface IntListFilter { + isNull?: boolean; + equalTo?: number[]; + notEqualTo?: number[]; + distinctFrom?: number[]; + notDistinctFrom?: number[]; + lessThan?: number[]; + lessThanOrEqualTo?: number[]; + greaterThan?: number[]; + greaterThanOrEqualTo?: number[]; + contains?: number[]; + containedBy?: number[]; + overlaps?: number[]; + anyEqualTo?: number; + anyNotEqualTo?: number; + anyLessThan?: number; + anyLessThanOrEqualTo?: number; + anyGreaterThan?: number; + anyGreaterThanOrEqualTo?: number; +} +export interface UUIDListFilter { + isNull?: boolean; + equalTo?: string[]; + notEqualTo?: string[]; + distinctFrom?: string[]; + notDistinctFrom?: string[]; + lessThan?: string[]; + lessThanOrEqualTo?: string[]; + greaterThan?: string[]; + greaterThanOrEqualTo?: string[]; + contains?: string[]; + containedBy?: string[]; + overlaps?: string[]; + anyEqualTo?: string; + anyNotEqualTo?: string; + anyLessThan?: string; + anyLessThanOrEqualTo?: string; + anyGreaterThan?: string; + anyGreaterThanOrEqualTo?: string; +} +// ============ Custom Scalar Types ============ +export type ConstructiveInternalTypeUpload = unknown; +// ============ Entity Types ============ +export interface MigrateFile { + id: string; + databaseId?: string | null; + upload?: ConstructiveInternalTypeUpload | null; +} +export interface SqlAction { + id: number; + name?: string | null; + databaseId?: string | null; + deploy?: string | null; + deps?: string | null; + payload?: Record | null; + content?: string | null; + revert?: string | null; + verify?: string | null; + createdAt?: string | null; + action?: string | null; + actionId?: string | null; + actorId?: string | null; + /** TRGM similarity when searching `name`. Returns null when no trgm search filter is active. */ + nameTrgmSimilarity?: number | null; + /** TRGM similarity when searching `deploy`. Returns null when no trgm search filter is active. */ + deployTrgmSimilarity?: number | null; + /** TRGM similarity when searching `content`. Returns null when no trgm search filter is active. */ + contentTrgmSimilarity?: number | null; + /** TRGM similarity when searching `revert`. Returns null when no trgm search filter is active. */ + revertTrgmSimilarity?: number | null; + /** TRGM similarity when searching `verify`. Returns null when no trgm search filter is active. */ + verifyTrgmSimilarity?: number | null; + /** TRGM similarity when searching `action`. Returns null when no trgm search filter is active. */ + actionTrgmSimilarity?: number | null; + /** Composite search relevance score (0..1, higher = more relevant). Computed by normalizing and averaging all active search signals. Returns null when no search filters are active. */ + searchScore?: number | null; +} +// ============ Relation Helper Types ============ +export interface ConnectionResult { + nodes: T[]; + totalCount: number; + pageInfo: PageInfo; +} +export interface PageInfo { + hasNextPage: boolean; + hasPreviousPage: boolean; + startCursor?: string | null; + endCursor?: string | null; +} +// ============ Entity Relation Types ============ +export interface MigrateFileRelations {} +export interface SqlActionRelations {} +// ============ Entity Types With Relations ============ +export type MigrateFileWithRelations = MigrateFile & MigrateFileRelations; +export type SqlActionWithRelations = SqlAction & SqlActionRelations; +// ============ Entity Select Types ============ +export type MigrateFileSelect = { + id?: boolean; + databaseId?: boolean; + upload?: boolean; +}; +export type SqlActionSelect = { + id?: boolean; + name?: boolean; + databaseId?: boolean; + deploy?: boolean; + deps?: boolean; + payload?: boolean; + content?: boolean; + revert?: boolean; + verify?: boolean; + createdAt?: boolean; + action?: boolean; + actionId?: boolean; + actorId?: boolean; + nameTrgmSimilarity?: boolean; + deployTrgmSimilarity?: boolean; + contentTrgmSimilarity?: boolean; + revertTrgmSimilarity?: boolean; + verifyTrgmSimilarity?: boolean; + actionTrgmSimilarity?: boolean; + searchScore?: boolean; +}; +// ============ Table Filter Types ============ +export interface MigrateFileFilter { + id?: UUIDFilter; + databaseId?: UUIDFilter; + upload?: StringFilter; + and?: MigrateFileFilter[]; + or?: MigrateFileFilter[]; + not?: MigrateFileFilter; +} +export interface SqlActionFilter { + id?: IntFilter; + name?: StringFilter; + databaseId?: UUIDFilter; + deploy?: StringFilter; + deps?: StringFilter; + payload?: JSONFilter; + content?: StringFilter; + revert?: StringFilter; + verify?: StringFilter; + createdAt?: DatetimeFilter; + action?: StringFilter; + actionId?: UUIDFilter; + actorId?: UUIDFilter; + nameTrgmSimilarity?: FloatFilter; + deployTrgmSimilarity?: FloatFilter; + contentTrgmSimilarity?: FloatFilter; + revertTrgmSimilarity?: FloatFilter; + verifyTrgmSimilarity?: FloatFilter; + actionTrgmSimilarity?: FloatFilter; + searchScore?: FloatFilter; + and?: SqlActionFilter[]; + or?: SqlActionFilter[]; + not?: SqlActionFilter; +} +// ============ OrderBy Types ============ +export type MigrateFileOrderBy = + | 'PRIMARY_KEY_ASC' + | 'PRIMARY_KEY_DESC' + | 'NATURAL' + | 'ID_ASC' + | 'ID_DESC' + | 'DATABASE_ID_ASC' + | 'DATABASE_ID_DESC' + | 'UPLOAD_ASC' + | 'UPLOAD_DESC'; +export type SqlActionOrderBy = + | 'PRIMARY_KEY_ASC' + | 'PRIMARY_KEY_DESC' + | 'NATURAL' + | 'ID_ASC' + | 'ID_DESC' + | 'NAME_ASC' + | 'NAME_DESC' + | 'DATABASE_ID_ASC' + | 'DATABASE_ID_DESC' + | 'DEPLOY_ASC' + | 'DEPLOY_DESC' + | 'DEPS_ASC' + | 'DEPS_DESC' + | 'PAYLOAD_ASC' + | 'PAYLOAD_DESC' + | 'CONTENT_ASC' + | 'CONTENT_DESC' + | 'REVERT_ASC' + | 'REVERT_DESC' + | 'VERIFY_ASC' + | 'VERIFY_DESC' + | 'CREATED_AT_ASC' + | 'CREATED_AT_DESC' + | 'ACTION_ASC' + | 'ACTION_DESC' + | 'ACTION_ID_ASC' + | 'ACTION_ID_DESC' + | 'ACTOR_ID_ASC' + | 'ACTOR_ID_DESC' + | 'NAME_TRGM_SIMILARITY_ASC' + | 'NAME_TRGM_SIMILARITY_DESC' + | 'DEPLOY_TRGM_SIMILARITY_ASC' + | 'DEPLOY_TRGM_SIMILARITY_DESC' + | 'CONTENT_TRGM_SIMILARITY_ASC' + | 'CONTENT_TRGM_SIMILARITY_DESC' + | 'REVERT_TRGM_SIMILARITY_ASC' + | 'REVERT_TRGM_SIMILARITY_DESC' + | 'VERIFY_TRGM_SIMILARITY_ASC' + | 'VERIFY_TRGM_SIMILARITY_DESC' + | 'ACTION_TRGM_SIMILARITY_ASC' + | 'ACTION_TRGM_SIMILARITY_DESC' + | 'SEARCH_SCORE_ASC' + | 'SEARCH_SCORE_DESC'; +// ============ CRUD Input Types ============ +export interface CreateMigrateFileInput { + clientMutationId?: string; + migrateFile: { + databaseId?: string; + upload?: ConstructiveInternalTypeUpload; + }; +} +export interface MigrateFilePatch { + databaseId?: string | null; + upload?: ConstructiveInternalTypeUpload | null; +} +export interface UpdateMigrateFileInput { + clientMutationId?: string; + id: string; + migrateFilePatch: MigrateFilePatch; +} +export interface DeleteMigrateFileInput { + clientMutationId?: string; + id: string; +} +export interface CreateSqlActionInput { + clientMutationId?: string; + sqlAction: { + name?: string; + databaseId?: string; + deploy?: string; + deps?: string[]; + payload?: Record; + content?: string; + revert?: string; + verify?: string; + action?: string; + actionId?: string; + actorId?: string; + }; +} +export interface SqlActionPatch { + name?: string | null; + databaseId?: string | null; + deploy?: string | null; + deps?: string | null; + payload?: Record | null; + content?: string | null; + revert?: string | null; + verify?: string | null; + action?: string | null; + actionId?: string | null; + actorId?: string | null; + nameTrgmSimilarity?: number | null; + deployTrgmSimilarity?: number | null; + contentTrgmSimilarity?: number | null; + revertTrgmSimilarity?: number | null; + verifyTrgmSimilarity?: number | null; + actionTrgmSimilarity?: number | null; + searchScore?: number | null; +} +export interface UpdateSqlActionInput { + clientMutationId?: string; + id: number; + sqlActionPatch: SqlActionPatch; +} +export interface DeleteSqlActionInput { + clientMutationId?: string; + id: number; +} +// ============ Connection Fields Map ============ +export const connectionFieldsMap = {} as Record>; +// ============ Custom Input Types (from schema) ============ +export interface ExecuteSqlInput { + clientMutationId?: string; + stmt?: string; +} +export interface RunMigrationInput { + clientMutationId?: string; + databaseId?: string; + migration?: number; + kind?: string; +} +// ============ Payload/Return Types (for custom operations) ============ +export interface ExecuteSqlPayload { + clientMutationId?: string | null; +} +export type ExecuteSqlPayloadSelect = { + clientMutationId?: boolean; +}; +export interface RunMigrationPayload { + clientMutationId?: string | null; +} +export type RunMigrationPayloadSelect = { + clientMutationId?: boolean; +}; +export interface CreateMigrateFilePayload { + clientMutationId?: string | null; + /** The `MigrateFile` that was created by this mutation. */ + migrateFile?: MigrateFile | null; + migrateFileEdge?: MigrateFileEdge | null; +} +export type CreateMigrateFilePayloadSelect = { + clientMutationId?: boolean; + migrateFile?: { + select: MigrateFileSelect; + }; + migrateFileEdge?: { + select: MigrateFileEdgeSelect; + }; +}; +export interface UpdateMigrateFilePayload { + clientMutationId?: string | null; + /** The `MigrateFile` that was updated by this mutation. */ + migrateFile?: MigrateFile | null; + migrateFileEdge?: MigrateFileEdge | null; +} +export type UpdateMigrateFilePayloadSelect = { + clientMutationId?: boolean; + migrateFile?: { + select: MigrateFileSelect; + }; + migrateFileEdge?: { + select: MigrateFileEdgeSelect; + }; +}; +export interface DeleteMigrateFilePayload { + clientMutationId?: string | null; + /** The `MigrateFile` that was deleted by this mutation. */ + migrateFile?: MigrateFile | null; + migrateFileEdge?: MigrateFileEdge | null; +} +export type DeleteMigrateFilePayloadSelect = { + clientMutationId?: boolean; + migrateFile?: { + select: MigrateFileSelect; + }; + migrateFileEdge?: { + select: MigrateFileEdgeSelect; + }; +}; +export interface CreateSqlActionPayload { + clientMutationId?: string | null; + /** The `SqlAction` that was created by this mutation. */ + sqlAction?: SqlAction | null; + sqlActionEdge?: SqlActionEdge | null; +} +export type CreateSqlActionPayloadSelect = { + clientMutationId?: boolean; + sqlAction?: { + select: SqlActionSelect; + }; + sqlActionEdge?: { + select: SqlActionEdgeSelect; + }; +}; +export interface UpdateSqlActionPayload { + clientMutationId?: string | null; + /** The `SqlAction` that was updated by this mutation. */ + sqlAction?: SqlAction | null; + sqlActionEdge?: SqlActionEdge | null; +} +export type UpdateSqlActionPayloadSelect = { + clientMutationId?: boolean; + sqlAction?: { + select: SqlActionSelect; + }; + sqlActionEdge?: { + select: SqlActionEdgeSelect; + }; +}; +export interface DeleteSqlActionPayload { + clientMutationId?: string | null; + /** The `SqlAction` that was deleted by this mutation. */ + sqlAction?: SqlAction | null; + sqlActionEdge?: SqlActionEdge | null; +} +export type DeleteSqlActionPayloadSelect = { + clientMutationId?: boolean; + sqlAction?: { + select: SqlActionSelect; + }; + sqlActionEdge?: { + select: SqlActionEdgeSelect; + }; +}; +/** A `MigrateFile` edge in the connection. */ +export interface MigrateFileEdge { + cursor?: string | null; + /** The `MigrateFile` at the end of the edge. */ + node?: MigrateFile | null; +} +export type MigrateFileEdgeSelect = { + cursor?: boolean; + node?: { + select: MigrateFileSelect; + }; +}; +/** A `SqlAction` edge in the connection. */ +export interface SqlActionEdge { + cursor?: string | null; + /** The `SqlAction` at the end of the edge. */ + node?: SqlAction | null; +} +export type SqlActionEdgeSelect = { + cursor?: boolean; + node?: { + select: SqlActionSelect; + }; +}; diff --git a/sdk/migrate-client/src/orm/models/index.ts b/sdk/migrate-client/src/orm/models/index.ts new file mode 100644 index 000000000..55121fb26 --- /dev/null +++ b/sdk/migrate-client/src/orm/models/index.ts @@ -0,0 +1,7 @@ +/** + * Models barrel export + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ +export { MigrateFileModel } from './migrateFile'; +export { SqlActionModel } from './sqlAction'; diff --git a/sdk/migrate-client/src/orm/models/migrateFile.ts b/sdk/migrate-client/src/orm/models/migrateFile.ts new file mode 100644 index 000000000..4f80df33e --- /dev/null +++ b/sdk/migrate-client/src/orm/models/migrateFile.ts @@ -0,0 +1,236 @@ +/** + * MigrateFile model for ORM client + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ +import { OrmClient } from '../client'; +import { + QueryBuilder, + buildFindManyDocument, + buildFindFirstDocument, + buildFindOneDocument, + buildCreateDocument, + buildUpdateByPkDocument, + buildDeleteByPkDocument, +} from '../query-builder'; +import type { + ConnectionResult, + FindManyArgs, + FindFirstArgs, + CreateArgs, + UpdateArgs, + DeleteArgs, + InferSelectResult, + StrictSelect, +} from '../select-types'; +import type { + MigrateFile, + MigrateFileWithRelations, + MigrateFileSelect, + MigrateFileFilter, + MigrateFileOrderBy, + CreateMigrateFileInput, + UpdateMigrateFileInput, + MigrateFilePatch, +} from '../input-types'; +import { connectionFieldsMap } from '../input-types'; +export class MigrateFileModel { + constructor(private client: OrmClient) {} + findMany( + args: FindManyArgs & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + migrateFiles: ConnectionResult>; + }> { + const { document, variables } = buildFindManyDocument( + 'MigrateFile', + 'migrateFiles', + args.select, + { + where: args?.where, + orderBy: args?.orderBy as string[] | undefined, + first: args?.first, + last: args?.last, + after: args?.after, + before: args?.before, + offset: args?.offset, + }, + 'MigrateFileFilter', + 'MigrateFileOrderBy', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'query', + operationName: 'MigrateFile', + fieldName: 'migrateFiles', + document, + variables, + }); + } + findFirst( + args: FindFirstArgs & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + migrateFiles: { + nodes: InferSelectResult[]; + }; + }> { + const { document, variables } = buildFindFirstDocument( + 'MigrateFile', + 'migrateFiles', + args.select, + { + where: args?.where, + }, + 'MigrateFileFilter', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'query', + operationName: 'MigrateFile', + fieldName: 'migrateFiles', + document, + variables, + }); + } + findOne( + args: { + id: string; + select: S; + } & StrictSelect + ): QueryBuilder<{ + migrateFile: InferSelectResult | null; + }> { + const { document, variables } = buildFindManyDocument( + 'MigrateFile', + 'migrateFiles', + args.select, + { + where: { + id: { + equalTo: args.id, + }, + }, + first: 1, + }, + 'MigrateFileFilter', + 'MigrateFileOrderBy', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'query', + operationName: 'MigrateFile', + fieldName: 'migrateFile', + document, + variables, + transform: (data: { + migrateFiles?: { + nodes?: InferSelectResult[]; + }; + }) => ({ + migrateFile: data.migrateFiles?.nodes?.[0] ?? null, + }), + }); + } + create( + args: CreateArgs & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + createMigrateFile: { + migrateFile: InferSelectResult; + }; + }> { + const { document, variables } = buildCreateDocument( + 'MigrateFile', + 'createMigrateFile', + 'migrateFile', + args.select, + args.data, + 'CreateMigrateFileInput', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'mutation', + operationName: 'MigrateFile', + fieldName: 'createMigrateFile', + document, + variables, + }); + } + update( + args: UpdateArgs< + S, + { + id: string; + }, + MigrateFilePatch + > & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + updateMigrateFile: { + migrateFile: InferSelectResult; + }; + }> { + const { document, variables } = buildUpdateByPkDocument( + 'MigrateFile', + 'updateMigrateFile', + 'migrateFile', + args.select, + args.where.id, + args.data, + 'UpdateMigrateFileInput', + 'id', + 'migrateFilePatch', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'mutation', + operationName: 'MigrateFile', + fieldName: 'updateMigrateFile', + document, + variables, + }); + } + delete( + args: DeleteArgs< + { + id: string; + }, + S + > & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + deleteMigrateFile: { + migrateFile: InferSelectResult; + }; + }> { + const { document, variables } = buildDeleteByPkDocument( + 'MigrateFile', + 'deleteMigrateFile', + 'migrateFile', + args.where.id, + 'DeleteMigrateFileInput', + 'id', + args.select, + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'mutation', + operationName: 'MigrateFile', + fieldName: 'deleteMigrateFile', + document, + variables, + }); + } +} diff --git a/sdk/migrate-client/src/orm/models/sqlAction.ts b/sdk/migrate-client/src/orm/models/sqlAction.ts new file mode 100644 index 000000000..e5dae1a05 --- /dev/null +++ b/sdk/migrate-client/src/orm/models/sqlAction.ts @@ -0,0 +1,236 @@ +/** + * SqlAction model for ORM client + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ +import { OrmClient } from '../client'; +import { + QueryBuilder, + buildFindManyDocument, + buildFindFirstDocument, + buildFindOneDocument, + buildCreateDocument, + buildUpdateByPkDocument, + buildDeleteByPkDocument, +} from '../query-builder'; +import type { + ConnectionResult, + FindManyArgs, + FindFirstArgs, + CreateArgs, + UpdateArgs, + DeleteArgs, + InferSelectResult, + StrictSelect, +} from '../select-types'; +import type { + SqlAction, + SqlActionWithRelations, + SqlActionSelect, + SqlActionFilter, + SqlActionOrderBy, + CreateSqlActionInput, + UpdateSqlActionInput, + SqlActionPatch, +} from '../input-types'; +import { connectionFieldsMap } from '../input-types'; +export class SqlActionModel { + constructor(private client: OrmClient) {} + findMany( + args: FindManyArgs & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + sqlActions: ConnectionResult>; + }> { + const { document, variables } = buildFindManyDocument( + 'SqlAction', + 'sqlActions', + args.select, + { + where: args?.where, + orderBy: args?.orderBy as string[] | undefined, + first: args?.first, + last: args?.last, + after: args?.after, + before: args?.before, + offset: args?.offset, + }, + 'SqlActionFilter', + 'SqlActionOrderBy', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'query', + operationName: 'SqlAction', + fieldName: 'sqlActions', + document, + variables, + }); + } + findFirst( + args: FindFirstArgs & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + sqlActions: { + nodes: InferSelectResult[]; + }; + }> { + const { document, variables } = buildFindFirstDocument( + 'SqlAction', + 'sqlActions', + args.select, + { + where: args?.where, + }, + 'SqlActionFilter', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'query', + operationName: 'SqlAction', + fieldName: 'sqlActions', + document, + variables, + }); + } + findOne( + args: { + id: number; + select: S; + } & StrictSelect + ): QueryBuilder<{ + sqlAction: InferSelectResult | null; + }> { + const { document, variables } = buildFindManyDocument( + 'SqlAction', + 'sqlActions', + args.select, + { + where: { + id: { + equalTo: args.id, + }, + }, + first: 1, + }, + 'SqlActionFilter', + 'SqlActionOrderBy', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'query', + operationName: 'SqlAction', + fieldName: 'sqlAction', + document, + variables, + transform: (data: { + sqlActions?: { + nodes?: InferSelectResult[]; + }; + }) => ({ + sqlAction: data.sqlActions?.nodes?.[0] ?? null, + }), + }); + } + create( + args: CreateArgs & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + createSqlAction: { + sqlAction: InferSelectResult; + }; + }> { + const { document, variables } = buildCreateDocument( + 'SqlAction', + 'createSqlAction', + 'sqlAction', + args.select, + args.data, + 'CreateSqlActionInput', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'mutation', + operationName: 'SqlAction', + fieldName: 'createSqlAction', + document, + variables, + }); + } + update( + args: UpdateArgs< + S, + { + id: number; + }, + SqlActionPatch + > & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + updateSqlAction: { + sqlAction: InferSelectResult; + }; + }> { + const { document, variables } = buildUpdateByPkDocument( + 'SqlAction', + 'updateSqlAction', + 'sqlAction', + args.select, + args.where.id, + args.data, + 'UpdateSqlActionInput', + 'id', + 'sqlActionPatch', + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'mutation', + operationName: 'SqlAction', + fieldName: 'updateSqlAction', + document, + variables, + }); + } + delete( + args: DeleteArgs< + { + id: number; + }, + S + > & { + select: S; + } & StrictSelect + ): QueryBuilder<{ + deleteSqlAction: { + sqlAction: InferSelectResult; + }; + }> { + const { document, variables } = buildDeleteByPkDocument( + 'SqlAction', + 'deleteSqlAction', + 'sqlAction', + args.where.id, + 'DeleteSqlActionInput', + 'id', + args.select, + connectionFieldsMap + ); + return new QueryBuilder({ + client: this.client, + operation: 'mutation', + operationName: 'SqlAction', + fieldName: 'deleteSqlAction', + document, + variables, + }); + } +} diff --git a/sdk/migrate-client/src/orm/mutation/index.ts b/sdk/migrate-client/src/orm/mutation/index.ts new file mode 100644 index 000000000..8d141837f --- /dev/null +++ b/sdk/migrate-client/src/orm/mutation/index.ts @@ -0,0 +1,85 @@ +/** + * Custom mutation operations + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ +import { OrmClient } from '../client'; +import { QueryBuilder, buildCustomDocument } from '../query-builder'; +import type { InferSelectResult, StrictSelect } from '../select-types'; +import type { + ExecuteSqlInput, + RunMigrationInput, + ExecuteSqlPayload, + RunMigrationPayload, + ExecuteSqlPayloadSelect, + RunMigrationPayloadSelect, +} from '../input-types'; +import { connectionFieldsMap } from '../input-types'; +export interface ExecuteSqlVariables { + input: ExecuteSqlInput; +} +export interface RunMigrationVariables { + input: RunMigrationInput; +} +export function createMutationOperations(client: OrmClient) { + return { + executeSql: ( + args: ExecuteSqlVariables, + options: { + select: S; + } & StrictSelect + ) => + new QueryBuilder<{ + executeSql: InferSelectResult | null; + }>({ + client, + operation: 'mutation', + operationName: 'ExecuteSql', + fieldName: 'executeSql', + ...buildCustomDocument( + 'mutation', + 'ExecuteSql', + 'executeSql', + options.select, + args, + [ + { + name: 'input', + type: 'ExecuteSqlInput!', + }, + ], + connectionFieldsMap, + 'ExecuteSqlPayload' + ), + }), + runMigration: ( + args: RunMigrationVariables, + options: { + select: S; + } & StrictSelect + ) => + new QueryBuilder<{ + runMigration: InferSelectResult | null; + }>({ + client, + operation: 'mutation', + operationName: 'RunMigration', + fieldName: 'runMigration', + ...buildCustomDocument( + 'mutation', + 'RunMigration', + 'runMigration', + options.select, + args, + [ + { + name: 'input', + type: 'RunMigrationInput!', + }, + ], + connectionFieldsMap, + 'RunMigrationPayload' + ), + }), + }; +} diff --git a/sdk/migrate-client/src/orm/query-builder.ts b/sdk/migrate-client/src/orm/query-builder.ts new file mode 100644 index 000000000..969c14937 --- /dev/null +++ b/sdk/migrate-client/src/orm/query-builder.ts @@ -0,0 +1,868 @@ +/** + * Query Builder - Builds and executes GraphQL operations + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ +import { parseType, print } from '@0no-co/graphql.web'; +import * as t from 'gql-ast'; +import type { ArgumentNode, EnumValueNode, FieldNode, VariableDefinitionNode } from 'graphql'; + +import { GraphQLRequestError, OrmClient, QueryResult } from './client'; + +export interface QueryBuilderConfig { + client: OrmClient; + operation: 'query' | 'mutation'; + operationName: string; + fieldName: string; + document: string; + variables?: Record; + transform?: (data: any) => TResult; +} + +export class QueryBuilder { + private config: QueryBuilderConfig; + + constructor(config: QueryBuilderConfig) { + this.config = config; + } + + /** + * Execute the query and return a discriminated union result + * Use result.ok to check success, or .unwrap() to throw on error + */ + async execute(): Promise> { + const rawResult = await this.config.client.execute( + this.config.document, + this.config.variables + ); + if (!rawResult.ok) { + return rawResult; + } + if (!this.config.transform) { + return rawResult as unknown as QueryResult; + } + return { + ok: true, + data: this.config.transform(rawResult.data), + errors: undefined, + }; + } + + /** + * Execute and unwrap the result, throwing GraphQLRequestError on failure + * @throws {GraphQLRequestError} If the query returns errors + */ + async unwrap(): Promise { + const result = await this.execute(); + if (!result.ok) { + throw new GraphQLRequestError(result.errors, result.data); + } + return result.data; + } + + /** + * Execute and unwrap, returning defaultValue on error instead of throwing + */ + async unwrapOr(defaultValue: D): Promise { + const result = await this.execute(); + if (!result.ok) { + return defaultValue; + } + return result.data; + } + + /** + * Execute and unwrap, calling onError callback on failure + */ + async unwrapOrElse( + onError: (errors: import('./client').GraphQLError[]) => D + ): Promise { + const result = await this.execute(); + if (!result.ok) { + return onError(result.errors); + } + return result.data; + } + + toGraphQL(): string { + return this.config.document; + } + + getVariables(): Record | undefined { + return this.config.variables; + } +} + +const OP_QUERY = 'query' as unknown as import('graphql').OperationTypeNode; +const OP_MUTATION = 'mutation' as unknown as import('graphql').OperationTypeNode; +const ENUM_VALUE_KIND = 'EnumValue' as unknown as EnumValueNode['kind']; + +// ============================================================================ +// Selection Builders +// ============================================================================ + +export function buildSelections( + select: Record | undefined, + connectionFieldsMap?: Record>, + entityType?: string +): FieldNode[] { + if (!select) { + return []; + } + + const fields: FieldNode[] = []; + const entityConnections = entityType ? connectionFieldsMap?.[entityType] : undefined; + + for (const [key, value] of Object.entries(select)) { + if (value === false || value === undefined) { + continue; + } + + if (value === true) { + fields.push(t.field({ name: key })); + continue; + } + + if (typeof value === 'object' && value !== null) { + const nested = value as { + select?: Record; + first?: number; + filter?: Record; + orderBy?: string[]; + connection?: boolean; + }; + + if (!nested.select || typeof nested.select !== 'object') { + throw new Error( + `Invalid selection for field "${key}": nested selections must include a "select" object.` + ); + } + + const relatedEntityType = entityConnections?.[key]; + const nestedSelections = buildSelections( + nested.select, + connectionFieldsMap, + relatedEntityType + ); + const isConnection = + nested.connection === true || + nested.first !== undefined || + nested.filter !== undefined || + relatedEntityType !== undefined; + const args = buildArgs([ + buildOptionalArg('first', nested.first), + nested.filter + ? t.argument({ + name: 'filter', + value: buildValueAst(nested.filter), + }) + : null, + buildEnumListArg('orderBy', nested.orderBy), + ]); + + if (isConnection) { + fields.push( + t.field({ + name: key, + args, + selectionSet: t.selectionSet({ + selections: buildConnectionSelections(nestedSelections), + }), + }) + ); + } else { + fields.push( + t.field({ + name: key, + args, + selectionSet: t.selectionSet({ selections: nestedSelections }), + }) + ); + } + } + } + + return fields; +} + +// ============================================================================ +// Document Builders +// ============================================================================ + +export function buildFindManyDocument( + operationName: string, + queryField: string, + select: TSelect, + args: { + where?: TWhere; + condition?: TCondition; + orderBy?: string[]; + first?: number; + last?: number; + after?: string; + before?: string; + offset?: number; + }, + filterTypeName: string, + orderByTypeName: string, + connectionFieldsMap?: Record>, + conditionTypeName?: string +): { document: string; variables: Record } { + const selections = select + ? buildSelections(select as Record, connectionFieldsMap, operationName) + : [t.field({ name: 'id' })]; + + const variableDefinitions: VariableDefinitionNode[] = []; + const queryArgs: ArgumentNode[] = []; + const variables: Record = {}; + + addVariable( + { + varName: 'condition', + typeName: conditionTypeName, + value: args.condition, + }, + variableDefinitions, + queryArgs, + variables + ); + addVariable( + { + varName: 'where', + typeName: filterTypeName, + value: args.where, + }, + variableDefinitions, + queryArgs, + variables + ); + addVariable( + { + varName: 'orderBy', + typeName: '[' + orderByTypeName + '!]', + value: args.orderBy?.length ? args.orderBy : undefined, + }, + variableDefinitions, + queryArgs, + variables + ); + addVariable( + { varName: 'first', typeName: 'Int', value: args.first }, + variableDefinitions, + queryArgs, + variables + ); + addVariable( + { varName: 'last', typeName: 'Int', value: args.last }, + variableDefinitions, + queryArgs, + variables + ); + addVariable( + { varName: 'after', typeName: 'Cursor', value: args.after }, + variableDefinitions, + queryArgs, + variables + ); + addVariable( + { varName: 'before', typeName: 'Cursor', value: args.before }, + variableDefinitions, + queryArgs, + variables + ); + addVariable( + { varName: 'offset', typeName: 'Int', value: args.offset }, + variableDefinitions, + queryArgs, + variables + ); + + const document = t.document({ + definitions: [ + t.operationDefinition({ + operation: OP_QUERY, + name: operationName + 'Query', + variableDefinitions: variableDefinitions.length ? variableDefinitions : undefined, + selectionSet: t.selectionSet({ + selections: [ + t.field({ + name: queryField, + args: queryArgs.length ? queryArgs : undefined, + selectionSet: t.selectionSet({ + selections: buildConnectionSelections(selections), + }), + }), + ], + }), + }), + ], + }); + + return { document: print(document), variables }; +} + +export function buildFindFirstDocument( + operationName: string, + queryField: string, + select: TSelect, + args: { where?: TWhere; condition?: TCondition }, + filterTypeName: string, + connectionFieldsMap?: Record>, + conditionTypeName?: string +): { document: string; variables: Record } { + const selections = select + ? buildSelections(select as Record, connectionFieldsMap, operationName) + : [t.field({ name: 'id' })]; + + const variableDefinitions: VariableDefinitionNode[] = []; + const queryArgs: ArgumentNode[] = []; + const variables: Record = {}; + + // Always add first: 1 for findFirst + addVariable( + { varName: 'first', typeName: 'Int', value: 1 }, + variableDefinitions, + queryArgs, + variables + ); + addVariable( + { + varName: 'condition', + typeName: conditionTypeName, + value: args.condition, + }, + variableDefinitions, + queryArgs, + variables + ); + addVariable( + { + varName: 'where', + typeName: filterTypeName, + value: args.where, + }, + variableDefinitions, + queryArgs, + variables + ); + + const document = t.document({ + definitions: [ + t.operationDefinition({ + operation: OP_QUERY, + name: operationName + 'Query', + variableDefinitions, + selectionSet: t.selectionSet({ + selections: [ + t.field({ + name: queryField, + args: queryArgs, + selectionSet: t.selectionSet({ + selections: [ + t.field({ + name: 'nodes', + selectionSet: t.selectionSet({ selections }), + }), + ], + }), + }), + ], + }), + }), + ], + }); + + return { document: print(document), variables }; +} + +export function buildCreateDocument( + operationName: string, + mutationField: string, + entityField: string, + select: TSelect, + data: TData, + inputTypeName: string, + connectionFieldsMap?: Record> +): { document: string; variables: Record } { + const selections = select + ? buildSelections(select as Record, connectionFieldsMap, operationName) + : [t.field({ name: 'id' })]; + + return { + document: buildInputMutationDocument({ + operationName, + mutationField, + inputTypeName, + resultSelections: [ + t.field({ + name: entityField, + selectionSet: t.selectionSet({ selections }), + }), + ], + }), + variables: { + input: { + [entityField]: data, + }, + }, + }; +} + +export function buildUpdateDocument( + operationName: string, + mutationField: string, + entityField: string, + select: TSelect, + where: TWhere, + data: TData, + inputTypeName: string, + patchFieldName: string, + connectionFieldsMap?: Record> +): { document: string; variables: Record } { + const selections = select + ? buildSelections(select as Record, connectionFieldsMap, operationName) + : [t.field({ name: 'id' })]; + + return { + document: buildInputMutationDocument({ + operationName, + mutationField, + inputTypeName, + resultSelections: [ + t.field({ + name: entityField, + selectionSet: t.selectionSet({ selections }), + }), + ], + }), + variables: { + input: { + id: where.id, + [patchFieldName]: data, + }, + }, + }; +} + +export function buildUpdateByPkDocument( + operationName: string, + mutationField: string, + entityField: string, + select: TSelect, + id: string | number, + data: TData, + inputTypeName: string, + idFieldName: string, + patchFieldName: string, + connectionFieldsMap?: Record> +): { document: string; variables: Record } { + const selections = select + ? buildSelections(select as Record, connectionFieldsMap, operationName) + : [t.field({ name: 'id' })]; + + return { + document: buildInputMutationDocument({ + operationName, + mutationField, + inputTypeName, + resultSelections: [ + t.field({ + name: entityField, + selectionSet: t.selectionSet({ selections }), + }), + ], + }), + variables: { + input: { + [idFieldName]: id, + [patchFieldName]: data, + }, + }, + }; +} + +export function buildFindOneDocument( + operationName: string, + queryField: string, + id: string | number, + select: TSelect, + idArgName: string, + idTypeName: string, + connectionFieldsMap?: Record> +): { document: string; variables: Record } { + const selections = select + ? buildSelections(select as Record, connectionFieldsMap, operationName) + : [t.field({ name: 'id' })]; + + const variableDefinitions: VariableDefinitionNode[] = [ + t.variableDefinition({ + variable: t.variable({ name: idArgName }), + type: parseType(idTypeName), + }), + ]; + + const queryArgs: ArgumentNode[] = [ + t.argument({ + name: idArgName, + value: t.variable({ name: idArgName }), + }), + ]; + + const document = t.document({ + definitions: [ + t.operationDefinition({ + operation: OP_QUERY, + name: operationName + 'Query', + variableDefinitions, + selectionSet: t.selectionSet({ + selections: [ + t.field({ + name: queryField, + args: queryArgs, + selectionSet: t.selectionSet({ selections }), + }), + ], + }), + }), + ], + }); + + return { + document: print(document), + variables: { [idArgName]: id }, + }; +} + +export function buildDeleteDocument( + operationName: string, + mutationField: string, + entityField: string, + where: TWhere, + inputTypeName: string, + select?: TSelect, + connectionFieldsMap?: Record> +): { document: string; variables: Record } { + const entitySelections = select + ? buildSelections(select as Record, connectionFieldsMap, operationName) + : [t.field({ name: 'id' })]; + + return { + document: buildInputMutationDocument({ + operationName, + mutationField, + inputTypeName, + resultSelections: [ + t.field({ + name: entityField, + selectionSet: t.selectionSet({ + selections: entitySelections, + }), + }), + ], + }), + variables: { + input: { + id: where.id, + }, + }, + }; +} + +export function buildDeleteByPkDocument( + operationName: string, + mutationField: string, + entityField: string, + id: string | number, + inputTypeName: string, + idFieldName: string, + select?: TSelect, + connectionFieldsMap?: Record> +): { document: string; variables: Record } { + const entitySelections = select + ? buildSelections(select as Record, connectionFieldsMap, operationName) + : [t.field({ name: 'id' })]; + + return { + document: buildInputMutationDocument({ + operationName, + mutationField, + inputTypeName, + resultSelections: [ + t.field({ + name: entityField, + selectionSet: t.selectionSet({ selections: entitySelections }), + }), + ], + }), + variables: { + input: { + [idFieldName]: id, + }, + }, + }; +} + +export function buildCustomDocument( + operationType: 'query' | 'mutation', + operationName: string, + fieldName: string, + select: TSelect, + args: TArgs, + variableDefinitions: Array<{ name: string; type: string }>, + connectionFieldsMap?: Record>, + entityType?: string +): { document: string; variables: Record } { + let actualSelect: TSelect = select; + let isConnection = false; + + if (isCustomSelectionWrapper(select)) { + actualSelect = select.select as TSelect; + isConnection = select.connection === true; + } + + const selections = actualSelect + ? buildSelections(actualSelect as Record, connectionFieldsMap, entityType) + : []; + + const variableDefs = variableDefinitions.map((definition) => + t.variableDefinition({ + variable: t.variable({ name: definition.name }), + type: parseType(definition.type), + }) + ); + const fieldArgs = variableDefinitions.map((definition) => + t.argument({ + name: definition.name, + value: t.variable({ name: definition.name }), + }) + ); + + const fieldSelections = isConnection ? buildConnectionSelections(selections) : selections; + + const document = t.document({ + definitions: [ + t.operationDefinition({ + operation: operationType === 'mutation' ? OP_MUTATION : OP_QUERY, + name: operationName, + variableDefinitions: variableDefs.length ? variableDefs : undefined, + selectionSet: t.selectionSet({ + selections: [ + t.field({ + name: fieldName, + args: fieldArgs.length ? fieldArgs : undefined, + selectionSet: fieldSelections.length + ? t.selectionSet({ selections: fieldSelections }) + : undefined, + }), + ], + }), + }), + ], + }); + + return { + document: print(document), + variables: (args ?? {}) as Record, + }; +} + +function isCustomSelectionWrapper( + value: unknown +): value is { select: Record; connection?: boolean } { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return false; + } + + const record = value as Record; + const keys = Object.keys(record); + + if (!keys.includes('select') || !keys.includes('connection')) { + return false; + } + + if (keys.some((key) => key !== 'select' && key !== 'connection')) { + return false; + } + + return !!record.select && typeof record.select === 'object' && !Array.isArray(record.select); +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +function buildArgs(args: Array): ArgumentNode[] { + return args.filter((arg): arg is ArgumentNode => arg !== null); +} + +function buildOptionalArg(name: string, value: number | string | undefined): ArgumentNode | null { + if (value === undefined) { + return null; + } + const valueNode = + typeof value === 'number' ? t.intValue({ value: value.toString() }) : t.stringValue({ value }); + return t.argument({ name, value: valueNode }); +} + +function buildEnumListArg(name: string, values: string[] | undefined): ArgumentNode | null { + if (!values || values.length === 0) { + return null; + } + return t.argument({ + name, + value: t.listValue({ + values: values.map((value) => buildEnumValue(value)), + }), + }); +} + +function buildEnumValue(value: string): EnumValueNode { + return { + kind: ENUM_VALUE_KIND, + value, + }; +} + +function buildPageInfoSelections(): FieldNode[] { + return [ + t.field({ name: 'hasNextPage' }), + t.field({ name: 'hasPreviousPage' }), + t.field({ name: 'startCursor' }), + t.field({ name: 'endCursor' }), + ]; +} + +function buildConnectionSelections(nodeSelections: FieldNode[]): FieldNode[] { + return [ + t.field({ + name: 'nodes', + selectionSet: t.selectionSet({ selections: nodeSelections }), + }), + t.field({ name: 'totalCount' }), + t.field({ + name: 'pageInfo', + selectionSet: t.selectionSet({ selections: buildPageInfoSelections() }), + }), + ]; +} + +interface VariableSpec { + varName: string; + argName?: string; + typeName?: string; + value: unknown; +} + +interface InputMutationConfig { + operationName: string; + mutationField: string; + inputTypeName: string; + resultSelections: FieldNode[]; +} + +function buildInputMutationDocument(config: InputMutationConfig): string { + const document = t.document({ + definitions: [ + t.operationDefinition({ + operation: OP_MUTATION, + name: config.operationName + 'Mutation', + variableDefinitions: [ + t.variableDefinition({ + variable: t.variable({ name: 'input' }), + type: parseType(config.inputTypeName + '!'), + }), + ], + selectionSet: t.selectionSet({ + selections: [ + t.field({ + name: config.mutationField, + args: [ + t.argument({ + name: 'input', + value: t.variable({ name: 'input' }), + }), + ], + selectionSet: t.selectionSet({ + selections: config.resultSelections, + }), + }), + ], + }), + }), + ], + }); + return print(document); +} + +function addVariable( + spec: VariableSpec, + definitions: VariableDefinitionNode[], + args: ArgumentNode[], + variables: Record +): void { + if (spec.value === undefined || !spec.typeName) return; + + definitions.push( + t.variableDefinition({ + variable: t.variable({ name: spec.varName }), + type: parseType(spec.typeName), + }) + ); + args.push( + t.argument({ + name: spec.argName ?? spec.varName, + value: t.variable({ name: spec.varName }), + }) + ); + variables[spec.varName] = spec.value; +} + +function buildValueAst( + value: unknown +): + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | EnumValueNode { + if (value === null) { + return t.nullValue(); + } + + if (typeof value === 'boolean') { + return t.booleanValue({ value }); + } + + if (typeof value === 'number') { + return Number.isInteger(value) + ? t.intValue({ value: value.toString() }) + : t.floatValue({ value: value.toString() }); + } + + if (typeof value === 'string') { + return t.stringValue({ value }); + } + + if (Array.isArray(value)) { + return t.listValue({ + values: value.map((item) => buildValueAst(item)), + }); + } + + if (typeof value === 'object' && value !== null) { + const obj = value as Record; + return t.objectValue({ + fields: Object.entries(obj).map(([key, val]) => + t.objectField({ + name: key, + value: buildValueAst(val), + }) + ), + }); + } + + throw new Error('Unsupported value type: ' + typeof value); +} diff --git a/sdk/migrate-client/src/orm/select-types.ts b/sdk/migrate-client/src/orm/select-types.ts new file mode 100644 index 000000000..919d1b935 --- /dev/null +++ b/sdk/migrate-client/src/orm/select-types.ts @@ -0,0 +1,142 @@ +/** + * Type utilities for select inference + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ +export interface ConnectionResult { + nodes: T[]; + totalCount: number; + pageInfo: PageInfo; +} + +export interface PageInfo { + hasNextPage: boolean; + hasPreviousPage: boolean; + startCursor?: string | null; + endCursor?: string | null; +} + +export interface FindManyArgs { + select?: TSelect; + where?: TWhere; + condition?: TCondition; + orderBy?: TOrderBy[]; + first?: number; + last?: number; + after?: string; + before?: string; + offset?: number; +} + +export interface FindFirstArgs { + select?: TSelect; + where?: TWhere; + condition?: TCondition; +} + +export interface CreateArgs { + data: TData; + select?: TSelect; +} + +export interface UpdateArgs { + where: TWhere; + data: TData; + select?: TSelect; +} + +export type FindOneArgs = { + select?: TSelect; +} & Record; + +export interface DeleteArgs { + where: TWhere; + select?: TSelect; +} + +type DepthLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; +type DecrementDepth = { + 0: 0; + 1: 0; + 2: 1; + 3: 2; + 4: 3; + 5: 4; + 6: 5; + 7: 6; + 8: 7; + 9: 8; + 10: 9; +}; + +/** + * Recursively validates select objects, rejecting unknown keys. + * + * NOTE: Depth is intentionally capped to avoid circular-instantiation issues + * in very large cyclic schemas. + */ +export type DeepExact = Depth extends 0 + ? T extends Shape + ? T + : never + : T extends Shape + ? Exclude extends never + ? { + [K in keyof T]: K extends keyof Shape + ? T[K] extends { select: infer NS } + ? Extract extends { + select?: infer ShapeNS; + } + ? DeepExact< + Omit & { + select: DeepExact, DecrementDepth[Depth]>; + }, + Extract, + DecrementDepth[Depth] + > + : never + : T[K] + : never; + } + : never + : never; + +/** + * Enforces exact select shape while keeping contextual typing on `S extends XxxSelect`. + * Use this as an intersection in overloads: + * `{ select: S } & StrictSelect`. + */ +export type StrictSelect = S extends DeepExact ? {} : never; + +/** + * Hook-optimized strict select variant. + * + * Uses a shallower recursion depth to keep editor autocomplete responsive + * in large schemas while still validating common nested-select mistakes. + */ +export type HookStrictSelect = S extends DeepExact ? {} : never; + +/** + * Infer result type from select configuration + */ +export type InferSelectResult = TSelect extends undefined + ? TEntity + : { + [K in keyof TSelect as TSelect[K] extends false | undefined + ? never + : K]: TSelect[K] extends true + ? K extends keyof TEntity + ? TEntity[K] + : never + : TSelect[K] extends { select: infer NestedSelect } + ? K extends keyof TEntity + ? NonNullable extends ConnectionResult + ? ConnectionResult> + : + | InferSelectResult, NestedSelect> + | (null extends TEntity[K] ? null : never) + : never + : K extends keyof TEntity + ? TEntity[K] + : never; + }; diff --git a/sdk/migrate-client/src/orm/types.ts b/sdk/migrate-client/src/orm/types.ts new file mode 100644 index 000000000..7c1120bcd --- /dev/null +++ b/sdk/migrate-client/src/orm/types.ts @@ -0,0 +1,8 @@ +/** + * Types re-export + * @generated by @constructive-io/graphql-codegen + * DO NOT EDIT - changes will be overwritten + */ + +// Re-export all types from input-types +export * from './input-types'; diff --git a/sdk/migrate-client/tsconfig.esm.json b/sdk/migrate-client/tsconfig.esm.json new file mode 100644 index 000000000..b8766339b --- /dev/null +++ b/sdk/migrate-client/tsconfig.esm.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ES2020", + "outDir": "./dist/esm" + } +} diff --git a/sdk/migrate-client/tsconfig.json b/sdk/migrate-client/tsconfig.json new file mode 100644 index 000000000..159f873e8 --- /dev/null +++ b/sdk/migrate-client/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020", "DOM"], + "moduleResolution": "node", + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}