graphile-settings 5.7.1 → 5.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/index.d.ts +0 -1
- package/esm/index.js +0 -2
- package/esm/plugins/meta-schema/graphql-meta-field.js +79 -1
- package/esm/plugins/meta-schema/storage-search-meta-builders.d.ts +29 -0
- package/esm/plugins/meta-schema/storage-search-meta-builders.js +180 -0
- package/esm/plugins/meta-schema/table-meta-builder.js +9 -0
- package/esm/plugins/meta-schema/type-mappings.js +14 -0
- package/esm/plugins/meta-schema/types.d.ts +65 -0
- package/esm/upload-resolver.d.ts +0 -11
- package/esm/upload-resolver.js +0 -20
- package/index.d.ts +0 -1
- package/index.js +1 -4
- package/package.json +4 -4
- package/plugins/meta-schema/graphql-meta-field.js +78 -0
- package/plugins/meta-schema/storage-search-meta-builders.d.ts +29 -0
- package/plugins/meta-schema/storage-search-meta-builders.js +186 -0
- package/plugins/meta-schema/table-meta-builder.js +9 -0
- package/plugins/meta-schema/type-mappings.js +14 -0
- package/plugins/meta-schema/types.d.ts +65 -0
- package/upload-resolver.d.ts +0 -11
- package/upload-resolver.js +0 -21
package/esm/index.d.ts
CHANGED
|
@@ -34,6 +34,5 @@ export type { ConstructivePresetOptions } from './presets/constructive-preset';
|
|
|
34
34
|
export * from './plugins/index';
|
|
35
35
|
export * from './presets/index';
|
|
36
36
|
export { makePgService };
|
|
37
|
-
export { streamToStorage } from './upload-resolver';
|
|
38
37
|
export { getPresignedUrlS3Config } from './presigned-url-resolver';
|
|
39
38
|
export { getBucketProvisionerConnection } from './bucket-provisioner-resolver';
|
package/esm/index.js
CHANGED
|
@@ -48,8 +48,6 @@ export * from './presets/index';
|
|
|
48
48
|
// ============================================================================
|
|
49
49
|
// Re-export makePgService for convenience
|
|
50
50
|
export { makePgService };
|
|
51
|
-
// Upload utilities
|
|
52
|
-
export { streamToStorage } from './upload-resolver';
|
|
53
51
|
// Presigned URL utilities
|
|
54
52
|
export { getPresignedUrlS3Config } from './presigned-url-resolver';
|
|
55
53
|
// Bucket provisioner utilities
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GraphQLBoolean, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString, } from 'graphql';
|
|
1
|
+
import { GraphQLBoolean, GraphQLFloat, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString, } from 'graphql';
|
|
2
2
|
function nn(type) {
|
|
3
3
|
return new GraphQLNonNull(type);
|
|
4
4
|
}
|
|
@@ -18,6 +18,14 @@ function createMetaSchemaType() {
|
|
|
18
18
|
subtype: { type: GraphQLString },
|
|
19
19
|
}),
|
|
20
20
|
});
|
|
21
|
+
const MetaEnumType = new GraphQLObjectType({
|
|
22
|
+
name: 'MetaEnum',
|
|
23
|
+
description: 'Information about a PostgreSQL enum type',
|
|
24
|
+
fields: () => ({
|
|
25
|
+
name: { type: nn(GraphQLString), description: 'The PostgreSQL enum type name' },
|
|
26
|
+
values: { type: nnList(GraphQLString), description: 'Allowed values for this enum' },
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
21
29
|
const MetaFieldType = new GraphQLObjectType({
|
|
22
30
|
name: 'MetaField',
|
|
23
31
|
description: 'Information about a table field/column',
|
|
@@ -29,6 +37,7 @@ function createMetaSchemaType() {
|
|
|
29
37
|
isPrimaryKey: { type: nn(GraphQLBoolean) },
|
|
30
38
|
isForeignKey: { type: nn(GraphQLBoolean) },
|
|
31
39
|
description: { type: GraphQLString },
|
|
40
|
+
enumValues: { type: MetaEnumType, description: 'Enum metadata if this field has an enum type' },
|
|
32
41
|
}),
|
|
33
42
|
});
|
|
34
43
|
const MetaIndexType = new GraphQLObjectType({
|
|
@@ -164,6 +173,71 @@ function createMetaSchemaType() {
|
|
|
164
173
|
manyToMany: { type: nnList(MetaManyToManyRelationType) },
|
|
165
174
|
}),
|
|
166
175
|
});
|
|
176
|
+
const MetaStorageType = new GraphQLObjectType({
|
|
177
|
+
name: 'MetaStorage',
|
|
178
|
+
description: 'Storage metadata for a table',
|
|
179
|
+
fields: () => ({
|
|
180
|
+
isFilesTable: { type: nn(GraphQLBoolean), description: 'Whether this table is a storage files table' },
|
|
181
|
+
isBucketsTable: { type: nn(GraphQLBoolean), description: 'Whether this table is a storage buckets table' },
|
|
182
|
+
}),
|
|
183
|
+
});
|
|
184
|
+
const MetaSearchConfigType = new GraphQLObjectType({
|
|
185
|
+
name: 'MetaSearchConfig',
|
|
186
|
+
description: 'Per-table search configuration from @searchConfig smart tag',
|
|
187
|
+
fields: () => ({
|
|
188
|
+
weights: {
|
|
189
|
+
type: GraphQLString,
|
|
190
|
+
description: 'JSON-encoded per-adapter score weights',
|
|
191
|
+
resolve(source) {
|
|
192
|
+
return source.weights ? JSON.stringify(source.weights) : null;
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
boostRecent: { type: nn(GraphQLBoolean), description: 'Whether recency boosting is enabled' },
|
|
196
|
+
boostRecencyField: { type: GraphQLString, description: 'Field used for recency decay' },
|
|
197
|
+
boostRecencyDecay: { type: GraphQLFloat, description: 'Exponential decay factor per day' },
|
|
198
|
+
}),
|
|
199
|
+
});
|
|
200
|
+
const MetaSearchColumnType = new GraphQLObjectType({
|
|
201
|
+
name: 'MetaSearchColumn',
|
|
202
|
+
description: 'A searchable column with its algorithm',
|
|
203
|
+
fields: () => ({
|
|
204
|
+
name: { type: nn(GraphQLString), description: 'Column name (camelCase)' },
|
|
205
|
+
algorithm: { type: nn(GraphQLString), description: 'Search algorithm: tsvector, bm25, trgm, or vector' },
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
const MetaSearchType = new GraphQLObjectType({
|
|
209
|
+
name: 'MetaSearch',
|
|
210
|
+
description: 'Search metadata for a table',
|
|
211
|
+
fields: () => ({
|
|
212
|
+
algorithms: { type: nnList(GraphQLString), description: 'Active search algorithms on this table' },
|
|
213
|
+
columns: { type: nnList(MetaSearchColumnType), description: 'Searchable columns with their algorithm' },
|
|
214
|
+
hasUnifiedSearch: { type: nn(GraphQLBoolean), description: 'Whether unifiedSearch composite filter is available' },
|
|
215
|
+
config: { type: MetaSearchConfigType, description: 'Per-table search configuration' },
|
|
216
|
+
}),
|
|
217
|
+
});
|
|
218
|
+
const MetaI18nFieldType = new GraphQLObjectType({
|
|
219
|
+
name: 'MetaI18nField',
|
|
220
|
+
description: 'A translatable field',
|
|
221
|
+
fields: () => ({
|
|
222
|
+
name: { type: nn(GraphQLString), description: 'GraphQL field name' },
|
|
223
|
+
type: { type: nn(GraphQLString), description: 'PostgreSQL column type (text, citext)' },
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
226
|
+
const MetaI18nType = new GraphQLObjectType({
|
|
227
|
+
name: 'MetaI18n',
|
|
228
|
+
description: 'i18n metadata for a table with @i18n tag',
|
|
229
|
+
fields: () => ({
|
|
230
|
+
translationTable: { type: nn(GraphQLString), description: 'Name of the translation table' },
|
|
231
|
+
translatableFields: { type: nnList(MetaI18nFieldType), description: 'Fields that are translatable' },
|
|
232
|
+
}),
|
|
233
|
+
});
|
|
234
|
+
const MetaRealtimeType = new GraphQLObjectType({
|
|
235
|
+
name: 'MetaRealtime',
|
|
236
|
+
description: 'Realtime metadata for a table with @realtime tag',
|
|
237
|
+
fields: () => ({
|
|
238
|
+
subscriptionFieldName: { type: nn(GraphQLString), description: 'The generated subscription field name (e.g. onPostChanged)' },
|
|
239
|
+
}),
|
|
240
|
+
});
|
|
167
241
|
const MetaTableType = new GraphQLObjectType({
|
|
168
242
|
name: 'MetaTable',
|
|
169
243
|
description: 'Information about a database table',
|
|
@@ -179,6 +253,10 @@ function createMetaSchemaType() {
|
|
|
179
253
|
relations: { type: nn(MetaRelationsType) },
|
|
180
254
|
inflection: { type: nn(MetaInflectionType) },
|
|
181
255
|
query: { type: nn(MetaQueryType) },
|
|
256
|
+
storage: { type: MetaStorageType, description: 'Storage metadata (null if not a storage table)' },
|
|
257
|
+
search: { type: MetaSearchType, description: 'Search metadata (null if no search configured)' },
|
|
258
|
+
i18n: { type: MetaI18nType, description: 'i18n metadata (null if no @i18n tag)' },
|
|
259
|
+
realtime: { type: MetaRealtimeType, description: 'Realtime metadata (null if no @realtime tag)' },
|
|
182
260
|
}),
|
|
183
261
|
});
|
|
184
262
|
return new GraphQLObjectType({
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { I18nMeta, PgCodec, RealtimeMeta, SearchMeta, StorageMeta } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Detect storage metadata from a codec's smart tags.
|
|
4
|
+
* Storage tables are identified by @storageFiles and @storageBuckets smart tags.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildStorageMeta(codec: PgCodec): StorageMeta | null;
|
|
7
|
+
/**
|
|
8
|
+
* Detect search metadata from a codec's columns and smart tags.
|
|
9
|
+
*
|
|
10
|
+
* Looks for:
|
|
11
|
+
* - tsvector columns (full-text search)
|
|
12
|
+
* - vector columns (pgvector semantic search)
|
|
13
|
+
* - @searchConfig smart tag (per-table search configuration)
|
|
14
|
+
* - @bm25Index smart tag on columns (BM25 search)
|
|
15
|
+
* - @trgmSearch smart tag (trigram search)
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildSearchMeta(codec: PgCodec, _build: unknown, inflectAttr: (attrName: string, codec: PgCodec) => string): SearchMeta | null;
|
|
18
|
+
/**
|
|
19
|
+
* Detect i18n metadata from a codec's @i18n smart tag.
|
|
20
|
+
* The @i18n tag value is the name of the translation table.
|
|
21
|
+
* Translatable fields are discovered by matching text/citext columns
|
|
22
|
+
* between the base table and the translation table codec.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildI18nMeta(codec: PgCodec, build: unknown, inflectAttr: (attrName: string, codec: PgCodec) => string): I18nMeta | null;
|
|
25
|
+
/**
|
|
26
|
+
* Detect realtime metadata from a codec's @realtime smart tag.
|
|
27
|
+
* Tables tagged with @realtime get subscription fields generated.
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildRealtimeMeta(codec: PgCodec, build: unknown): RealtimeMeta | null;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect storage metadata from a codec's smart tags.
|
|
3
|
+
* Storage tables are identified by @storageFiles and @storageBuckets smart tags.
|
|
4
|
+
*/
|
|
5
|
+
export function buildStorageMeta(codec) {
|
|
6
|
+
const tags = codec.extensions?.tags;
|
|
7
|
+
if (!tags)
|
|
8
|
+
return null;
|
|
9
|
+
const isFilesTable = !!tags.storageFiles;
|
|
10
|
+
const isBucketsTable = !!tags.storageBuckets;
|
|
11
|
+
if (!isFilesTable && !isBucketsTable)
|
|
12
|
+
return null;
|
|
13
|
+
return {
|
|
14
|
+
isFilesTable,
|
|
15
|
+
isBucketsTable,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Detect search metadata from a codec's columns and smart tags.
|
|
20
|
+
*
|
|
21
|
+
* Looks for:
|
|
22
|
+
* - tsvector columns (full-text search)
|
|
23
|
+
* - vector columns (pgvector semantic search)
|
|
24
|
+
* - @searchConfig smart tag (per-table search configuration)
|
|
25
|
+
* - @bm25Index smart tag on columns (BM25 search)
|
|
26
|
+
* - @trgmSearch smart tag (trigram search)
|
|
27
|
+
*/
|
|
28
|
+
export function buildSearchMeta(codec, _build, inflectAttr) {
|
|
29
|
+
const attributes = codec.attributes;
|
|
30
|
+
if (!attributes)
|
|
31
|
+
return null;
|
|
32
|
+
const tags = codec.extensions?.tags || {};
|
|
33
|
+
const columns = [];
|
|
34
|
+
const algorithmSet = new Set();
|
|
35
|
+
// Detect columns by type
|
|
36
|
+
for (const [attrName, attr] of Object.entries(attributes)) {
|
|
37
|
+
const pgType = attr?.codec?.name;
|
|
38
|
+
if (!pgType)
|
|
39
|
+
continue;
|
|
40
|
+
const inflectedName = inflectAttr(attrName, codec);
|
|
41
|
+
if (pgType === 'tsvector') {
|
|
42
|
+
columns.push({ name: inflectedName, algorithm: 'tsvector' });
|
|
43
|
+
algorithmSet.add('tsvector');
|
|
44
|
+
}
|
|
45
|
+
else if (pgType === 'vector') {
|
|
46
|
+
columns.push({ name: inflectedName, algorithm: 'vector' });
|
|
47
|
+
algorithmSet.add('vector');
|
|
48
|
+
}
|
|
49
|
+
// Check per-column @bm25Index tag
|
|
50
|
+
const attrTags = attr?.extensions?.tags;
|
|
51
|
+
if (attrTags?.bm25Index) {
|
|
52
|
+
columns.push({ name: inflectedName, algorithm: 'bm25' });
|
|
53
|
+
algorithmSet.add('bm25');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Check for table-level @trgmSearch tag
|
|
57
|
+
if (tags.trgmSearch) {
|
|
58
|
+
algorithmSet.add('trgm');
|
|
59
|
+
// trgm operates on text columns — detect which ones
|
|
60
|
+
for (const [attrName, attr] of Object.entries(attributes)) {
|
|
61
|
+
const pgType = attr?.codec?.name;
|
|
62
|
+
if (pgType === 'text' || pgType === 'varchar' || pgType === 'citext') {
|
|
63
|
+
const attrTags = attr?.extensions?.tags;
|
|
64
|
+
if (attrTags?.trgmSearch) {
|
|
65
|
+
columns.push({ name: inflectAttr(attrName, codec), algorithm: 'trgm' });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Parse @searchConfig smart tag
|
|
71
|
+
const config = parseSearchConfig(tags);
|
|
72
|
+
// If nothing search-related was found, return null
|
|
73
|
+
if (columns.length === 0 && !config)
|
|
74
|
+
return null;
|
|
75
|
+
// Determine if unified search is available
|
|
76
|
+
// unifiedSearch requires at least one text-compatible adapter (tsvector or bm25)
|
|
77
|
+
const hasUnifiedSearch = algorithmSet.has('tsvector') || algorithmSet.has('bm25');
|
|
78
|
+
return {
|
|
79
|
+
algorithms: Array.from(algorithmSet).sort(),
|
|
80
|
+
columns,
|
|
81
|
+
hasUnifiedSearch,
|
|
82
|
+
config,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Detect i18n metadata from a codec's @i18n smart tag.
|
|
87
|
+
* The @i18n tag value is the name of the translation table.
|
|
88
|
+
* Translatable fields are discovered by matching text/citext columns
|
|
89
|
+
* between the base table and the translation table codec.
|
|
90
|
+
*/
|
|
91
|
+
export function buildI18nMeta(codec, build, inflectAttr) {
|
|
92
|
+
const tags = codec.extensions?.tags;
|
|
93
|
+
if (!tags)
|
|
94
|
+
return null;
|
|
95
|
+
const i18nTag = tags.i18n;
|
|
96
|
+
if (typeof i18nTag !== 'string' || i18nTag.length === 0)
|
|
97
|
+
return null;
|
|
98
|
+
const attributes = codec.attributes;
|
|
99
|
+
if (!attributes)
|
|
100
|
+
return { translationTable: i18nTag, translatableFields: [] };
|
|
101
|
+
// Discover translatable fields: text/citext columns on the base table
|
|
102
|
+
const allowedTypes = ['text', 'citext'];
|
|
103
|
+
const translatableFields = [];
|
|
104
|
+
// Try to find the translation codec to get the intersection of fields
|
|
105
|
+
const pgRegistry = build?.input?.pgRegistry;
|
|
106
|
+
let translationAttrs = null;
|
|
107
|
+
if (pgRegistry?.pgResources) {
|
|
108
|
+
for (const r of Object.values(pgRegistry.pgResources)) {
|
|
109
|
+
const sqlName = r?.codec?.extensions?.pg?.name ?? r?.codec?.name;
|
|
110
|
+
if (sqlName === i18nTag) {
|
|
111
|
+
const tAttrs = r?.codec?.attributes;
|
|
112
|
+
if (tAttrs) {
|
|
113
|
+
translationAttrs = new Set(Object.keys(tAttrs));
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const [attrName, attr] of Object.entries(attributes)) {
|
|
120
|
+
const pgType = attr?.codec?.name;
|
|
121
|
+
if (!pgType || !allowedTypes.includes(pgType))
|
|
122
|
+
continue;
|
|
123
|
+
// If we found the translation table, only include columns that exist there too
|
|
124
|
+
if (translationAttrs && !translationAttrs.has(attrName))
|
|
125
|
+
continue;
|
|
126
|
+
translatableFields.push({
|
|
127
|
+
name: inflectAttr(attrName, codec),
|
|
128
|
+
type: pgType,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
translationTable: i18nTag,
|
|
133
|
+
translatableFields,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Detect realtime metadata from a codec's @realtime smart tag.
|
|
138
|
+
* Tables tagged with @realtime get subscription fields generated.
|
|
139
|
+
*/
|
|
140
|
+
export function buildRealtimeMeta(codec, build) {
|
|
141
|
+
const tags = codec.extensions?.tags;
|
|
142
|
+
if (!tags?.realtime)
|
|
143
|
+
return null;
|
|
144
|
+
const typeName = build.inflection?.tableType?.(codec);
|
|
145
|
+
if (!typeName)
|
|
146
|
+
return null;
|
|
147
|
+
return {
|
|
148
|
+
subscriptionFieldName: `on${typeName}Changed`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function parseSearchConfig(tags) {
|
|
152
|
+
const raw = tags.searchConfig;
|
|
153
|
+
if (!raw)
|
|
154
|
+
return null;
|
|
155
|
+
let parsed;
|
|
156
|
+
if (typeof raw === 'string') {
|
|
157
|
+
try {
|
|
158
|
+
parsed = JSON.parse(raw);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else if (typeof raw === 'object') {
|
|
165
|
+
parsed = raw;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
weights: parsed.weights && typeof parsed.weights === 'object'
|
|
172
|
+
? parsed.weights
|
|
173
|
+
: null,
|
|
174
|
+
boostRecent: !!parsed.boost_recent,
|
|
175
|
+
boostRecencyField: parsed.boost_recency_field || null,
|
|
176
|
+
boostRecencyDecay: typeof parsed.boost_recency_decay === 'number'
|
|
177
|
+
? parsed.boost_recency_decay
|
|
178
|
+
: null,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildForeignKeyConstraints, buildIndexes, buildPrimaryKey, buildUniqueConstraints, } from './constraint-meta-builders';
|
|
2
2
|
import { buildInflectionMeta, buildQueryMeta, resolveTableType } from './name-meta-builders';
|
|
3
3
|
import { buildBelongsToRelations, buildManyToManyRelations, buildReverseRelations, } from './relation-meta-builders';
|
|
4
|
+
import { buildStorageMeta, buildSearchMeta, buildI18nMeta, buildRealtimeMeta } from './storage-search-meta-builders';
|
|
4
5
|
import { buildFieldMeta } from './type-mappings';
|
|
5
6
|
import { createBuildContext, } from './table-meta-context';
|
|
6
7
|
import { getConfiguredSchemas, getRelations, getSchemaName, getUniques, isTableResource, } from './table-resource-utils';
|
|
@@ -48,6 +49,10 @@ function buildTableMeta(resource, schemaName, context) {
|
|
|
48
49
|
manyToMany,
|
|
49
50
|
};
|
|
50
51
|
const tableType = resolveTableType(context.build, codec);
|
|
52
|
+
const storage = buildStorageMeta(codec);
|
|
53
|
+
const search = buildSearchMeta(codec, context.build, context.inflectAttr);
|
|
54
|
+
const i18n = buildI18nMeta(codec, context.build, context.inflectAttr);
|
|
55
|
+
const realtime = buildRealtimeMeta(codec, context.build);
|
|
51
56
|
return {
|
|
52
57
|
name: tableType,
|
|
53
58
|
schemaName,
|
|
@@ -60,6 +65,10 @@ function buildTableMeta(resource, schemaName, context) {
|
|
|
60
65
|
relations: relationsMeta,
|
|
61
66
|
inflection: buildInflectionMeta(resource, tableType, context.build),
|
|
62
67
|
query: buildQueryMeta(resource, uniques, tableType, context.build),
|
|
68
|
+
storage,
|
|
69
|
+
search,
|
|
70
|
+
i18n,
|
|
71
|
+
realtime,
|
|
63
72
|
};
|
|
64
73
|
}
|
|
65
74
|
export function collectTablesMeta(build) {
|
|
@@ -60,6 +60,19 @@ function resolveGqlTypeName(build, codec) {
|
|
|
60
60
|
const nestedTypeName = codec.arrayOfCodec?.name;
|
|
61
61
|
return nestedTypeName ? pgTypeToGqlType(nestedTypeName) : pgTypeName;
|
|
62
62
|
}
|
|
63
|
+
function extractEnumMeta(codec) {
|
|
64
|
+
if (!codec)
|
|
65
|
+
return null;
|
|
66
|
+
// Check the codec itself, or unwrap domain/array wrappers
|
|
67
|
+
const inner = codec.domainOfCodec ?? codec.arrayOfCodec ?? codec;
|
|
68
|
+
const values = inner.values;
|
|
69
|
+
if (!Array.isArray(values) || values.length === 0)
|
|
70
|
+
return null;
|
|
71
|
+
return {
|
|
72
|
+
name: inner.name || codec.name || 'unknown',
|
|
73
|
+
values: values.map((v) => (typeof v === 'string' ? v : v.value)),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
63
76
|
export function buildFieldMeta(name, attr, build, options) {
|
|
64
77
|
const pgType = attr?.codec?.name || 'unknown';
|
|
65
78
|
const isNotNull = attr?.notNull || false;
|
|
@@ -80,5 +93,6 @@ export function buildFieldMeta(name, attr, build, options) {
|
|
|
80
93
|
isPrimaryKey: options?.isPrimaryKey ?? false,
|
|
81
94
|
isForeignKey: options?.isForeignKey ?? false,
|
|
82
95
|
description: attr?.description ?? null,
|
|
96
|
+
enumValues: extractEnumMeta(attr?.codec),
|
|
83
97
|
};
|
|
84
98
|
}
|
|
@@ -10,6 +10,64 @@ export interface TableMeta {
|
|
|
10
10
|
relations: RelationsMeta;
|
|
11
11
|
inflection: InflectionMeta;
|
|
12
12
|
query: QueryMeta;
|
|
13
|
+
storage: StorageMeta | null;
|
|
14
|
+
search: SearchMeta | null;
|
|
15
|
+
i18n: I18nMeta | null;
|
|
16
|
+
realtime: RealtimeMeta | null;
|
|
17
|
+
}
|
|
18
|
+
export interface StorageMeta {
|
|
19
|
+
/** Whether this table is tagged as a storage files table */
|
|
20
|
+
isFilesTable: boolean;
|
|
21
|
+
/** Whether this table is tagged as a storage buckets table */
|
|
22
|
+
isBucketsTable: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface SearchColumnMeta {
|
|
25
|
+
/** Column name (camelCase inflected) */
|
|
26
|
+
name: string;
|
|
27
|
+
/** Search algorithm: 'tsvector', 'bm25', 'trgm', 'vector' */
|
|
28
|
+
algorithm: string;
|
|
29
|
+
}
|
|
30
|
+
export interface SearchMeta {
|
|
31
|
+
/** Which search algorithms are active on this table */
|
|
32
|
+
algorithms: string[];
|
|
33
|
+
/** Searchable columns with their algorithm */
|
|
34
|
+
columns: SearchColumnMeta[];
|
|
35
|
+
/** Whether unifiedSearch composite filter is available */
|
|
36
|
+
hasUnifiedSearch: boolean;
|
|
37
|
+
/** Per-table search config from @searchConfig smart tag */
|
|
38
|
+
config: SearchConfigMeta | null;
|
|
39
|
+
}
|
|
40
|
+
export interface SearchConfigMeta {
|
|
41
|
+
/** Per-adapter score weights */
|
|
42
|
+
weights: Record<string, number> | null;
|
|
43
|
+
/** Whether recency boosting is enabled */
|
|
44
|
+
boostRecent: boolean;
|
|
45
|
+
/** Field used for recency decay */
|
|
46
|
+
boostRecencyField: string | null;
|
|
47
|
+
/** Exponential decay factor per day */
|
|
48
|
+
boostRecencyDecay: number | null;
|
|
49
|
+
}
|
|
50
|
+
export interface I18nFieldMeta {
|
|
51
|
+
/** Inflected GraphQL field name */
|
|
52
|
+
name: string;
|
|
53
|
+
/** PostgreSQL column type (text, citext) */
|
|
54
|
+
type: string;
|
|
55
|
+
}
|
|
56
|
+
export interface I18nMeta {
|
|
57
|
+
/** Name of the translation table */
|
|
58
|
+
translationTable: string;
|
|
59
|
+
/** Fields that are translatable */
|
|
60
|
+
translatableFields: I18nFieldMeta[];
|
|
61
|
+
}
|
|
62
|
+
export interface RealtimeMeta {
|
|
63
|
+
/** The generated subscription field name (e.g. onPostChanged) */
|
|
64
|
+
subscriptionFieldName: string;
|
|
65
|
+
}
|
|
66
|
+
export interface EnumMeta {
|
|
67
|
+
/** The PostgreSQL enum type name */
|
|
68
|
+
name: string;
|
|
69
|
+
/** Allowed values for this enum */
|
|
70
|
+
values: string[];
|
|
13
71
|
}
|
|
14
72
|
export interface FieldMeta {
|
|
15
73
|
name: string;
|
|
@@ -19,6 +77,7 @@ export interface FieldMeta {
|
|
|
19
77
|
isPrimaryKey: boolean;
|
|
20
78
|
isForeignKey: boolean;
|
|
21
79
|
description: string | null;
|
|
80
|
+
enumValues: EnumMeta | null;
|
|
22
81
|
}
|
|
23
82
|
export interface TypeMeta {
|
|
24
83
|
pgType: string;
|
|
@@ -213,3 +272,9 @@ export interface MetaBuild extends GqlTypeResolverBuild {
|
|
|
213
272
|
};
|
|
214
273
|
pgManyToManyRealtionshipsByResource?: Map<unknown, unknown>;
|
|
215
274
|
}
|
|
275
|
+
export interface PgCodecExtensions {
|
|
276
|
+
pg?: {
|
|
277
|
+
schemaName?: string;
|
|
278
|
+
};
|
|
279
|
+
tags?: Record<string, unknown>;
|
|
280
|
+
}
|
package/esm/upload-resolver.d.ts
CHANGED
|
@@ -15,18 +15,7 @@
|
|
|
15
15
|
* AWS_SECRET_KEY - secret key (default: 'minioadmin')
|
|
16
16
|
* CDN_ENDPOINT - S3-compatible endpoint (default: 'http://localhost:9000')
|
|
17
17
|
*/
|
|
18
|
-
import type { Readable } from 'stream';
|
|
19
18
|
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
19
|
/**
|
|
31
20
|
* Upload field definitions for Constructive's three upload domain types.
|
|
32
21
|
*
|
package/esm/upload-resolver.js
CHANGED
|
@@ -59,26 +59,6 @@ function generateKey(filename) {
|
|
|
59
59
|
const rand = randomBytes(12).toString('hex');
|
|
60
60
|
return `${rand}-${uploadNames(filename)}`;
|
|
61
61
|
}
|
|
62
|
-
/**
|
|
63
|
-
* Streams a file to S3/MinIO storage and returns the URL and metadata.
|
|
64
|
-
*
|
|
65
|
-
* Reusable by both the GraphQL upload resolver and REST /upload endpoint.
|
|
66
|
-
*/
|
|
67
|
-
export async function streamToStorage(readStream, filename) {
|
|
68
|
-
const s3 = getStreamer();
|
|
69
|
-
const key = generateKey(filename);
|
|
70
|
-
const result = await s3.upload({
|
|
71
|
-
readStream,
|
|
72
|
-
filename,
|
|
73
|
-
key,
|
|
74
|
-
bucket: bucketName,
|
|
75
|
-
});
|
|
76
|
-
return {
|
|
77
|
-
url: result.upload.Location,
|
|
78
|
-
filename,
|
|
79
|
-
mime: result.contentType,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
62
|
/**
|
|
83
63
|
* Upload resolver that streams files to S3/MinIO.
|
|
84
64
|
*
|
package/index.d.ts
CHANGED
|
@@ -34,6 +34,5 @@ export type { ConstructivePresetOptions } from './presets/constructive-preset';
|
|
|
34
34
|
export * from './plugins/index';
|
|
35
35
|
export * from './presets/index';
|
|
36
36
|
export { makePgService };
|
|
37
|
-
export { streamToStorage } from './upload-resolver';
|
|
38
37
|
export { getPresignedUrlS3Config } from './presigned-url-resolver';
|
|
39
38
|
export { getBucketProvisionerConnection } from './bucket-provisioner-resolver';
|
package/index.js
CHANGED
|
@@ -42,7 +42,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
42
42
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
43
43
|
};
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
-
exports.getBucketProvisionerConnection = exports.getPresignedUrlS3Config = exports.
|
|
45
|
+
exports.getBucketProvisionerConnection = exports.getPresignedUrlS3Config = exports.makePgService = exports.createConstructivePreset = exports.ConstructivePreset = void 0;
|
|
46
46
|
const pg_1 = require("postgraphile/adaptors/pg");
|
|
47
47
|
Object.defineProperty(exports, "makePgService", { enumerable: true, get: function () { return pg_1.makePgService; } });
|
|
48
48
|
// Import modules for type augmentation
|
|
@@ -63,9 +63,6 @@ Object.defineProperty(exports, "createConstructivePreset", { enumerable: true, g
|
|
|
63
63
|
__exportStar(require("./plugins/index"), exports);
|
|
64
64
|
// Re-export presets
|
|
65
65
|
__exportStar(require("./presets/index"), exports);
|
|
66
|
-
// Upload utilities
|
|
67
|
-
var upload_resolver_1 = require("./upload-resolver");
|
|
68
|
-
Object.defineProperty(exports, "streamToStorage", { enumerable: true, get: function () { return upload_resolver_1.streamToStorage; } });
|
|
69
66
|
// Presigned URL utilities
|
|
70
67
|
var presigned_url_resolver_1 = require("./presigned-url-resolver");
|
|
71
68
|
Object.defineProperty(exports, "getPresignedUrlS3Config", { enumerable: true, get: function () { return presigned_url_resolver_1.getPresignedUrlS3Config; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "graphile-settings",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.8.1",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "graphile settings",
|
|
6
6
|
"main": "index.js",
|
|
@@ -57,10 +57,10 @@
|
|
|
57
57
|
"graphile-ltree": "^1.9.5",
|
|
58
58
|
"graphile-pg-aggregates": "^1.5.4",
|
|
59
59
|
"graphile-postgis": "^2.18.4",
|
|
60
|
-
"graphile-presigned-url-plugin": "^0.20.
|
|
60
|
+
"graphile-presigned-url-plugin": "^0.20.1",
|
|
61
61
|
"graphile-realtime-subscriptions": "^0.8.1",
|
|
62
62
|
"graphile-search": "^1.17.0",
|
|
63
|
-
"graphile-upload-plugin": "^2.12.
|
|
63
|
+
"graphile-upload-plugin": "^2.12.2",
|
|
64
64
|
"graphile-utils": "5.0.1",
|
|
65
65
|
"graphql": "16.13.0",
|
|
66
66
|
"inflekt": "^0.7.1",
|
|
@@ -89,5 +89,5 @@
|
|
|
89
89
|
"constructive",
|
|
90
90
|
"graphql"
|
|
91
91
|
],
|
|
92
|
-
"gitHead": "
|
|
92
|
+
"gitHead": "d398ac2288f57fa02713353ce36024a63dd6b9f1"
|
|
93
93
|
}
|
|
@@ -21,6 +21,14 @@ function createMetaSchemaType() {
|
|
|
21
21
|
subtype: { type: graphql_1.GraphQLString },
|
|
22
22
|
}),
|
|
23
23
|
});
|
|
24
|
+
const MetaEnumType = new graphql_1.GraphQLObjectType({
|
|
25
|
+
name: 'MetaEnum',
|
|
26
|
+
description: 'Information about a PostgreSQL enum type',
|
|
27
|
+
fields: () => ({
|
|
28
|
+
name: { type: nn(graphql_1.GraphQLString), description: 'The PostgreSQL enum type name' },
|
|
29
|
+
values: { type: nnList(graphql_1.GraphQLString), description: 'Allowed values for this enum' },
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
24
32
|
const MetaFieldType = new graphql_1.GraphQLObjectType({
|
|
25
33
|
name: 'MetaField',
|
|
26
34
|
description: 'Information about a table field/column',
|
|
@@ -32,6 +40,7 @@ function createMetaSchemaType() {
|
|
|
32
40
|
isPrimaryKey: { type: nn(graphql_1.GraphQLBoolean) },
|
|
33
41
|
isForeignKey: { type: nn(graphql_1.GraphQLBoolean) },
|
|
34
42
|
description: { type: graphql_1.GraphQLString },
|
|
43
|
+
enumValues: { type: MetaEnumType, description: 'Enum metadata if this field has an enum type' },
|
|
35
44
|
}),
|
|
36
45
|
});
|
|
37
46
|
const MetaIndexType = new graphql_1.GraphQLObjectType({
|
|
@@ -167,6 +176,71 @@ function createMetaSchemaType() {
|
|
|
167
176
|
manyToMany: { type: nnList(MetaManyToManyRelationType) },
|
|
168
177
|
}),
|
|
169
178
|
});
|
|
179
|
+
const MetaStorageType = new graphql_1.GraphQLObjectType({
|
|
180
|
+
name: 'MetaStorage',
|
|
181
|
+
description: 'Storage metadata for a table',
|
|
182
|
+
fields: () => ({
|
|
183
|
+
isFilesTable: { type: nn(graphql_1.GraphQLBoolean), description: 'Whether this table is a storage files table' },
|
|
184
|
+
isBucketsTable: { type: nn(graphql_1.GraphQLBoolean), description: 'Whether this table is a storage buckets table' },
|
|
185
|
+
}),
|
|
186
|
+
});
|
|
187
|
+
const MetaSearchConfigType = new graphql_1.GraphQLObjectType({
|
|
188
|
+
name: 'MetaSearchConfig',
|
|
189
|
+
description: 'Per-table search configuration from @searchConfig smart tag',
|
|
190
|
+
fields: () => ({
|
|
191
|
+
weights: {
|
|
192
|
+
type: graphql_1.GraphQLString,
|
|
193
|
+
description: 'JSON-encoded per-adapter score weights',
|
|
194
|
+
resolve(source) {
|
|
195
|
+
return source.weights ? JSON.stringify(source.weights) : null;
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
boostRecent: { type: nn(graphql_1.GraphQLBoolean), description: 'Whether recency boosting is enabled' },
|
|
199
|
+
boostRecencyField: { type: graphql_1.GraphQLString, description: 'Field used for recency decay' },
|
|
200
|
+
boostRecencyDecay: { type: graphql_1.GraphQLFloat, description: 'Exponential decay factor per day' },
|
|
201
|
+
}),
|
|
202
|
+
});
|
|
203
|
+
const MetaSearchColumnType = new graphql_1.GraphQLObjectType({
|
|
204
|
+
name: 'MetaSearchColumn',
|
|
205
|
+
description: 'A searchable column with its algorithm',
|
|
206
|
+
fields: () => ({
|
|
207
|
+
name: { type: nn(graphql_1.GraphQLString), description: 'Column name (camelCase)' },
|
|
208
|
+
algorithm: { type: nn(graphql_1.GraphQLString), description: 'Search algorithm: tsvector, bm25, trgm, or vector' },
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
const MetaSearchType = new graphql_1.GraphQLObjectType({
|
|
212
|
+
name: 'MetaSearch',
|
|
213
|
+
description: 'Search metadata for a table',
|
|
214
|
+
fields: () => ({
|
|
215
|
+
algorithms: { type: nnList(graphql_1.GraphQLString), description: 'Active search algorithms on this table' },
|
|
216
|
+
columns: { type: nnList(MetaSearchColumnType), description: 'Searchable columns with their algorithm' },
|
|
217
|
+
hasUnifiedSearch: { type: nn(graphql_1.GraphQLBoolean), description: 'Whether unifiedSearch composite filter is available' },
|
|
218
|
+
config: { type: MetaSearchConfigType, description: 'Per-table search configuration' },
|
|
219
|
+
}),
|
|
220
|
+
});
|
|
221
|
+
const MetaI18nFieldType = new graphql_1.GraphQLObjectType({
|
|
222
|
+
name: 'MetaI18nField',
|
|
223
|
+
description: 'A translatable field',
|
|
224
|
+
fields: () => ({
|
|
225
|
+
name: { type: nn(graphql_1.GraphQLString), description: 'GraphQL field name' },
|
|
226
|
+
type: { type: nn(graphql_1.GraphQLString), description: 'PostgreSQL column type (text, citext)' },
|
|
227
|
+
}),
|
|
228
|
+
});
|
|
229
|
+
const MetaI18nType = new graphql_1.GraphQLObjectType({
|
|
230
|
+
name: 'MetaI18n',
|
|
231
|
+
description: 'i18n metadata for a table with @i18n tag',
|
|
232
|
+
fields: () => ({
|
|
233
|
+
translationTable: { type: nn(graphql_1.GraphQLString), description: 'Name of the translation table' },
|
|
234
|
+
translatableFields: { type: nnList(MetaI18nFieldType), description: 'Fields that are translatable' },
|
|
235
|
+
}),
|
|
236
|
+
});
|
|
237
|
+
const MetaRealtimeType = new graphql_1.GraphQLObjectType({
|
|
238
|
+
name: 'MetaRealtime',
|
|
239
|
+
description: 'Realtime metadata for a table with @realtime tag',
|
|
240
|
+
fields: () => ({
|
|
241
|
+
subscriptionFieldName: { type: nn(graphql_1.GraphQLString), description: 'The generated subscription field name (e.g. onPostChanged)' },
|
|
242
|
+
}),
|
|
243
|
+
});
|
|
170
244
|
const MetaTableType = new graphql_1.GraphQLObjectType({
|
|
171
245
|
name: 'MetaTable',
|
|
172
246
|
description: 'Information about a database table',
|
|
@@ -182,6 +256,10 @@ function createMetaSchemaType() {
|
|
|
182
256
|
relations: { type: nn(MetaRelationsType) },
|
|
183
257
|
inflection: { type: nn(MetaInflectionType) },
|
|
184
258
|
query: { type: nn(MetaQueryType) },
|
|
259
|
+
storage: { type: MetaStorageType, description: 'Storage metadata (null if not a storage table)' },
|
|
260
|
+
search: { type: MetaSearchType, description: 'Search metadata (null if no search configured)' },
|
|
261
|
+
i18n: { type: MetaI18nType, description: 'i18n metadata (null if no @i18n tag)' },
|
|
262
|
+
realtime: { type: MetaRealtimeType, description: 'Realtime metadata (null if no @realtime tag)' },
|
|
185
263
|
}),
|
|
186
264
|
});
|
|
187
265
|
return new graphql_1.GraphQLObjectType({
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { I18nMeta, PgCodec, RealtimeMeta, SearchMeta, StorageMeta } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Detect storage metadata from a codec's smart tags.
|
|
4
|
+
* Storage tables are identified by @storageFiles and @storageBuckets smart tags.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildStorageMeta(codec: PgCodec): StorageMeta | null;
|
|
7
|
+
/**
|
|
8
|
+
* Detect search metadata from a codec's columns and smart tags.
|
|
9
|
+
*
|
|
10
|
+
* Looks for:
|
|
11
|
+
* - tsvector columns (full-text search)
|
|
12
|
+
* - vector columns (pgvector semantic search)
|
|
13
|
+
* - @searchConfig smart tag (per-table search configuration)
|
|
14
|
+
* - @bm25Index smart tag on columns (BM25 search)
|
|
15
|
+
* - @trgmSearch smart tag (trigram search)
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildSearchMeta(codec: PgCodec, _build: unknown, inflectAttr: (attrName: string, codec: PgCodec) => string): SearchMeta | null;
|
|
18
|
+
/**
|
|
19
|
+
* Detect i18n metadata from a codec's @i18n smart tag.
|
|
20
|
+
* The @i18n tag value is the name of the translation table.
|
|
21
|
+
* Translatable fields are discovered by matching text/citext columns
|
|
22
|
+
* between the base table and the translation table codec.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildI18nMeta(codec: PgCodec, build: unknown, inflectAttr: (attrName: string, codec: PgCodec) => string): I18nMeta | null;
|
|
25
|
+
/**
|
|
26
|
+
* Detect realtime metadata from a codec's @realtime smart tag.
|
|
27
|
+
* Tables tagged with @realtime get subscription fields generated.
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildRealtimeMeta(codec: PgCodec, build: unknown): RealtimeMeta | null;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildStorageMeta = buildStorageMeta;
|
|
4
|
+
exports.buildSearchMeta = buildSearchMeta;
|
|
5
|
+
exports.buildI18nMeta = buildI18nMeta;
|
|
6
|
+
exports.buildRealtimeMeta = buildRealtimeMeta;
|
|
7
|
+
/**
|
|
8
|
+
* Detect storage metadata from a codec's smart tags.
|
|
9
|
+
* Storage tables are identified by @storageFiles and @storageBuckets smart tags.
|
|
10
|
+
*/
|
|
11
|
+
function buildStorageMeta(codec) {
|
|
12
|
+
const tags = codec.extensions?.tags;
|
|
13
|
+
if (!tags)
|
|
14
|
+
return null;
|
|
15
|
+
const isFilesTable = !!tags.storageFiles;
|
|
16
|
+
const isBucketsTable = !!tags.storageBuckets;
|
|
17
|
+
if (!isFilesTable && !isBucketsTable)
|
|
18
|
+
return null;
|
|
19
|
+
return {
|
|
20
|
+
isFilesTable,
|
|
21
|
+
isBucketsTable,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Detect search metadata from a codec's columns and smart tags.
|
|
26
|
+
*
|
|
27
|
+
* Looks for:
|
|
28
|
+
* - tsvector columns (full-text search)
|
|
29
|
+
* - vector columns (pgvector semantic search)
|
|
30
|
+
* - @searchConfig smart tag (per-table search configuration)
|
|
31
|
+
* - @bm25Index smart tag on columns (BM25 search)
|
|
32
|
+
* - @trgmSearch smart tag (trigram search)
|
|
33
|
+
*/
|
|
34
|
+
function buildSearchMeta(codec, _build, inflectAttr) {
|
|
35
|
+
const attributes = codec.attributes;
|
|
36
|
+
if (!attributes)
|
|
37
|
+
return null;
|
|
38
|
+
const tags = codec.extensions?.tags || {};
|
|
39
|
+
const columns = [];
|
|
40
|
+
const algorithmSet = new Set();
|
|
41
|
+
// Detect columns by type
|
|
42
|
+
for (const [attrName, attr] of Object.entries(attributes)) {
|
|
43
|
+
const pgType = attr?.codec?.name;
|
|
44
|
+
if (!pgType)
|
|
45
|
+
continue;
|
|
46
|
+
const inflectedName = inflectAttr(attrName, codec);
|
|
47
|
+
if (pgType === 'tsvector') {
|
|
48
|
+
columns.push({ name: inflectedName, algorithm: 'tsvector' });
|
|
49
|
+
algorithmSet.add('tsvector');
|
|
50
|
+
}
|
|
51
|
+
else if (pgType === 'vector') {
|
|
52
|
+
columns.push({ name: inflectedName, algorithm: 'vector' });
|
|
53
|
+
algorithmSet.add('vector');
|
|
54
|
+
}
|
|
55
|
+
// Check per-column @bm25Index tag
|
|
56
|
+
const attrTags = attr?.extensions?.tags;
|
|
57
|
+
if (attrTags?.bm25Index) {
|
|
58
|
+
columns.push({ name: inflectedName, algorithm: 'bm25' });
|
|
59
|
+
algorithmSet.add('bm25');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Check for table-level @trgmSearch tag
|
|
63
|
+
if (tags.trgmSearch) {
|
|
64
|
+
algorithmSet.add('trgm');
|
|
65
|
+
// trgm operates on text columns — detect which ones
|
|
66
|
+
for (const [attrName, attr] of Object.entries(attributes)) {
|
|
67
|
+
const pgType = attr?.codec?.name;
|
|
68
|
+
if (pgType === 'text' || pgType === 'varchar' || pgType === 'citext') {
|
|
69
|
+
const attrTags = attr?.extensions?.tags;
|
|
70
|
+
if (attrTags?.trgmSearch) {
|
|
71
|
+
columns.push({ name: inflectAttr(attrName, codec), algorithm: 'trgm' });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Parse @searchConfig smart tag
|
|
77
|
+
const config = parseSearchConfig(tags);
|
|
78
|
+
// If nothing search-related was found, return null
|
|
79
|
+
if (columns.length === 0 && !config)
|
|
80
|
+
return null;
|
|
81
|
+
// Determine if unified search is available
|
|
82
|
+
// unifiedSearch requires at least one text-compatible adapter (tsvector or bm25)
|
|
83
|
+
const hasUnifiedSearch = algorithmSet.has('tsvector') || algorithmSet.has('bm25');
|
|
84
|
+
return {
|
|
85
|
+
algorithms: Array.from(algorithmSet).sort(),
|
|
86
|
+
columns,
|
|
87
|
+
hasUnifiedSearch,
|
|
88
|
+
config,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Detect i18n metadata from a codec's @i18n smart tag.
|
|
93
|
+
* The @i18n tag value is the name of the translation table.
|
|
94
|
+
* Translatable fields are discovered by matching text/citext columns
|
|
95
|
+
* between the base table and the translation table codec.
|
|
96
|
+
*/
|
|
97
|
+
function buildI18nMeta(codec, build, inflectAttr) {
|
|
98
|
+
const tags = codec.extensions?.tags;
|
|
99
|
+
if (!tags)
|
|
100
|
+
return null;
|
|
101
|
+
const i18nTag = tags.i18n;
|
|
102
|
+
if (typeof i18nTag !== 'string' || i18nTag.length === 0)
|
|
103
|
+
return null;
|
|
104
|
+
const attributes = codec.attributes;
|
|
105
|
+
if (!attributes)
|
|
106
|
+
return { translationTable: i18nTag, translatableFields: [] };
|
|
107
|
+
// Discover translatable fields: text/citext columns on the base table
|
|
108
|
+
const allowedTypes = ['text', 'citext'];
|
|
109
|
+
const translatableFields = [];
|
|
110
|
+
// Try to find the translation codec to get the intersection of fields
|
|
111
|
+
const pgRegistry = build?.input?.pgRegistry;
|
|
112
|
+
let translationAttrs = null;
|
|
113
|
+
if (pgRegistry?.pgResources) {
|
|
114
|
+
for (const r of Object.values(pgRegistry.pgResources)) {
|
|
115
|
+
const sqlName = r?.codec?.extensions?.pg?.name ?? r?.codec?.name;
|
|
116
|
+
if (sqlName === i18nTag) {
|
|
117
|
+
const tAttrs = r?.codec?.attributes;
|
|
118
|
+
if (tAttrs) {
|
|
119
|
+
translationAttrs = new Set(Object.keys(tAttrs));
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const [attrName, attr] of Object.entries(attributes)) {
|
|
126
|
+
const pgType = attr?.codec?.name;
|
|
127
|
+
if (!pgType || !allowedTypes.includes(pgType))
|
|
128
|
+
continue;
|
|
129
|
+
// If we found the translation table, only include columns that exist there too
|
|
130
|
+
if (translationAttrs && !translationAttrs.has(attrName))
|
|
131
|
+
continue;
|
|
132
|
+
translatableFields.push({
|
|
133
|
+
name: inflectAttr(attrName, codec),
|
|
134
|
+
type: pgType,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
translationTable: i18nTag,
|
|
139
|
+
translatableFields,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Detect realtime metadata from a codec's @realtime smart tag.
|
|
144
|
+
* Tables tagged with @realtime get subscription fields generated.
|
|
145
|
+
*/
|
|
146
|
+
function buildRealtimeMeta(codec, build) {
|
|
147
|
+
const tags = codec.extensions?.tags;
|
|
148
|
+
if (!tags?.realtime)
|
|
149
|
+
return null;
|
|
150
|
+
const typeName = build.inflection?.tableType?.(codec);
|
|
151
|
+
if (!typeName)
|
|
152
|
+
return null;
|
|
153
|
+
return {
|
|
154
|
+
subscriptionFieldName: `on${typeName}Changed`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function parseSearchConfig(tags) {
|
|
158
|
+
const raw = tags.searchConfig;
|
|
159
|
+
if (!raw)
|
|
160
|
+
return null;
|
|
161
|
+
let parsed;
|
|
162
|
+
if (typeof raw === 'string') {
|
|
163
|
+
try {
|
|
164
|
+
parsed = JSON.parse(raw);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else if (typeof raw === 'object') {
|
|
171
|
+
parsed = raw;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
weights: parsed.weights && typeof parsed.weights === 'object'
|
|
178
|
+
? parsed.weights
|
|
179
|
+
: null,
|
|
180
|
+
boostRecent: !!parsed.boost_recent,
|
|
181
|
+
boostRecencyField: parsed.boost_recency_field || null,
|
|
182
|
+
boostRecencyDecay: typeof parsed.boost_recency_decay === 'number'
|
|
183
|
+
? parsed.boost_recency_decay
|
|
184
|
+
: null,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -4,6 +4,7 @@ exports.collectTablesMeta = collectTablesMeta;
|
|
|
4
4
|
const constraint_meta_builders_1 = require("./constraint-meta-builders");
|
|
5
5
|
const name_meta_builders_1 = require("./name-meta-builders");
|
|
6
6
|
const relation_meta_builders_1 = require("./relation-meta-builders");
|
|
7
|
+
const storage_search_meta_builders_1 = require("./storage-search-meta-builders");
|
|
7
8
|
const type_mappings_1 = require("./type-mappings");
|
|
8
9
|
const table_meta_context_1 = require("./table-meta-context");
|
|
9
10
|
const table_resource_utils_1 = require("./table-resource-utils");
|
|
@@ -51,6 +52,10 @@ function buildTableMeta(resource, schemaName, context) {
|
|
|
51
52
|
manyToMany,
|
|
52
53
|
};
|
|
53
54
|
const tableType = (0, name_meta_builders_1.resolveTableType)(context.build, codec);
|
|
55
|
+
const storage = (0, storage_search_meta_builders_1.buildStorageMeta)(codec);
|
|
56
|
+
const search = (0, storage_search_meta_builders_1.buildSearchMeta)(codec, context.build, context.inflectAttr);
|
|
57
|
+
const i18n = (0, storage_search_meta_builders_1.buildI18nMeta)(codec, context.build, context.inflectAttr);
|
|
58
|
+
const realtime = (0, storage_search_meta_builders_1.buildRealtimeMeta)(codec, context.build);
|
|
54
59
|
return {
|
|
55
60
|
name: tableType,
|
|
56
61
|
schemaName,
|
|
@@ -63,6 +68,10 @@ function buildTableMeta(resource, schemaName, context) {
|
|
|
63
68
|
relations: relationsMeta,
|
|
64
69
|
inflection: (0, name_meta_builders_1.buildInflectionMeta)(resource, tableType, context.build),
|
|
65
70
|
query: (0, name_meta_builders_1.buildQueryMeta)(resource, uniques, tableType, context.build),
|
|
71
|
+
storage,
|
|
72
|
+
search,
|
|
73
|
+
i18n,
|
|
74
|
+
realtime,
|
|
66
75
|
};
|
|
67
76
|
}
|
|
68
77
|
function collectTablesMeta(build) {
|
|
@@ -64,6 +64,19 @@ function resolveGqlTypeName(build, codec) {
|
|
|
64
64
|
const nestedTypeName = codec.arrayOfCodec?.name;
|
|
65
65
|
return nestedTypeName ? pgTypeToGqlType(nestedTypeName) : pgTypeName;
|
|
66
66
|
}
|
|
67
|
+
function extractEnumMeta(codec) {
|
|
68
|
+
if (!codec)
|
|
69
|
+
return null;
|
|
70
|
+
// Check the codec itself, or unwrap domain/array wrappers
|
|
71
|
+
const inner = codec.domainOfCodec ?? codec.arrayOfCodec ?? codec;
|
|
72
|
+
const values = inner.values;
|
|
73
|
+
if (!Array.isArray(values) || values.length === 0)
|
|
74
|
+
return null;
|
|
75
|
+
return {
|
|
76
|
+
name: inner.name || codec.name || 'unknown',
|
|
77
|
+
values: values.map((v) => (typeof v === 'string' ? v : v.value)),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
67
80
|
function buildFieldMeta(name, attr, build, options) {
|
|
68
81
|
const pgType = attr?.codec?.name || 'unknown';
|
|
69
82
|
const isNotNull = attr?.notNull || false;
|
|
@@ -84,5 +97,6 @@ function buildFieldMeta(name, attr, build, options) {
|
|
|
84
97
|
isPrimaryKey: options?.isPrimaryKey ?? false,
|
|
85
98
|
isForeignKey: options?.isForeignKey ?? false,
|
|
86
99
|
description: attr?.description ?? null,
|
|
100
|
+
enumValues: extractEnumMeta(attr?.codec),
|
|
87
101
|
};
|
|
88
102
|
}
|
|
@@ -10,6 +10,64 @@ export interface TableMeta {
|
|
|
10
10
|
relations: RelationsMeta;
|
|
11
11
|
inflection: InflectionMeta;
|
|
12
12
|
query: QueryMeta;
|
|
13
|
+
storage: StorageMeta | null;
|
|
14
|
+
search: SearchMeta | null;
|
|
15
|
+
i18n: I18nMeta | null;
|
|
16
|
+
realtime: RealtimeMeta | null;
|
|
17
|
+
}
|
|
18
|
+
export interface StorageMeta {
|
|
19
|
+
/** Whether this table is tagged as a storage files table */
|
|
20
|
+
isFilesTable: boolean;
|
|
21
|
+
/** Whether this table is tagged as a storage buckets table */
|
|
22
|
+
isBucketsTable: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface SearchColumnMeta {
|
|
25
|
+
/** Column name (camelCase inflected) */
|
|
26
|
+
name: string;
|
|
27
|
+
/** Search algorithm: 'tsvector', 'bm25', 'trgm', 'vector' */
|
|
28
|
+
algorithm: string;
|
|
29
|
+
}
|
|
30
|
+
export interface SearchMeta {
|
|
31
|
+
/** Which search algorithms are active on this table */
|
|
32
|
+
algorithms: string[];
|
|
33
|
+
/** Searchable columns with their algorithm */
|
|
34
|
+
columns: SearchColumnMeta[];
|
|
35
|
+
/** Whether unifiedSearch composite filter is available */
|
|
36
|
+
hasUnifiedSearch: boolean;
|
|
37
|
+
/** Per-table search config from @searchConfig smart tag */
|
|
38
|
+
config: SearchConfigMeta | null;
|
|
39
|
+
}
|
|
40
|
+
export interface SearchConfigMeta {
|
|
41
|
+
/** Per-adapter score weights */
|
|
42
|
+
weights: Record<string, number> | null;
|
|
43
|
+
/** Whether recency boosting is enabled */
|
|
44
|
+
boostRecent: boolean;
|
|
45
|
+
/** Field used for recency decay */
|
|
46
|
+
boostRecencyField: string | null;
|
|
47
|
+
/** Exponential decay factor per day */
|
|
48
|
+
boostRecencyDecay: number | null;
|
|
49
|
+
}
|
|
50
|
+
export interface I18nFieldMeta {
|
|
51
|
+
/** Inflected GraphQL field name */
|
|
52
|
+
name: string;
|
|
53
|
+
/** PostgreSQL column type (text, citext) */
|
|
54
|
+
type: string;
|
|
55
|
+
}
|
|
56
|
+
export interface I18nMeta {
|
|
57
|
+
/** Name of the translation table */
|
|
58
|
+
translationTable: string;
|
|
59
|
+
/** Fields that are translatable */
|
|
60
|
+
translatableFields: I18nFieldMeta[];
|
|
61
|
+
}
|
|
62
|
+
export interface RealtimeMeta {
|
|
63
|
+
/** The generated subscription field name (e.g. onPostChanged) */
|
|
64
|
+
subscriptionFieldName: string;
|
|
65
|
+
}
|
|
66
|
+
export interface EnumMeta {
|
|
67
|
+
/** The PostgreSQL enum type name */
|
|
68
|
+
name: string;
|
|
69
|
+
/** Allowed values for this enum */
|
|
70
|
+
values: string[];
|
|
13
71
|
}
|
|
14
72
|
export interface FieldMeta {
|
|
15
73
|
name: string;
|
|
@@ -19,6 +77,7 @@ export interface FieldMeta {
|
|
|
19
77
|
isPrimaryKey: boolean;
|
|
20
78
|
isForeignKey: boolean;
|
|
21
79
|
description: string | null;
|
|
80
|
+
enumValues: EnumMeta | null;
|
|
22
81
|
}
|
|
23
82
|
export interface TypeMeta {
|
|
24
83
|
pgType: string;
|
|
@@ -213,3 +272,9 @@ export interface MetaBuild extends GqlTypeResolverBuild {
|
|
|
213
272
|
};
|
|
214
273
|
pgManyToManyRealtionshipsByResource?: Map<unknown, unknown>;
|
|
215
274
|
}
|
|
275
|
+
export interface PgCodecExtensions {
|
|
276
|
+
pg?: {
|
|
277
|
+
schemaName?: string;
|
|
278
|
+
};
|
|
279
|
+
tags?: Record<string, unknown>;
|
|
280
|
+
}
|
package/upload-resolver.d.ts
CHANGED
|
@@ -15,18 +15,7 @@
|
|
|
15
15
|
* AWS_SECRET_KEY - secret key (default: 'minioadmin')
|
|
16
16
|
* CDN_ENDPOINT - S3-compatible endpoint (default: 'http://localhost:9000')
|
|
17
17
|
*/
|
|
18
|
-
import type { Readable } from 'stream';
|
|
19
18
|
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
19
|
/**
|
|
31
20
|
* Upload field definitions for Constructive's three upload domain types.
|
|
32
21
|
*
|
package/upload-resolver.js
CHANGED
|
@@ -21,7 +21,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
21
21
|
};
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
23
|
exports.constructiveUploadFieldDefinitions = void 0;
|
|
24
|
-
exports.streamToStorage = streamToStorage;
|
|
25
24
|
const s3_streamer_1 = __importDefault(require("@constructive-io/s3-streamer"));
|
|
26
25
|
const upload_names_1 = __importDefault(require("@constructive-io/upload-names"));
|
|
27
26
|
const graphql_env_1 = require("@constructive-io/graphql-env");
|
|
@@ -66,26 +65,6 @@ function generateKey(filename) {
|
|
|
66
65
|
const rand = (0, crypto_1.randomBytes)(12).toString('hex');
|
|
67
66
|
return `${rand}-${(0, upload_names_1.default)(filename)}`;
|
|
68
67
|
}
|
|
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
68
|
/**
|
|
90
69
|
* Upload resolver that streams files to S3/MinIO.
|
|
91
70
|
*
|