graphile-settings 4.1.1 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/esm/index.d.ts +1 -2
  2. package/esm/index.js +2 -4
  3. package/esm/plugins/index.d.ts +4 -13
  4. package/esm/plugins/index.js +5 -21
  5. package/esm/presets/constructive-preset.d.ts +4 -0
  6. package/esm/presets/constructive-preset.js +17 -9
  7. package/esm/upload-resolver.d.ts +43 -0
  8. package/esm/upload-resolver.js +168 -0
  9. package/index.d.ts +1 -2
  10. package/index.js +4 -7
  11. package/package.json +14 -6
  12. package/plugins/index.d.ts +4 -13
  13. package/plugins/index.js +32 -44
  14. package/presets/constructive-preset.d.ts +4 -0
  15. package/presets/constructive-preset.js +26 -18
  16. package/upload-resolver.d.ts +43 -0
  17. package/upload-resolver.js +175 -0
  18. package/esm/plugins/conflict-detector.d.ts +0 -7
  19. package/esm/plugins/conflict-detector.js +0 -67
  20. package/esm/plugins/custom-inflector.d.ts +0 -9
  21. package/esm/plugins/custom-inflector.js +0 -382
  22. package/esm/plugins/enable-all-filter-columns.d.ts +0 -60
  23. package/esm/plugins/enable-all-filter-columns.js +0 -85
  24. package/esm/plugins/inflector-logger.d.ts +0 -7
  25. package/esm/plugins/inflector-logger.js +0 -215
  26. package/esm/plugins/many-to-many-preset.d.ts +0 -62
  27. package/esm/plugins/many-to-many-preset.js +0 -86
  28. package/esm/plugins/meta-schema/cache.d.ts +0 -4
  29. package/esm/plugins/meta-schema/cache.js +0 -7
  30. package/esm/plugins/meta-schema/constraint-meta-builders.d.ts +0 -13
  31. package/esm/plugins/meta-schema/constraint-meta-builders.js +0 -51
  32. package/esm/plugins/meta-schema/graphql-meta-field.d.ts +0 -4
  33. package/esm/plugins/meta-schema/graphql-meta-field.js +0 -201
  34. package/esm/plugins/meta-schema/inflection-utils.d.ts +0 -4
  35. package/esm/plugins/meta-schema/inflection-utils.js +0 -20
  36. package/esm/plugins/meta-schema/name-meta-builders.d.ts +0 -4
  37. package/esm/plugins/meta-schema/name-meta-builders.js +0 -38
  38. package/esm/plugins/meta-schema/plugin.d.ts +0 -2
  39. package/esm/plugins/meta-schema/plugin.js +0 -23
  40. package/esm/plugins/meta-schema/relation-meta-builders.d.ts +0 -8
  41. package/esm/plugins/meta-schema/relation-meta-builders.js +0 -115
  42. package/esm/plugins/meta-schema/table-meta-builder.d.ts +0 -2
  43. package/esm/plugins/meta-schema/table-meta-builder.js +0 -69
  44. package/esm/plugins/meta-schema/table-meta-context.d.ts +0 -13
  45. package/esm/plugins/meta-schema/table-meta-context.js +0 -11
  46. package/esm/plugins/meta-schema/table-resource-utils.d.ts +0 -12
  47. package/esm/plugins/meta-schema/table-resource-utils.js +0 -50
  48. package/esm/plugins/meta-schema/type-mappings.d.ts +0 -3
  49. package/esm/plugins/meta-schema/type-mappings.js +0 -75
  50. package/esm/plugins/meta-schema/types.d.ts +0 -206
  51. package/esm/plugins/meta-schema/types.js +0 -1
  52. package/esm/plugins/meta-schema.d.ts +0 -19
  53. package/esm/plugins/meta-schema.js +0 -20
  54. package/esm/plugins/minimal-preset.d.ts +0 -7
  55. package/esm/plugins/minimal-preset.js +0 -42
  56. package/esm/plugins/pg-type-mappings.d.ts +0 -41
  57. package/esm/plugins/pg-type-mappings.js +0 -122
  58. package/esm/plugins/primary-key-only.d.ts +0 -96
  59. package/esm/plugins/primary-key-only.js +0 -143
  60. package/plugins/conflict-detector.d.ts +0 -7
  61. package/plugins/conflict-detector.js +0 -70
  62. package/plugins/custom-inflector.d.ts +0 -9
  63. package/plugins/custom-inflector.js +0 -385
  64. package/plugins/enable-all-filter-columns.d.ts +0 -60
  65. package/plugins/enable-all-filter-columns.js +0 -88
  66. package/plugins/inflector-logger.d.ts +0 -7
  67. package/plugins/inflector-logger.js +0 -218
  68. package/plugins/many-to-many-preset.d.ts +0 -62
  69. package/plugins/many-to-many-preset.js +0 -89
  70. package/plugins/meta-schema/cache.d.ts +0 -4
  71. package/plugins/meta-schema/cache.js +0 -12
  72. package/plugins/meta-schema/constraint-meta-builders.d.ts +0 -13
  73. package/plugins/meta-schema/constraint-meta-builders.js +0 -58
  74. package/plugins/meta-schema/graphql-meta-field.d.ts +0 -4
  75. package/plugins/meta-schema/graphql-meta-field.js +0 -204
  76. package/plugins/meta-schema/inflection-utils.d.ts +0 -4
  77. package/plugins/meta-schema/inflection-utils.js +0 -25
  78. package/plugins/meta-schema/name-meta-builders.d.ts +0 -4
  79. package/plugins/meta-schema/name-meta-builders.js +0 -43
  80. package/plugins/meta-schema/plugin.d.ts +0 -2
  81. package/plugins/meta-schema/plugin.js +0 -26
  82. package/plugins/meta-schema/relation-meta-builders.d.ts +0 -8
  83. package/plugins/meta-schema/relation-meta-builders.js +0 -120
  84. package/plugins/meta-schema/table-meta-builder.d.ts +0 -2
  85. package/plugins/meta-schema/table-meta-builder.js +0 -72
  86. package/plugins/meta-schema/table-meta-context.d.ts +0 -13
  87. package/plugins/meta-schema/table-meta-context.js +0 -15
  88. package/plugins/meta-schema/table-resource-utils.d.ts +0 -12
  89. package/plugins/meta-schema/table-resource-utils.js +0 -60
  90. package/plugins/meta-schema/type-mappings.d.ts +0 -3
  91. package/plugins/meta-schema/type-mappings.js +0 -79
  92. package/plugins/meta-schema/types.d.ts +0 -206
  93. package/plugins/meta-schema/types.js +0 -2
  94. package/plugins/meta-schema.d.ts +0 -19
  95. package/plugins/meta-schema.js +0 -20
  96. package/plugins/minimal-preset.d.ts +0 -7
  97. package/plugins/minimal-preset.js +0 -45
  98. package/plugins/pg-type-mappings.d.ts +0 -41
  99. package/plugins/pg-type-mappings.js +0 -128
  100. package/plugins/primary-key-only.d.ts +0 -96
  101. package/plugins/primary-key-only.js +0 -147
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Upload resolver for the Constructive upload plugin.
3
+ *
4
+ * Reads CDN/S3/MinIO configuration from environment variables (via getEnvOptions)
5
+ * and streams uploaded files to the configured storage backend.
6
+ *
7
+ * Lazily initializes the S3 streamer on first upload to avoid requiring
8
+ * env vars at module load time.
9
+ *
10
+ * ENV VARS:
11
+ * BUCKET_PROVIDER - 'minio' | 's3' (default: 'minio')
12
+ * BUCKET_NAME - bucket name (default: 'test-bucket')
13
+ * AWS_REGION - AWS region (default: 'us-east-1')
14
+ * AWS_ACCESS_KEY - access key (default: 'minioadmin')
15
+ * AWS_SECRET_KEY - secret key (default: 'minioadmin')
16
+ * MINIO_ENDPOINT - MinIO endpoint (default: 'http://localhost:9000')
17
+ */
18
+ import type { Readable } from 'stream';
19
+ import type { UploadFieldDefinition } from 'graphile-upload-plugin';
20
+ /**
21
+ * Streams a file to S3/MinIO storage and returns the URL and metadata.
22
+ *
23
+ * Reusable by both the GraphQL upload resolver and REST /upload endpoint.
24
+ */
25
+ export declare function streamToStorage(readStream: Readable, filename: string): Promise<{
26
+ url: string;
27
+ filename: string;
28
+ mime: string;
29
+ }>;
30
+ /**
31
+ * Upload field definitions for Constructive's three upload domain types.
32
+ *
33
+ * These match columns whose PostgreSQL type is one of the domains defined
34
+ * in constructive-db/pgpm-modules/types/:
35
+ *
36
+ * - `image` (public schema) — jsonb domain for images with versions
37
+ * - `upload` (public schema) — jsonb domain for generic file uploads
38
+ * - `attachment` (public schema) — text domain for simple URL attachments
39
+ *
40
+ * These domain types are part of the platform's core type system, deployed
41
+ * to every application database. They rarely change, so this config is stable.
42
+ */
43
+ export declare const constructiveUploadFieldDefinitions: UploadFieldDefinition[];
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ /**
3
+ * Upload resolver for the Constructive upload plugin.
4
+ *
5
+ * Reads CDN/S3/MinIO configuration from environment variables (via getEnvOptions)
6
+ * and streams uploaded files to the configured storage backend.
7
+ *
8
+ * Lazily initializes the S3 streamer on first upload to avoid requiring
9
+ * env vars at module load time.
10
+ *
11
+ * ENV VARS:
12
+ * BUCKET_PROVIDER - 'minio' | 's3' (default: 'minio')
13
+ * BUCKET_NAME - bucket name (default: 'test-bucket')
14
+ * AWS_REGION - AWS region (default: 'us-east-1')
15
+ * AWS_ACCESS_KEY - access key (default: 'minioadmin')
16
+ * AWS_SECRET_KEY - secret key (default: 'minioadmin')
17
+ * MINIO_ENDPOINT - MinIO endpoint (default: 'http://localhost:9000')
18
+ */
19
+ var __importDefault = (this && this.__importDefault) || function (mod) {
20
+ return (mod && mod.__esModule) ? mod : { "default": mod };
21
+ };
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.constructiveUploadFieldDefinitions = void 0;
24
+ exports.streamToStorage = streamToStorage;
25
+ const s3_streamer_1 = __importDefault(require("@constructive-io/s3-streamer"));
26
+ const upload_names_1 = __importDefault(require("@constructive-io/upload-names"));
27
+ const graphql_env_1 = require("@constructive-io/graphql-env");
28
+ const logger_1 = require("@pgpmjs/logger");
29
+ const crypto_1 = require("crypto");
30
+ const log = new logger_1.Logger('upload-resolver');
31
+ const DEFAULT_IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/svg+xml'];
32
+ let streamer = null;
33
+ let bucketName;
34
+ function getStreamer() {
35
+ if (streamer)
36
+ return streamer;
37
+ const opts = (0, graphql_env_1.getEnvOptions)();
38
+ const cdn = opts.cdn || {};
39
+ const provider = cdn.provider || 'minio';
40
+ bucketName = cdn.bucketName || 'test-bucket';
41
+ const awsRegion = cdn.awsRegion || 'us-east-1';
42
+ const awsAccessKey = cdn.awsAccessKey || 'minioadmin';
43
+ const awsSecretKey = cdn.awsSecretKey || 'minioadmin';
44
+ const minioEndpoint = cdn.minioEndpoint || 'http://localhost:9000';
45
+ if (process.env.NODE_ENV === 'production') {
46
+ if (!cdn.awsAccessKey || !cdn.awsSecretKey) {
47
+ log.warn('[upload-resolver] WARNING: Using default credentials in production.');
48
+ }
49
+ }
50
+ log.info(`[upload-resolver] Initializing: provider=${provider} bucket=${bucketName}`);
51
+ streamer = new s3_streamer_1.default({
52
+ defaultBucket: bucketName,
53
+ awsRegion,
54
+ awsSecretKey,
55
+ awsAccessKey,
56
+ minioEndpoint,
57
+ provider,
58
+ });
59
+ return streamer;
60
+ }
61
+ /**
62
+ * Generates a randomized storage key from a filename.
63
+ * Format: {random10chars}-{sanitized-filename}
64
+ */
65
+ function generateKey(filename) {
66
+ const rand = (0, crypto_1.randomBytes)(12).toString('hex');
67
+ return `${rand}-${(0, upload_names_1.default)(filename)}`;
68
+ }
69
+ /**
70
+ * Streams a file to S3/MinIO storage and returns the URL and metadata.
71
+ *
72
+ * Reusable by both the GraphQL upload resolver and REST /upload endpoint.
73
+ */
74
+ async function streamToStorage(readStream, filename) {
75
+ const s3 = getStreamer();
76
+ const key = generateKey(filename);
77
+ const result = await s3.upload({
78
+ readStream,
79
+ filename,
80
+ key,
81
+ bucket: bucketName,
82
+ });
83
+ return {
84
+ url: result.upload.Location,
85
+ filename,
86
+ mime: result.contentType,
87
+ };
88
+ }
89
+ /**
90
+ * Upload resolver that streams files to S3/MinIO.
91
+ *
92
+ * Returns different shapes based on the column's type hint:
93
+ * - 'image' / 'upload' → { filename, mime, url } (for jsonb domain columns)
94
+ * - 'attachment' / default → url string (for text domain columns)
95
+ *
96
+ * MIME validation happens before persistence: content type is detected from
97
+ * stream bytes, validated against smart-tag/type rules, and only then uploaded.
98
+ */
99
+ async function uploadResolver(upload, _args, _context, info) {
100
+ const { tags, type } = info.uploadPlugin;
101
+ const s3 = getStreamer();
102
+ const { filename } = upload;
103
+ const key = generateKey(filename);
104
+ // MIME type validation from smart tags
105
+ const typ = type || tags?.type;
106
+ const VALID_MIME = /^[a-z]+\/[a-z0-9][a-z0-9!#$&\-.^_+]*$/i;
107
+ const mim = tags?.mime
108
+ ? String(tags.mime)
109
+ .trim()
110
+ .split(',')
111
+ .map((a) => a.trim())
112
+ .filter((m) => VALID_MIME.test(m))
113
+ : typ === 'image'
114
+ ? DEFAULT_IMAGE_MIME_TYPES
115
+ : [];
116
+ const detected = await s3.detectContentType({
117
+ readStream: upload.createReadStream(),
118
+ filename,
119
+ });
120
+ const detectedContentType = detected.contentType;
121
+ if (mim.length && !mim.includes(detectedContentType)) {
122
+ detected.stream.destroy();
123
+ throw new Error('UPLOAD_MIMETYPE');
124
+ }
125
+ const result = await s3.uploadWithContentType({
126
+ readStream: detected.stream,
127
+ contentType: detectedContentType,
128
+ magic: detected.magic,
129
+ key,
130
+ bucket: bucketName,
131
+ });
132
+ const url = result.upload.Location;
133
+ const { contentType } = result;
134
+ switch (typ) {
135
+ case 'image':
136
+ case 'upload':
137
+ return { filename, mime: contentType, url };
138
+ case 'attachment':
139
+ default:
140
+ return url;
141
+ }
142
+ }
143
+ /**
144
+ * Upload field definitions for Constructive's three upload domain types.
145
+ *
146
+ * These match columns whose PostgreSQL type is one of the domains defined
147
+ * in constructive-db/pgpm-modules/types/:
148
+ *
149
+ * - `image` (public schema) — jsonb domain for images with versions
150
+ * - `upload` (public schema) — jsonb domain for generic file uploads
151
+ * - `attachment` (public schema) — text domain for simple URL attachments
152
+ *
153
+ * These domain types are part of the platform's core type system, deployed
154
+ * to every application database. They rarely change, so this config is stable.
155
+ */
156
+ exports.constructiveUploadFieldDefinitions = [
157
+ {
158
+ name: 'image',
159
+ namespaceName: 'public',
160
+ type: 'image',
161
+ resolve: uploadResolver,
162
+ },
163
+ {
164
+ name: 'upload',
165
+ namespaceName: 'public',
166
+ type: 'upload',
167
+ resolve: uploadResolver,
168
+ },
169
+ {
170
+ name: 'attachment',
171
+ namespaceName: 'public',
172
+ type: 'attachment',
173
+ resolve: uploadResolver,
174
+ },
175
+ ];
@@ -1,7 +0,0 @@
1
- import type { GraphileConfig } from 'graphile-config';
2
- export declare const ConflictDetectorPlugin: GraphileConfig.Plugin;
3
- /**
4
- * Preset that includes the conflict detector plugin.
5
- */
6
- export declare const ConflictDetectorPreset: GraphileConfig.Preset;
7
- export default ConflictDetectorPlugin;
@@ -1,67 +0,0 @@
1
- export const ConflictDetectorPlugin = {
2
- name: 'ConflictDetectorPlugin',
3
- version: '1.0.0',
4
- schema: {
5
- hooks: {
6
- build(build) {
7
- // Track codecs by their GraphQL name to detect conflicts
8
- const codecsByName = new Map();
9
- // Get configured schemas from pgServices to only check relevant codecs
10
- const configuredSchemas = new Set();
11
- const pgServices = build.resolvedPreset?.pgServices ?? [];
12
- for (const service of pgServices) {
13
- for (const schema of service.schemas ?? ['public']) {
14
- configuredSchemas.add(schema);
15
- }
16
- }
17
- // Iterate through all codecs to find tables
18
- for (const codec of Object.values(build.input.pgRegistry.pgCodecs)) {
19
- // Skip non-table codecs (those without attributes or anonymous ones)
20
- if (!codec.attributes || codec.isAnonymous)
21
- continue;
22
- // Get the schema name from the codec's extensions
23
- const pgExtensions = codec.extensions?.pg;
24
- const schemaName = pgExtensions?.schemaName || 'unknown';
25
- const tableName = codec.name;
26
- // Skip codecs from schemas not in the configured list
27
- if (configuredSchemas.size > 0 && !configuredSchemas.has(schemaName)) {
28
- continue;
29
- }
30
- // Get the GraphQL name that would be generated
31
- const graphqlName = build.inflection.tableType(codec);
32
- const info = {
33
- name: graphqlName,
34
- schemaName,
35
- tableName,
36
- };
37
- if (!codecsByName.has(graphqlName)) {
38
- codecsByName.set(graphqlName, []);
39
- }
40
- codecsByName.get(graphqlName).push(info);
41
- }
42
- // Check for conflicts and log warnings
43
- for (const [graphqlName, codecs] of codecsByName) {
44
- if (codecs.length > 1) {
45
- const locations = codecs
46
- .map((c) => `${c.schemaName}.${c.tableName}`)
47
- .join(', ');
48
- console.warn(`\nNAMING CONFLICT DETECTED: GraphQL type "${graphqlName}" would be generated from multiple tables:\n` +
49
- ` Tables: ${locations}\n` +
50
- ` Resolution options:\n` +
51
- ` 1. Add @name smart tag to one table: COMMENT ON TABLE schema.table IS E'@name UniqueTypeName';\n` +
52
- ` 2. Rename one of the tables in the database\n` +
53
- ` 3. Exclude one table from the schema using @omit smart tag\n`);
54
- }
55
- }
56
- return build;
57
- },
58
- },
59
- },
60
- };
61
- /**
62
- * Preset that includes the conflict detector plugin.
63
- */
64
- export const ConflictDetectorPreset = {
65
- plugins: [ConflictDetectorPlugin],
66
- };
67
- export default ConflictDetectorPlugin;
@@ -1,9 +0,0 @@
1
- import type { GraphileConfig } from 'graphile-config';
2
- export declare const InflektPlugin: GraphileConfig.Plugin;
3
- /**
4
- * Preset that includes the inflekt-based inflector plugin.
5
- * Use this in your main preset's `extends` array.
6
- */
7
- export declare const InflektPreset: GraphileConfig.Preset;
8
- export declare const CustomInflectorPlugin: GraphileConfig.Plugin;
9
- export declare const CustomInflectorPreset: GraphileConfig.Preset;
@@ -1,382 +0,0 @@
1
- import { singularize, singularizeLast, pluralizeLast, distinctPluralize, fixCapitalisedPlural, camelize, } from 'inflekt';
2
- /**
3
- * Custom inflector plugin for Constructive using the inflekt library.
4
- *
5
- * This plugin provides inflection rules based on the inflekt package from dev-utils.
6
- * It gives us full control over naming conventions and handles Latin plural suffixes
7
- * correctly (e.g., "schemata" -> "schema" instead of "schematum").
8
- *
9
- * Key features:
10
- * - Uses inflekt for pluralization/singularization with PostGraphile-compatible Latin handling
11
- * - Simplifies field names (allUsers -> users, postsByAuthorId -> posts)
12
- * - Customizable opposite name mappings for relations
13
- */
14
- /**
15
- * Custom opposite name mappings for relations.
16
- * For example, if you have a `parent_id` column, this determines
17
- * what the reverse relation should be called.
18
- *
19
- * Add your own mappings here as needed.
20
- */
21
- const CUSTOM_OPPOSITES = {
22
- parent: 'child',
23
- child: 'parent',
24
- author: 'authored',
25
- editor: 'edited',
26
- reviewer: 'reviewed',
27
- owner: 'owned',
28
- creator: 'created',
29
- updater: 'updated',
30
- };
31
- /**
32
- * Extract base name from attribute names like "author_id" -> "author"
33
- */
34
- function getBaseName(attributeName) {
35
- const matches = attributeName.match(/^(.+?)(_row_id|_id|_uuid|_fk|_pk|RowId|Id|Uuid|UUID|Fk|Pk)$/);
36
- if (matches) {
37
- return matches[1];
38
- }
39
- return null;
40
- }
41
- /**
42
- * Check if a base name matches another name (singularized)
43
- */
44
- function baseNameMatches(baseName, otherName) {
45
- const singularizedName = singularize(otherName);
46
- return camelize(baseName, true) === camelize(singularizedName, true);
47
- }
48
- /**
49
- * Get the opposite name for a relation base name
50
- */
51
- function getOppositeBaseName(baseName) {
52
- return CUSTOM_OPPOSITES[baseName] || null;
53
- }
54
- /**
55
- * Returns true if array1 and array2 have the same length and values
56
- */
57
- function arraysMatch(array1, array2, comparator = (v1, v2) => v1 === v2) {
58
- if (array1 === array2)
59
- return true;
60
- const l = array1.length;
61
- if (l !== array2.length)
62
- return false;
63
- for (let i = 0; i < l; i++) {
64
- if (!comparator(array1[i], array2[i]))
65
- return false;
66
- }
67
- return true;
68
- }
69
- export const InflektPlugin = {
70
- name: 'InflektPlugin',
71
- version: '1.0.0',
72
- inflection: {
73
- replace: {
74
- /**
75
- * Remove schema prefixes from all schemas.
76
- *
77
- * WHY THIS EXISTS:
78
- * PostGraphile v5's default `_schemaPrefix` inflector only removes the prefix
79
- * for the FIRST schema in the pgServices.schemas array. All other schemas get
80
- * prefixed with their schema name (e.g., "services_public_api" -> "servicesPublicApi").
81
- *
82
- * This is problematic for multi-schema setups where you want clean, consistent
83
- * naming across all schemas.
84
- *
85
- * SOURCE CODE REFERENCE:
86
- * https://github.com/graphile/crystal/blob/924b2515c6bd30e5905ac1419a25244b40c8bb4d/graphile-build/graphile-build-pg/src/plugins/PgTablesPlugin.ts#L261-L271
87
- *
88
- * The relevant v5 code:
89
- * ```typescript
90
- * _schemaPrefix(options, { pgNamespace, serviceName }) {
91
- * const pgService = options.pgServices?.find((db) => db.name === serviceName);
92
- * const databasePrefix = serviceName === "main" ? "" : `${serviceName}_`;
93
- * const schemaPrefix =
94
- * pgNamespace.nspname === pgService?.schemas?.[0] // <-- Only first schema!
95
- * ? ""
96
- * : `${pgNamespace.nspname}_`;
97
- * return `${databasePrefix}${schemaPrefix}`;
98
- * }
99
- * ```
100
- *
101
- * OUR FIX:
102
- * We override this to always return an empty string, giving clean names for
103
- * all schemas. Use the ConflictDetectorPlugin to detect naming conflicts.
104
- *
105
- * WARNING: This may cause naming conflicts if you have tables with the
106
- * same name in different schemas. Use @name smart tags to disambiguate.
107
- */
108
- _schemaPrefix(_previous, _options, _details) {
109
- return '';
110
- },
111
- /**
112
- * Keep `id` columns as `id` instead of renaming to `rowId`.
113
- *
114
- * WHY THIS EXISTS:
115
- * PostGraphile v5's default `_attributeName` inflector renames any column
116
- * named "id" to "row_id" to avoid conflicts with the Relay Global Object
117
- * Identification spec's `id` field. Since we don't use Relay/Node (we use
118
- * UUIDs), there's no conflict to avoid.
119
- *
120
- * NOTE: Disabling NodePlugin does NOT fix this! The renaming happens in
121
- * PgAttributesPlugin which is a core plugin we need for basic column
122
- * functionality.
123
- *
124
- * SOURCE CODE REFERENCE:
125
- * https://github.com/graphile/crystal/blob/924b2515c6bd30e5905ac1419a25244b40c8bb4d/graphile-build/graphile-build-pg/src/plugins/PgAttributesPlugin.ts#L289-L298
126
- *
127
- * The relevant v5 code:
128
- * ```typescript
129
- * _attributeName(options, { attributeName, codec, skipRowId }) {
130
- * const attribute = codec.attributes[attributeName];
131
- * const name = attribute.extensions?.tags?.name || attributeName;
132
- * // Avoid conflict with 'id' field used for Relay.
133
- * const nonconflictName =
134
- * !skipRowId && name.toLowerCase() === "id" && !codec.isAnonymous
135
- * ? "row_id" // <-- This renames id to row_id!
136
- * : name;
137
- * return this.coerceToGraphQLName(nonconflictName);
138
- * }
139
- * ```
140
- *
141
- * OUR FIX:
142
- * We override this to always use the original attribute name, never
143
- * renaming `id` to `row_id`. Since we use UUIDs and don't use Relay,
144
- * there's no naming conflict.
145
- */
146
- _attributeName(_previous, _options, details) {
147
- const attribute = details.codec.attributes[details.attributeName];
148
- const name = attribute?.extensions?.tags?.name || details.attributeName;
149
- return this.coerceToGraphQLName(name);
150
- },
151
- /**
152
- * Fix capitalized plurals (e.g., "Table1S" -> "Table1s")
153
- */
154
- camelCase(previous, _preset, str) {
155
- const original = previous(str);
156
- return fixCapitalisedPlural(original);
157
- },
158
- upperCamelCase(previous, _preset, str) {
159
- const original = previous(str);
160
- return fixCapitalisedPlural(original);
161
- },
162
- /**
163
- * Use inflekt's singularize/pluralize which only changes the last word
164
- */
165
- pluralize(_previous, _preset, str) {
166
- return pluralizeLast(str);
167
- },
168
- singularize(_previous, _preset, str) {
169
- return singularizeLast(str);
170
- },
171
- /**
172
- * Simplify root query connection fields (allUsers -> users)
173
- */
174
- allRowsConnection(_previous, _options, resource) {
175
- const resourceName = this._singularizedResourceName(resource);
176
- return camelize(distinctPluralize(resourceName), true);
177
- },
178
- /**
179
- * Simplify root query list fields
180
- */
181
- allRowsList(_previous, _options, resource) {
182
- const resourceName = this._singularizedResourceName(resource);
183
- return camelize(distinctPluralize(resourceName), true) + 'List';
184
- },
185
- /**
186
- * Simplify single relation field names (userByAuthorId -> author)
187
- */
188
- singleRelation(previous, _options, details) {
189
- const { registry, codec, relationName } = details;
190
- const relation = registry.pgRelations[codec.name]?.[relationName];
191
- if (typeof relation.extensions?.tags?.fieldName === 'string') {
192
- return relation.extensions.tags.fieldName;
193
- }
194
- // Try to extract base name from the local attribute
195
- if (relation.localAttributes.length === 1) {
196
- const attributeName = relation.localAttributes[0];
197
- const baseName = getBaseName(attributeName);
198
- if (baseName) {
199
- return camelize(baseName, true);
200
- }
201
- }
202
- // Fall back to the remote resource name
203
- const foreignPk = relation.remoteResource.uniques.find((u) => u.isPrimary);
204
- if (foreignPk &&
205
- arraysMatch(foreignPk.attributes, relation.remoteAttributes)) {
206
- return camelize(this._singularizedCodecName(relation.remoteResource.codec), true);
207
- }
208
- return previous(details);
209
- },
210
- /**
211
- * Simplify backwards single relation field names
212
- */
213
- singleRelationBackwards(previous, _options, details) {
214
- const { registry, codec, relationName } = details;
215
- const relation = registry.pgRelations[codec.name]?.[relationName];
216
- if (typeof relation.extensions?.tags?.foreignSingleFieldName === 'string') {
217
- return relation.extensions.tags.foreignSingleFieldName;
218
- }
219
- if (typeof relation.extensions?.tags?.foreignFieldName === 'string') {
220
- return relation.extensions.tags.foreignFieldName;
221
- }
222
- // Try to extract base name from the remote attribute
223
- if (relation.remoteAttributes.length === 1) {
224
- const attributeName = relation.remoteAttributes[0];
225
- const baseName = getBaseName(attributeName);
226
- if (baseName) {
227
- const oppositeBaseName = getOppositeBaseName(baseName);
228
- if (oppositeBaseName) {
229
- return camelize(`${oppositeBaseName}_${this._singularizedCodecName(relation.remoteResource.codec)}`, true);
230
- }
231
- if (baseNameMatches(baseName, codec.name)) {
232
- return camelize(this._singularizedCodecName(relation.remoteResource.codec), true);
233
- }
234
- }
235
- }
236
- return previous(details);
237
- },
238
- /**
239
- * Simplify many relation field names (postsByAuthorId -> posts)
240
- */
241
- _manyRelation(previous, _options, details) {
242
- const { registry, codec, relationName } = details;
243
- const relation = registry.pgRelations[codec.name]?.[relationName];
244
- const baseOverride = relation.extensions?.tags.foreignFieldName;
245
- if (typeof baseOverride === 'string') {
246
- return baseOverride;
247
- }
248
- // Try to extract base name from the remote attribute
249
- if (relation.remoteAttributes.length === 1) {
250
- const attributeName = relation.remoteAttributes[0];
251
- const baseName = getBaseName(attributeName);
252
- if (baseName) {
253
- const oppositeBaseName = getOppositeBaseName(baseName);
254
- if (oppositeBaseName) {
255
- return camelize(`${oppositeBaseName}_${distinctPluralize(this._singularizedCodecName(relation.remoteResource.codec))}`, true);
256
- }
257
- if (baseNameMatches(baseName, codec.name)) {
258
- return camelize(distinctPluralize(this._singularizedCodecName(relation.remoteResource.codec)), true);
259
- }
260
- }
261
- }
262
- // Fall back to pluralized remote resource name
263
- const pk = relation.remoteResource.uniques.find((u) => u.isPrimary);
264
- if (pk && arraysMatch(pk.attributes, relation.remoteAttributes)) {
265
- return camelize(distinctPluralize(this._singularizedCodecName(relation.remoteResource.codec)), true);
266
- }
267
- return previous(details);
268
- },
269
- /**
270
- * Simplify many-to-many relation field names with conflict detection.
271
- *
272
- * Default pg-many-to-many naming: tagsByPostTagPostIdAndTagId
273
- * Our simplified naming: tags
274
- *
275
- * Falls back to verbose naming if:
276
- * - Smart tag override exists (manyToManyFieldName)
277
- * - There's a direct relation to the same target table (would conflict)
278
- * - There are multiple many-to-many relations to the same target table
279
- */
280
- _manyToManyRelation(previous, _options, details) {
281
- const { leftTable, rightTable, junctionTable, rightRelationName } = details;
282
- const junctionRightRelation = junctionTable.getRelation(rightRelationName);
283
- const baseOverride = junctionRightRelation.extensions?.tags?.manyToManyFieldName;
284
- if (typeof baseOverride === 'string') {
285
- return baseOverride;
286
- }
287
- const simpleName = camelize(distinctPluralize(this._singularizedCodecName(rightTable.codec)), true);
288
- const leftRelations = leftTable.getRelations();
289
- let hasDirectRelation = false;
290
- let manyToManyCount = 0;
291
- for (const [_relName, rel] of Object.entries(leftRelations)) {
292
- if (rel.remoteResource?.codec?.name === rightTable.codec.name) {
293
- if (!rel.isReferencee) {
294
- hasDirectRelation = true;
295
- }
296
- }
297
- if (rel.isReferencee &&
298
- rel.remoteResource?.codec?.name !== rightTable.codec.name) {
299
- const junctionRelations = rel.remoteResource?.getRelations?.() || {};
300
- for (const [_jRelName, jRel] of Object.entries(junctionRelations)) {
301
- if (!jRel.isReferencee &&
302
- jRel.remoteResource?.codec?.name === rightTable.codec.name) {
303
- manyToManyCount++;
304
- }
305
- }
306
- }
307
- }
308
- if (hasDirectRelation || manyToManyCount > 1) {
309
- return previous(details);
310
- }
311
- return simpleName;
312
- },
313
- /**
314
- * Shorten primary key lookups (userById -> user)
315
- */
316
- rowByUnique(previous, _options, details) {
317
- const { unique, resource } = details;
318
- if (typeof unique.extensions?.tags?.fieldName === 'string') {
319
- return unique.extensions?.tags?.fieldName;
320
- }
321
- if (unique.isPrimary) {
322
- return camelize(this._singularizedCodecName(resource.codec), true);
323
- }
324
- return previous(details);
325
- },
326
- /**
327
- * Shorten update mutation names
328
- */
329
- updateByKeysField(previous, _options, details) {
330
- const { resource, unique } = details;
331
- if (typeof unique.extensions?.tags.updateFieldName === 'string') {
332
- return unique.extensions.tags.updateFieldName;
333
- }
334
- if (unique.isPrimary) {
335
- return camelize(`update_${this._singularizedCodecName(resource.codec)}`, true);
336
- }
337
- return previous(details);
338
- },
339
- /**
340
- * Shorten delete mutation names
341
- */
342
- deleteByKeysField(previous, _options, details) {
343
- const { resource, unique } = details;
344
- if (typeof unique.extensions?.tags.deleteFieldName === 'string') {
345
- return unique.extensions.tags.deleteFieldName;
346
- }
347
- if (unique.isPrimary) {
348
- return camelize(`delete_${this._singularizedCodecName(resource.codec)}`, true);
349
- }
350
- return previous(details);
351
- },
352
- /**
353
- * Uppercase enum values to match GraphQL CONSTANT_CASE convention.
354
- *
355
- * WHY THIS EXISTS:
356
- * In PostGraphile v4, custom PostgreSQL enum values (e.g., 'app', 'core', 'module')
357
- * were automatically uppercased to CONSTANT_CASE ('APP', 'CORE', 'MODULE').
358
- * In PostGraphile v5, the default `enumValue` inflector preserves the original
359
- * PostgreSQL casing via `coerceToGraphQLName(value)`, resulting in lowercase
360
- * enum values in the GraphQL schema.
361
- *
362
- * OUR FIX:
363
- * We call the previous inflector to retain all special character handling
364
- * (asterisks, symbols, etc.), then uppercase the result to restore v4 behavior.
365
- */
366
- enumValue(previous, _options, value, codec) {
367
- const result = previous(value, codec);
368
- return result.toUpperCase();
369
- },
370
- },
371
- },
372
- };
373
- /**
374
- * Preset that includes the inflekt-based inflector plugin.
375
- * Use this in your main preset's `extends` array.
376
- */
377
- export const InflektPreset = {
378
- plugins: [InflektPlugin],
379
- };
380
- // Re-export for backwards compatibility
381
- export const CustomInflectorPlugin = InflektPlugin;
382
- export const CustomInflectorPreset = InflektPreset;