graphile-i18n 1.1.0 → 1.2.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.
- package/README.md +127 -60
- package/esm/index.d.ts +37 -0
- package/esm/index.js +38 -5
- package/esm/middleware.d.ts +38 -0
- package/esm/middleware.js +43 -50
- package/esm/plugin.d.ts +31 -0
- package/esm/plugin.js +281 -144
- package/esm/preset.d.ts +34 -0
- package/esm/preset.js +37 -0
- package/esm/types.d.ts +46 -0
- package/esm/types.js +4 -0
- package/index.d.ts +37 -5
- package/index.js +42 -11
- package/middleware.d.ts +36 -24
- package/middleware.js +78 -56
- package/package.json +36 -28
- package/plugin.d.ts +30 -9
- package/plugin.js +283 -148
- package/preset.d.ts +34 -0
- package/preset.js +40 -0
- package/types.d.ts +46 -0
- package/types.js +5 -0
- package/env.d.ts +0 -5
- package/env.js +0 -17
- package/esm/env.js +0 -14
package/esm/plugin.js
CHANGED
|
@@ -1,147 +1,284 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
1
|
+
/**
|
|
2
|
+
* PostGraphile v5 i18n Plugin
|
|
3
|
+
*
|
|
4
|
+
* Discovers tables tagged with @i18n and adds a `localeStrings` field to the
|
|
5
|
+
* base type. The field resolves the best-matching translation row based on
|
|
6
|
+
* language codes provided in the GraphQL context, falling back to the base
|
|
7
|
+
* table's own values when no translation exists.
|
|
8
|
+
*
|
|
9
|
+
* Smart tag format:
|
|
10
|
+
* COMMENT ON TABLE app_public.posts IS E'@i18n posts_translations';
|
|
11
|
+
*
|
|
12
|
+
* The value of @i18n is the name of the translation table in the same schema.
|
|
13
|
+
* The translation table must have:
|
|
14
|
+
* - A FK column referencing the base table's PK
|
|
15
|
+
* - A lang_code column (configurable)
|
|
16
|
+
* - UNIQUE(fk_column, lang_code)
|
|
17
|
+
* - One or more text/citext columns matching the base table's columns
|
|
18
|
+
*/
|
|
19
|
+
import 'graphile-build';
|
|
20
|
+
import 'graphile-build-pg';
|
|
21
|
+
import { TYPES } from '@dataplan/pg';
|
|
22
|
+
import { context as grafastContext, lambda, object } from 'grafast';
|
|
23
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
24
|
+
function hasI18nTag(codec) {
|
|
25
|
+
const tags = codec.extensions?.tags;
|
|
26
|
+
if (!tags)
|
|
27
|
+
return false;
|
|
28
|
+
const val = tags.i18n;
|
|
29
|
+
if (typeof val === 'string' && val.length > 0)
|
|
30
|
+
return val;
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
function resolvePgTypeName(codec) {
|
|
34
|
+
if (codec === TYPES.uuid)
|
|
35
|
+
return 'uuid';
|
|
36
|
+
if (codec === TYPES.int)
|
|
37
|
+
return 'int4';
|
|
38
|
+
if (codec === TYPES.bigint)
|
|
39
|
+
return 'int8';
|
|
40
|
+
if (codec === TYPES.text)
|
|
41
|
+
return 'text';
|
|
42
|
+
if (codec === TYPES.varchar)
|
|
43
|
+
return 'text';
|
|
44
|
+
return codec?.name ?? 'text';
|
|
45
|
+
}
|
|
46
|
+
function resolveAttrPgType(codec) {
|
|
47
|
+
if (codec === TYPES.text)
|
|
48
|
+
return 'text';
|
|
49
|
+
if (codec === TYPES.varchar)
|
|
50
|
+
return 'text';
|
|
51
|
+
if (codec?.name === 'citext')
|
|
52
|
+
return 'citext';
|
|
53
|
+
return codec?.name ?? 'text';
|
|
54
|
+
}
|
|
55
|
+
// ─── Plugin Factory ──────────────────────────────────────────────────────────
|
|
56
|
+
export function createI18nPlugin(options = {}) {
|
|
57
|
+
const { langCodeColumn = 'lang_code', langCodeGqlField = 'langCode', allowedTypes = ['text', 'citext'], defaultLanguages = ['en'], } = options;
|
|
58
|
+
// Closure-scoped state shared between init and field hooks
|
|
59
|
+
let i18nRegistry = {};
|
|
60
|
+
const localeTypeCache = {};
|
|
61
|
+
return {
|
|
62
|
+
name: 'I18nPlugin',
|
|
63
|
+
version: '1.0.0',
|
|
64
|
+
schema: {
|
|
65
|
+
hooks: {
|
|
66
|
+
init: {
|
|
67
|
+
callback(_, build) {
|
|
68
|
+
i18nRegistry = {};
|
|
69
|
+
for (const [, codec] of Object.entries(build.input.pgRegistry.pgCodecs)) {
|
|
70
|
+
const c = codec;
|
|
71
|
+
if (!c.attributes)
|
|
72
|
+
continue;
|
|
73
|
+
const translationTableName = hasI18nTag(c);
|
|
74
|
+
if (!translationTableName)
|
|
75
|
+
continue;
|
|
76
|
+
// Get schema name from the codec's pg extensions
|
|
77
|
+
let schemaName = c.extensions?.pg?.schemaName ?? 'public';
|
|
78
|
+
let pkColumn = null;
|
|
79
|
+
let pkType = 'text';
|
|
80
|
+
for (const [, resource] of Object.entries(build.input.pgRegistry.pgResources)) {
|
|
81
|
+
const r = resource;
|
|
82
|
+
if (r.codec === c) {
|
|
83
|
+
// Try multiple sources for schema name
|
|
84
|
+
const rSchema = r.extensions?.pg?.schemaName ?? r.schemaName;
|
|
85
|
+
if (rSchema)
|
|
86
|
+
schemaName = rSchema;
|
|
87
|
+
// Extract PK from the resource's uniques array
|
|
88
|
+
const uniques = r.uniques;
|
|
89
|
+
if (uniques) {
|
|
90
|
+
const pk = uniques.find((u) => u.isPrimary);
|
|
91
|
+
if (pk && pk.attributes.length === 1) {
|
|
92
|
+
pkColumn = pk.attributes[0];
|
|
93
|
+
const pkAttr = c.attributes[pkColumn];
|
|
94
|
+
if (pkAttr) {
|
|
95
|
+
pkType = resolvePgTypeName(pkAttr.codec);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!pkColumn)
|
|
103
|
+
continue;
|
|
104
|
+
// Find the translation codec. The @i18n tag value is the SQL table name
|
|
105
|
+
// (e.g. 'posts_translations'), but PostGraphile inflects codec names
|
|
106
|
+
// to camelCase (e.g. 'postsTranslations'). Match via resource name.
|
|
107
|
+
let translationCodec = null;
|
|
108
|
+
for (const [, resource] of Object.entries(build.input.pgRegistry.pgResources)) {
|
|
109
|
+
const r = resource;
|
|
110
|
+
if (!r.codec?.attributes)
|
|
111
|
+
continue;
|
|
112
|
+
// Match by the resource's SQL name (which preserves snake_case)
|
|
113
|
+
const sqlName = r.codec?.extensions?.pg?.name ?? r.name;
|
|
114
|
+
if (sqlName === translationTableName) {
|
|
115
|
+
translationCodec = r.codec;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Fallback: try matching the inflected codec name directly
|
|
120
|
+
if (!translationCodec) {
|
|
121
|
+
const inflectedName = build.inflection.camelCase(translationTableName);
|
|
122
|
+
for (const [, tCodec] of Object.entries(build.input.pgRegistry.pgCodecs)) {
|
|
123
|
+
const tc = tCodec;
|
|
124
|
+
if (!tc.attributes)
|
|
125
|
+
continue;
|
|
126
|
+
if (tc.name === translationTableName || tc.name === inflectedName) {
|
|
127
|
+
translationCodec = tc;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!translationCodec)
|
|
133
|
+
continue;
|
|
134
|
+
// Find FK column on translation table — convention first, then type match
|
|
135
|
+
let fkColumn = null;
|
|
136
|
+
const conventionalFk = `${c.name}_id`;
|
|
137
|
+
if (translationCodec.attributes[conventionalFk]) {
|
|
138
|
+
fkColumn = conventionalFk;
|
|
139
|
+
}
|
|
140
|
+
if (!fkColumn) {
|
|
141
|
+
// Fallback: find a column with the same type as the PK, excluding
|
|
142
|
+
// common non-FK columns (id, lang_code)
|
|
143
|
+
for (const [attrName, attr] of Object.entries(translationCodec.attributes)) {
|
|
144
|
+
if (attrName === 'id' || attrName === langCodeColumn)
|
|
145
|
+
continue;
|
|
146
|
+
const a = attr;
|
|
147
|
+
if (a.codec === c.attributes[pkColumn].codec) {
|
|
148
|
+
fkColumn = attrName;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!fkColumn)
|
|
154
|
+
continue;
|
|
155
|
+
// Discover translatable fields
|
|
156
|
+
const fields = {};
|
|
157
|
+
for (const [attrName, attr] of Object.entries(translationCodec.attributes)) {
|
|
158
|
+
if (attrName === langCodeColumn || attrName === fkColumn)
|
|
159
|
+
continue;
|
|
160
|
+
if (attrName === 'id' || attrName === 'created_at' || attrName === 'updated_at')
|
|
161
|
+
continue;
|
|
162
|
+
const pgType = resolveAttrPgType(attr.codec);
|
|
163
|
+
if (!allowedTypes.includes(pgType))
|
|
164
|
+
continue;
|
|
165
|
+
const gqlName = build.inflection.camelCase(attrName);
|
|
166
|
+
fields[gqlName] = {
|
|
167
|
+
column: attrName,
|
|
168
|
+
type: pgType,
|
|
169
|
+
isNotNull: !!attr.notNull,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (Object.keys(fields).length === 0)
|
|
173
|
+
continue;
|
|
174
|
+
i18nRegistry[c.name] = {
|
|
175
|
+
baseTable: c.name,
|
|
176
|
+
translationTable: translationTableName,
|
|
177
|
+
schemaName,
|
|
178
|
+
fkColumn,
|
|
179
|
+
pkColumn,
|
|
180
|
+
pkType,
|
|
181
|
+
fields,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return _;
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
GraphQLObjectType_fields(fields, build, context) {
|
|
188
|
+
const { graphql: { GraphQLString, GraphQLObjectType, GraphQLNonNull } } = build;
|
|
189
|
+
const { scope } = context;
|
|
190
|
+
if (!scope.pgCodec || !scope.isPgClassType)
|
|
191
|
+
return fields;
|
|
192
|
+
const codec = scope.pgCodec;
|
|
193
|
+
const info = i18nRegistry[codec.name];
|
|
194
|
+
if (!info)
|
|
195
|
+
return fields;
|
|
196
|
+
const localeFieldsConfig = {
|
|
197
|
+
[langCodeGqlField]: { type: GraphQLString },
|
|
65
198
|
};
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
return build.extend(build, { i18n: { i18nTables, tables } });
|
|
75
|
-
});
|
|
76
|
-
builder.hook('GraphQLObjectType:fields', (fields, build, context) => {
|
|
77
|
-
const { graphql: { GraphQLString, GraphQLObjectType, GraphQLNonNull }, i18n: { i18nTables, tables } } = build;
|
|
78
|
-
const { scope: { pgIntrospection: table, isPgRowType }, fieldWithHooks } = context;
|
|
79
|
-
if (!isPgRowType || !table || table.kind !== 'class') {
|
|
80
|
-
return fields;
|
|
81
|
-
}
|
|
82
|
-
const variationsTableName = tables[table.name];
|
|
83
|
-
if (!variationsTableName) {
|
|
84
|
-
return fields;
|
|
85
|
-
}
|
|
86
|
-
const i18nTable = i18nTables[variationsTableName];
|
|
87
|
-
const { identifier, idType } = i18nTable.keyInfo;
|
|
88
|
-
if (!identifier || !idType) {
|
|
89
|
-
return fields;
|
|
90
|
-
}
|
|
91
|
-
const { key, fields: i18nFields } = i18nTable;
|
|
92
|
-
const localeFieldName = 'localeStrings';
|
|
93
|
-
const localeFieldsConfig = Object.keys(i18nFields).reduce((memo, field) => {
|
|
94
|
-
memo[field] = {
|
|
95
|
-
type: i18nFields[field].isNotNull
|
|
96
|
-
? new GraphQLNonNull(GraphQLString)
|
|
97
|
-
: GraphQLString,
|
|
98
|
-
description: `Locale for ${field}`
|
|
99
|
-
};
|
|
100
|
-
return memo;
|
|
101
|
-
}, {
|
|
102
|
-
langCode: {
|
|
103
|
-
type: GraphQLString
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
const localeFieldsType = new GraphQLObjectType({
|
|
107
|
-
name: `${context.Self.name}LocaleStrings`,
|
|
108
|
-
description: `Locales for ${context.Self.name}`,
|
|
109
|
-
fields: localeFieldsConfig
|
|
110
|
-
});
|
|
111
|
-
return build.extend(fields, {
|
|
112
|
-
[localeFieldName]: fieldWithHooks(localeFieldName, (fieldContext) => {
|
|
113
|
-
const { addDataGenerator } = fieldContext;
|
|
114
|
-
addDataGenerator(() => ({
|
|
115
|
-
pgQuery: (queryBuilder) => {
|
|
116
|
-
queryBuilder.select(build.pgSql.fragment `${queryBuilder.getTableAlias()}.${build.pgSql.identifier(identifier)}`, identifier);
|
|
199
|
+
for (const [gqlName, field] of Object.entries(info.fields)) {
|
|
200
|
+
localeFieldsConfig[gqlName] = {
|
|
201
|
+
type: field.isNotNull ? new GraphQLNonNull(GraphQLString) : GraphQLString,
|
|
202
|
+
};
|
|
117
203
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
});
|
|
125
|
-
const props = {
|
|
126
|
-
table,
|
|
127
|
-
coalescedFields,
|
|
128
|
-
variationsTableName,
|
|
129
|
-
key
|
|
130
|
-
};
|
|
131
|
-
return {
|
|
132
|
-
description: `Locales for ${context.Self.name}`,
|
|
133
|
-
type: new GraphQLNonNull(localeFieldsType),
|
|
134
|
-
async resolve(source, _args, gqlContext) {
|
|
135
|
-
const languageCodes = gqlContext.langCodes ?? langPluginDefaultLanguages;
|
|
136
|
-
const getLoader = gqlContext.getLanguageDataLoader ??
|
|
137
|
-
defaultLanguageLoaderFactory;
|
|
138
|
-
const dataloader = getLoader(props, gqlContext.pgClient, languageCodes, identifier, idType, langPluginLanguageCodeColumn, langPluginLanguageCodeGqlField);
|
|
139
|
-
return dataloader.load(source.id);
|
|
204
|
+
const localeTypeName = `${build.inflection.tableType(codec)}LocaleStrings`;
|
|
205
|
+
if (!localeTypeCache[localeTypeName]) {
|
|
206
|
+
localeTypeCache[localeTypeName] = new GraphQLObjectType({
|
|
207
|
+
name: localeTypeName,
|
|
208
|
+
fields: localeFieldsConfig,
|
|
209
|
+
});
|
|
140
210
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
211
|
+
const localeType = localeTypeCache[localeTypeName];
|
|
212
|
+
const { schemaName, baseTable, translationTable, fkColumn, pkColumn, pkType, fields: i18nFields } = info;
|
|
213
|
+
const coalescedCols = Object.values(i18nFields)
|
|
214
|
+
.map(f => `coalesce(v."${f.column}", b."${f.column}") as "${f.column}"`)
|
|
215
|
+
.join(', ');
|
|
216
|
+
// Build the SQL query template
|
|
217
|
+
const sqlQuery = `SELECT v."${langCodeColumn}" AS "lang_code", ${coalescedCols}
|
|
218
|
+
FROM "${schemaName}"."${baseTable}" b
|
|
219
|
+
LEFT JOIN "${schemaName}"."${translationTable}" v
|
|
220
|
+
ON v."${fkColumn}" = b."${pkColumn}"
|
|
221
|
+
AND array_position($2::text[], v."${langCodeColumn}") IS NOT NULL
|
|
222
|
+
WHERE b."${pkColumn}" = $1::${pkType}
|
|
223
|
+
ORDER BY array_position($2::text[], v."${langCodeColumn}") ASC NULLS LAST
|
|
224
|
+
LIMIT 1`;
|
|
225
|
+
// Build column names list for mapping base values
|
|
226
|
+
const baseColNames = Object.entries(i18nFields).map(([gqlName, f]) => ({
|
|
227
|
+
gqlName,
|
|
228
|
+
column: f.column,
|
|
229
|
+
}));
|
|
230
|
+
return build.extend(fields, {
|
|
231
|
+
localeStrings: {
|
|
232
|
+
type: new GraphQLNonNull(localeType),
|
|
233
|
+
plan($parent) {
|
|
234
|
+
// Extract PK and all base translatable columns from the parent row
|
|
235
|
+
const $id = $parent.get(pkColumn);
|
|
236
|
+
const $baseCols = {};
|
|
237
|
+
for (const { column } of baseColNames) {
|
|
238
|
+
$baseCols[column] = $parent.get(column);
|
|
239
|
+
}
|
|
240
|
+
const $withPgClient = grafastContext().get('withPgClient');
|
|
241
|
+
const $langCodes = grafastContext().get('langCodes');
|
|
242
|
+
// Combine all inputs into a single step
|
|
243
|
+
const $input = object({
|
|
244
|
+
id: $id,
|
|
245
|
+
withPgClient: $withPgClient,
|
|
246
|
+
langCodes: $langCodes,
|
|
247
|
+
...$baseCols,
|
|
248
|
+
});
|
|
249
|
+
return lambda($input, async (input) => {
|
|
250
|
+
const { id, withPgClient, langCodes: ctxLangCodes, ...baseCols } = input;
|
|
251
|
+
const langs = ctxLangCodes ?? defaultLanguages;
|
|
252
|
+
if (!withPgClient || !id) {
|
|
253
|
+
const result = { [langCodeGqlField]: null };
|
|
254
|
+
for (const { gqlName, column } of baseColNames) {
|
|
255
|
+
result[gqlName] = baseCols[column] ?? null;
|
|
256
|
+
}
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
const row = await withPgClient(null, async (client) => {
|
|
260
|
+
const { rows } = await client.query(sqlQuery, [id, langs]);
|
|
261
|
+
return rows[0] ?? null;
|
|
262
|
+
});
|
|
263
|
+
if (!row) {
|
|
264
|
+
const result = { [langCodeGqlField]: null };
|
|
265
|
+
for (const { gqlName, column } of baseColNames) {
|
|
266
|
+
result[gqlName] = baseCols[column] ?? null;
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
const result = { [langCodeGqlField]: row.lang_code };
|
|
271
|
+
for (const { gqlName, column } of baseColNames) {
|
|
272
|
+
result[gqlName] = row[column] ?? null;
|
|
273
|
+
}
|
|
274
|
+
return result;
|
|
275
|
+
});
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
}, 'Adding i18n localeStrings field');
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
export const I18nPlugin = createI18nPlugin();
|
package/esm/preset.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostGraphile v5 i18n Preset
|
|
3
|
+
*
|
|
4
|
+
* Convenience preset that bundles the i18n plugin with configurable options.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { I18nPreset } from 'graphile-i18n';
|
|
9
|
+
*
|
|
10
|
+
* const preset = {
|
|
11
|
+
* extends: [
|
|
12
|
+
* I18nPreset(),
|
|
13
|
+
* ],
|
|
14
|
+
* };
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import { I18nPreset } from 'graphile-i18n';
|
|
20
|
+
*
|
|
21
|
+
* const preset = {
|
|
22
|
+
* extends: [
|
|
23
|
+
* I18nPreset({
|
|
24
|
+
* defaultLanguages: ['en', 'es'],
|
|
25
|
+
* langCodeColumn: 'lang_code',
|
|
26
|
+
* }),
|
|
27
|
+
* ],
|
|
28
|
+
* };
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
32
|
+
import type { I18nPluginOptions } from './types';
|
|
33
|
+
export declare function I18nPreset(options?: I18nPluginOptions): GraphileConfig.Preset;
|
|
34
|
+
export default I18nPreset;
|
package/esm/preset.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostGraphile v5 i18n Preset
|
|
3
|
+
*
|
|
4
|
+
* Convenience preset that bundles the i18n plugin with configurable options.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { I18nPreset } from 'graphile-i18n';
|
|
9
|
+
*
|
|
10
|
+
* const preset = {
|
|
11
|
+
* extends: [
|
|
12
|
+
* I18nPreset(),
|
|
13
|
+
* ],
|
|
14
|
+
* };
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import { I18nPreset } from 'graphile-i18n';
|
|
20
|
+
*
|
|
21
|
+
* const preset = {
|
|
22
|
+
* extends: [
|
|
23
|
+
* I18nPreset({
|
|
24
|
+
* defaultLanguages: ['en', 'es'],
|
|
25
|
+
* langCodeColumn: 'lang_code',
|
|
26
|
+
* }),
|
|
27
|
+
* ],
|
|
28
|
+
* };
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
import { createI18nPlugin } from './plugin';
|
|
32
|
+
export function I18nPreset(options = {}) {
|
|
33
|
+
return {
|
|
34
|
+
plugins: [createI18nPlugin(options)],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export default I18nPreset;
|
package/esm/types.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the graphile-i18n v5 plugin.
|
|
3
|
+
*/
|
|
4
|
+
export interface I18nPluginOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Column name on the translation table that stores the language code.
|
|
7
|
+
* @default 'lang_code'
|
|
8
|
+
*/
|
|
9
|
+
langCodeColumn?: string;
|
|
10
|
+
/**
|
|
11
|
+
* GraphQL field name for the language code in the locale object.
|
|
12
|
+
* @default 'langCode'
|
|
13
|
+
*/
|
|
14
|
+
langCodeGqlField?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Column types eligible for translation overlay.
|
|
17
|
+
* @default ['text', 'citext']
|
|
18
|
+
*/
|
|
19
|
+
allowedTypes?: string[];
|
|
20
|
+
/**
|
|
21
|
+
* Fallback language codes when no Accept-Language header is provided.
|
|
22
|
+
* @default ['en']
|
|
23
|
+
*/
|
|
24
|
+
defaultLanguages?: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface I18nTableInfo {
|
|
27
|
+
/** Base table name */
|
|
28
|
+
baseTable: string;
|
|
29
|
+
/** Translation table name */
|
|
30
|
+
translationTable: string;
|
|
31
|
+
/** Schema name */
|
|
32
|
+
schemaName: string;
|
|
33
|
+
/** FK column on the translation table referencing the base table PK */
|
|
34
|
+
fkColumn: string;
|
|
35
|
+
/** Base table PK column name */
|
|
36
|
+
pkColumn: string;
|
|
37
|
+
/** Base table PK PostgreSQL type */
|
|
38
|
+
pkType: string;
|
|
39
|
+
/** Translatable field mappings: { gqlFieldName: { column, type, isNotNull } } */
|
|
40
|
+
fields: Record<string, TranslatableField>;
|
|
41
|
+
}
|
|
42
|
+
export interface TranslatableField {
|
|
43
|
+
column: string;
|
|
44
|
+
type: string;
|
|
45
|
+
isNotNull: boolean;
|
|
46
|
+
}
|
package/esm/types.js
ADDED
package/index.d.ts
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* graphile-i18n — PostGraphile v5 i18n Plugin
|
|
3
|
+
*
|
|
4
|
+
* Language-aware fields sourced from @i18n translation tables
|
|
5
|
+
* with Accept-Language negotiation and configurable fallback chains.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { I18nPreset } from 'graphile-i18n';
|
|
10
|
+
*
|
|
11
|
+
* const preset = {
|
|
12
|
+
* extends: [
|
|
13
|
+
* I18nPreset(),
|
|
14
|
+
* ],
|
|
15
|
+
* };
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { I18nPreset } from 'graphile-i18n';
|
|
21
|
+
*
|
|
22
|
+
* const preset = {
|
|
23
|
+
* extends: [
|
|
24
|
+
* I18nPreset({
|
|
25
|
+
* defaultLanguages: ['en', 'es'],
|
|
26
|
+
* langCodeColumn: 'lang_code',
|
|
27
|
+
* allowedTypes: ['text', 'citext'],
|
|
28
|
+
* }),
|
|
29
|
+
* ],
|
|
30
|
+
* };
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export { createI18nPlugin, I18nPlugin } from './plugin';
|
|
34
|
+
export { I18nPreset } from './preset';
|
|
35
|
+
export { makeI18nContext, additionalGraphQLContextFromRequest } from './middleware';
|
|
36
|
+
export type { I18nPluginOptions, I18nTableInfo, TranslatableField } from './types';
|
|
37
|
+
export type { I18nMiddlewareOptions } from './middleware';
|
package/index.js
CHANGED
|
@@ -1,15 +1,46 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/**
|
|
3
|
+
* graphile-i18n — PostGraphile v5 i18n Plugin
|
|
4
|
+
*
|
|
5
|
+
* Language-aware fields sourced from @i18n translation tables
|
|
6
|
+
* with Accept-Language negotiation and configurable fallback chains.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { I18nPreset } from 'graphile-i18n';
|
|
11
|
+
*
|
|
12
|
+
* const preset = {
|
|
13
|
+
* extends: [
|
|
14
|
+
* I18nPreset(),
|
|
15
|
+
* ],
|
|
16
|
+
* };
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { I18nPreset } from 'graphile-i18n';
|
|
22
|
+
*
|
|
23
|
+
* const preset = {
|
|
24
|
+
* extends: [
|
|
25
|
+
* I18nPreset({
|
|
26
|
+
* defaultLanguages: ['en', 'es'],
|
|
27
|
+
* langCodeColumn: 'lang_code',
|
|
28
|
+
* allowedTypes: ['text', 'citext'],
|
|
29
|
+
* }),
|
|
30
|
+
* ],
|
|
31
|
+
* };
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
5
34
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
7
|
-
|
|
8
|
-
var
|
|
9
|
-
Object.defineProperty(exports, "
|
|
35
|
+
exports.additionalGraphQLContextFromRequest = exports.makeI18nContext = exports.I18nPreset = exports.I18nPlugin = exports.createI18nPlugin = void 0;
|
|
36
|
+
// Plugin
|
|
37
|
+
var plugin_1 = require("./plugin");
|
|
38
|
+
Object.defineProperty(exports, "createI18nPlugin", { enumerable: true, get: function () { return plugin_1.createI18nPlugin; } });
|
|
39
|
+
Object.defineProperty(exports, "I18nPlugin", { enumerable: true, get: function () { return plugin_1.I18nPlugin; } });
|
|
40
|
+
// Preset
|
|
41
|
+
var preset_1 = require("./preset");
|
|
42
|
+
Object.defineProperty(exports, "I18nPreset", { enumerable: true, get: function () { return preset_1.I18nPreset; } });
|
|
43
|
+
// Middleware
|
|
10
44
|
var middleware_1 = require("./middleware");
|
|
45
|
+
Object.defineProperty(exports, "makeI18nContext", { enumerable: true, get: function () { return middleware_1.makeI18nContext; } });
|
|
11
46
|
Object.defineProperty(exports, "additionalGraphQLContextFromRequest", { enumerable: true, get: function () { return middleware_1.additionalGraphQLContextFromRequest; } });
|
|
12
|
-
Object.defineProperty(exports, "makeLanguageDataLoaderForTable", { enumerable: true, get: function () { return middleware_1.makeLanguageDataLoaderForTable; } });
|
|
13
|
-
var env_1 = require("./env");
|
|
14
|
-
Object.defineProperty(exports, "env", { enumerable: true, get: function () { return env_1.env; } });
|
|
15
|
-
exports.default = plugin_1.default;
|