graphile-i18n 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Dan Lynch <pyramation@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # graphile-i18n [![Build Status](https://travis-ci.com/pyramation/graphile-i18n.svg?branch=master)](https://travis-ci.com/pyramation/graphile-i18n)
2
+
3
+ ```sh
4
+ npm install graphile-i18n
5
+ ```
6
+
7
+ This [PostGraphile](http://postgraphile.org/) schema plugin was built to enable i18n language translation tables.
8
+
9
+ ## Usage
10
+
11
+ 1. Create a language translation table
12
+ 2. Add smart comments
13
+ 3. Register plugin with postgraphile
14
+
15
+ ## language table
16
+
17
+ Add language table with `lang_code` field and a smart comment for `i18n`:
18
+
19
+ ```sql
20
+ CREATE TABLE app_public.projects (
21
+ id serial PRIMARY KEY,
22
+ name citext,
23
+ description citext
24
+ );
25
+ COMMENT ON TABLE app_public.projects IS E'@i18n project_language_variations';
26
+
27
+ CREATE TABLE app_public.project_language_variations (
28
+ id serial PRIMARY KEY,
29
+ project_id int NOT NULL REFERENCES app_public.projects(id),
30
+ lang_code citext,
31
+ name citext,
32
+ description citext,
33
+ UNIQUE(project_id, lang_code)
34
+ );
35
+ ```
36
+
37
+ ## Register Plugin
38
+
39
+ ```js
40
+ app.use(
41
+ postgraphile(connectionStr, schemas, {
42
+ appendPlugins: [
43
+ LangPlugin
44
+ ],
45
+ graphileBuildOptions: {
46
+ langPluginDefaultLanguages: ['en']
47
+ }
48
+ })
49
+ );
50
+ ```
51
+
52
+ ## testing
53
+
54
+ ```
55
+ createdb test_database
56
+ psql test_database < sql/roles.sql
57
+ psql test_database < sql/test.sql
58
+ yarn test
59
+ ```
package/main/env.js ADDED
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports["default"] = void 0;
7
+
8
+ var _envalid = require("envalid");
9
+
10
+ var array = (0, _envalid.makeValidator)(function (x) {
11
+ return x.split(',');
12
+ }, '');
13
+
14
+ var _default = (0, _envalid.cleanEnv)(process.env, {
15
+ ACCEPTED_LANGUAGES: array({
16
+ "default": 'en,es'
17
+ })
18
+ }, {
19
+ dotEnvPath: null
20
+ });
21
+
22
+ exports["default"] = _default;
package/main/index.js ADDED
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ var _exportNames = {};
7
+ exports["default"] = void 0;
8
+
9
+ var _plugin = require("./plugin");
10
+
11
+ Object.keys(_plugin).forEach(function (key) {
12
+ if (key === "default" || key === "__esModule") return;
13
+ if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
14
+ if (key in exports && exports[key] === _plugin[key]) return;
15
+ Object.defineProperty(exports, key, {
16
+ enumerable: true,
17
+ get: function get() {
18
+ return _plugin[key];
19
+ }
20
+ });
21
+ });
22
+
23
+ var _middleware = require("./middleware");
24
+
25
+ Object.keys(_middleware).forEach(function (key) {
26
+ if (key === "default" || key === "__esModule") return;
27
+ if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
28
+ if (key in exports && exports[key] === _middleware[key]) return;
29
+ Object.defineProperty(exports, key, {
30
+ enumerable: true,
31
+ get: function get() {
32
+ return _middleware[key];
33
+ }
34
+ });
35
+ });
36
+ var _default = _plugin.LangPlugin;
37
+ exports["default"] = _default;
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.makeLanguageDataLoaderForTable = exports.additionalGraphQLContextFromRequest = void 0;
9
+
10
+ var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
11
+
12
+ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
13
+
14
+ var _dataloader = _interopRequireDefault(require("dataloader"));
15
+
16
+ var _acceptLanguageParser = _interopRequireDefault(require("accept-language-parser"));
17
+
18
+ var _env = require("./env");
19
+
20
+ var escapeIdentifier = function escapeIdentifier(str) {
21
+ return "\"".concat(str.replace(/"/g, '""'), "\"");
22
+ };
23
+
24
+ var makeLanguageDataLoaderForTable = function makeLanguageDataLoaderForTable(_req) {
25
+ var cache = new Map();
26
+ return function (props, pgClient, languageCodes, identifer, idType, sqlField, gqlField) {
27
+ var dataLoader = cache.get(props);
28
+
29
+ if (!dataLoader) {
30
+ var table = props.table,
31
+ coalescedFields = props.coalescedFields,
32
+ variationsTableName = props.variationsTableName,
33
+ key = props.key;
34
+ var schemaName = escapeIdentifier(table.namespaceName);
35
+ var baseTable = escapeIdentifier(table.name);
36
+ var variationTable = escapeIdentifier(variationsTableName);
37
+ var joinKey = escapeIdentifier(key);
38
+ var fields = coalescedFields.join(', ');
39
+ var b = [schemaName, baseTable].join('.');
40
+ var v = [schemaName, variationTable].join('.');
41
+ dataLoader = new _dataloader["default"]( /*#__PURE__*/function () {
42
+ var _ref = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(ids) {
43
+ var _yield$pgClient$query, rows;
44
+
45
+ return _regenerator["default"].wrap(function _callee$(_context) {
46
+ while (1) {
47
+ switch (_context.prev = _context.next) {
48
+ case 0:
49
+ _context.next = 2;
50
+ return pgClient.query("\n select *\n from unnest($1::".concat(idType, "[]) ids(").concat(identifer, ")\n inner join lateral (\n select b.").concat(identifer, ", v.").concat(sqlField, " as \"").concat(gqlField, "\", ").concat(fields, "\n from ").concat(b, " b\n left join ").concat(v, " v\n on (v.").concat(joinKey, " = b.").concat(identifer, " and array_position($2, ").concat(sqlField, ") is not null)\n where b.").concat(identifer, " = ids.").concat(identifer, "\n order by array_position($2, ").concat(sqlField, ") asc nulls last\n limit 1\n ) tmp on (true)\n "), [ids, languageCodes]);
51
+
52
+ case 2:
53
+ _yield$pgClient$query = _context.sent;
54
+ rows = _yield$pgClient$query.rows;
55
+ return _context.abrupt("return", ids.map(function (id) {
56
+ return rows.find(function (r) {
57
+ return r.id === id;
58
+ });
59
+ }));
60
+
61
+ case 5:
62
+ case "end":
63
+ return _context.stop();
64
+ }
65
+ }
66
+ }, _callee);
67
+ }));
68
+
69
+ return function (_x) {
70
+ return _ref.apply(this, arguments);
71
+ };
72
+ }());
73
+ cache.set(props, dataLoader);
74
+ }
75
+
76
+ return dataLoader;
77
+ };
78
+ };
79
+
80
+ exports.makeLanguageDataLoaderForTable = makeLanguageDataLoaderForTable;
81
+
82
+ var additionalGraphQLContextFromRequest = function additionalGraphQLContextFromRequest(req, res) {
83
+ var language = _acceptLanguageParser["default"].pick(_env.ACCEPTED_LANGUAGES, req.get('accept-language')); // Accept-Language: *
84
+ // Accept-Language: en-US,en;q=0.5
85
+ // Accept-Language: fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5
86
+
87
+
88
+ return {
89
+ langCodes: [language],
90
+ // in future make fallback languages
91
+ getLanguageDataLoader: makeLanguageDataLoaderForTable(req)
92
+ };
93
+ };
94
+
95
+ exports.additionalGraphQLContextFromRequest = additionalGraphQLContextFromRequest;
package/main/plugin.js ADDED
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.LangPlugin = void 0;
9
+
10
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
11
+
12
+ var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
13
+
14
+ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
15
+
16
+ var _taggedTemplateLiteral2 = _interopRequireDefault(require("@babel/runtime/helpers/taggedTemplateLiteral"));
17
+
18
+ var _templateObject;
19
+
20
+ var escapeIdentifier = function escapeIdentifier(str) {
21
+ return "\"".concat(str.replace(/"/g, '""'), "\"");
22
+ };
23
+
24
+ var LangPlugin = function LangPlugin(builder, options) {
25
+ var _options$langPluginLa = options.langPluginLanguageCodeColumn,
26
+ langPluginLanguageCodeColumn = _options$langPluginLa === void 0 ? 'lang_code' : _options$langPluginLa,
27
+ _options$langPluginLa2 = options.langPluginLanguageCodeGqlField,
28
+ langPluginLanguageCodeGqlField = _options$langPluginLa2 === void 0 ? 'langCode' : _options$langPluginLa2,
29
+ _options$langPluginAl = options.langPluginAllowedTypes,
30
+ langPluginAllowedTypes = _options$langPluginAl === void 0 ? ['citext', 'text'] : _options$langPluginAl,
31
+ _options$langPluginDe = options.langPluginDefaultLanguages,
32
+ langPluginDefaultLanguages = _options$langPluginDe === void 0 ? ['en'] : _options$langPluginDe;
33
+ builder.hook('build', function (build) {
34
+ var sql = build.pgSql;
35
+ /** @type {import('graphile-build-pg').PgIntrospectionResultsByKind} */
36
+
37
+ var introspection = build.pgIntrospectionResultsByKind;
38
+ var inflection = build.inflection;
39
+ var tablesWithLanguageTables = introspection["class"].filter(function (table) {
40
+ return table.tags.hasOwnProperty('i18n');
41
+ });
42
+ var tablesWithLanguageTablesIdInfo = introspection["class"].filter(function (table) {
43
+ return table.tags.hasOwnProperty('i18n');
44
+ }).map(function (table) {
45
+ return {
46
+ identifier: table.primaryKeyConstraint.keyAttributes[0].name,
47
+ idType: table.primaryKeyConstraint.keyAttributes[0].type.name
48
+ };
49
+ });
50
+ var languageVariationTables = tablesWithLanguageTables.map(function (table) {
51
+ return introspection["class"].find(function (t) {
52
+ return t.name === table.tags.i18n && t.namespaceName === table.namespaceName;
53
+ });
54
+ });
55
+ var i18nTables = {};
56
+ var tables = {};
57
+ tablesWithLanguageTables.forEach(function (table, i) {
58
+ i18nTables[table.tags.i18n] = {
59
+ table: table.name,
60
+ key: null,
61
+ // action_id
62
+ connection: inflection.connection(inflection.tableType(table)),
63
+ attrs: {},
64
+ fields: {},
65
+ keyInfo: tablesWithLanguageTablesIdInfo[i]
66
+ };
67
+ tables[table.name] = table.tags.i18n;
68
+ });
69
+ languageVariationTables.forEach(function (table) {
70
+ var foreignConstraintsThatMatter = table.constraints.filter(function (c) {
71
+ return c.type === 'f';
72
+ }).filter(function (c) {
73
+ return c.foreignClass.name === i18nTables[table.name].table;
74
+ });
75
+ if (foreignConstraintsThatMatter.length !== 1) throw new Error('lang table only supports one foreign key to parent table');
76
+ if (foreignConstraintsThatMatter[0].keyAttributes.length !== 1) throw new Error('lang table only supports one non compound foreign key to parent table');
77
+ i18nTables[table.name].key = foreignConstraintsThatMatter[0].keyAttributes[0].name;
78
+ var _i18nTables$table$nam = i18nTables[table.name].keyInfo,
79
+ identifier = _i18nTables$table$nam.identifier,
80
+ idType = _i18nTables$table$nam.idType;
81
+ table.attributes.forEach(function (attr) {
82
+ if ([langPluginLanguageCodeColumn, identifier].includes(attr.name)) return;
83
+
84
+ if (langPluginAllowedTypes.includes(attr.type.name)) {
85
+ i18nTables[table.name].fields[inflection.column(attr)] = {
86
+ type: attr.type.name,
87
+ attr: attr.name,
88
+ isNotNull: attr.isNotNull,
89
+ column: inflection.column(attr)
90
+ };
91
+ i18nTables[table.name].attrs[attr.name] = {
92
+ type: attr.type.name,
93
+ attr: attr.name,
94
+ column: inflection.column(attr)
95
+ };
96
+ }
97
+ });
98
+ });
99
+ return build.extend(build, {
100
+ i18n: {
101
+ i18nTables: i18nTables,
102
+ tables: tables
103
+ }
104
+ });
105
+ });
106
+ builder.hook('GraphQLObjectType:fields', function (fields, build, context) {
107
+ var _build$graphql = build.graphql,
108
+ GraphQLString = _build$graphql.GraphQLString,
109
+ GraphQLObjectType = _build$graphql.GraphQLObjectType,
110
+ GraphQLNonNull = _build$graphql.GraphQLNonNull,
111
+ _build$i18n = build.i18n,
112
+ i18nTables = _build$i18n.i18nTables,
113
+ tables = _build$i18n.tables,
114
+ sql = build.pgSql;
115
+ var _context$scope = context.scope,
116
+ table = _context$scope.pgIntrospection,
117
+ isPgRowType = _context$scope.isPgRowType,
118
+ fieldWithHooks = context.fieldWithHooks;
119
+
120
+ if (!isPgRowType || !table || table.kind !== 'class') {
121
+ return fields;
122
+ }
123
+
124
+ var variationsTableName = tables[table.name];
125
+
126
+ if (!variationsTableName) {
127
+ return fields;
128
+ }
129
+
130
+ var i18nTable = i18nTables[variationsTableName];
131
+ var _i18nTable$keyInfo = i18nTable.keyInfo,
132
+ identifier = _i18nTable$keyInfo.identifier,
133
+ idType = _i18nTable$keyInfo.idType;
134
+ var key = i18nTable.key,
135
+ connection = i18nTable.connection,
136
+ attrs = i18nTable.attrs,
137
+ i18nFields = i18nTable.fields;
138
+ var localeFieldName = 'localeStrings';
139
+ var localeFieldsType = new GraphQLObjectType({
140
+ name: "".concat(context.Self.name, "LocaleStrings"),
141
+ description: "Locales for ".concat(context.Self.name),
142
+ fields: Object.keys(i18nFields).reduce(function (memo, field) {
143
+ memo[field] = {
144
+ type: i18nFields[field].isNotNull ? new GraphQLNonNull(GraphQLString) : GraphQLString,
145
+ description: "Locale for ".concat(field)
146
+ };
147
+ return memo;
148
+ }, {
149
+ langCode: {
150
+ type: GraphQLString // MUST BE NULLABLE
151
+
152
+ }
153
+ })
154
+ });
155
+ return build.extend(fields, (0, _defineProperty2["default"])({}, localeFieldName, fieldWithHooks(localeFieldName, function (fieldContext) {
156
+ var addDataGenerator = fieldContext.addDataGenerator;
157
+ addDataGenerator(function (parsedResolveInfoFragment) {
158
+ return {
159
+ pgQuery: function pgQuery(queryBuilder) {
160
+ queryBuilder.select(sql.fragment(_templateObject || (_templateObject = (0, _taggedTemplateLiteral2["default"])(["", ".", ""])), queryBuilder.getTableAlias(), sql.identifier(identifier)), identifier);
161
+ }
162
+ };
163
+ });
164
+ var coalescedFields = Object.keys(i18nFields).map(function (field) {
165
+ var columnName = i18nFields[field].attr;
166
+ var escColumnName = escapeIdentifier(columnName);
167
+ var escFieldName = escapeIdentifier(field);
168
+ return "coalesce(v.".concat(escColumnName, ", b.").concat(escColumnName, ") as ").concat(escFieldName);
169
+ });
170
+ var props = {
171
+ table: table,
172
+ coalescedFields: coalescedFields,
173
+ variationsTableName: variationsTableName,
174
+ key: key
175
+ };
176
+ return {
177
+ description: "Locales for ".concat(context.Self.name),
178
+ type: new GraphQLNonNull(localeFieldsType),
179
+ resolve: function resolve(_ref, args, context) {
180
+ return (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee() {
181
+ var _context$langCodes;
182
+
183
+ var id, languageCodes, dataloader;
184
+ return _regenerator["default"].wrap(function _callee$(_context) {
185
+ while (1) {
186
+ switch (_context.prev = _context.next) {
187
+ case 0:
188
+ id = _ref.id;
189
+ languageCodes = (_context$langCodes = context.langCodes) !== null && _context$langCodes !== void 0 ? _context$langCodes : langPluginDefaultLanguages;
190
+ dataloader = context.getLanguageDataLoader(props, context.pgClient, languageCodes, identifier, idType, langPluginLanguageCodeColumn, langPluginLanguageCodeGqlField);
191
+ return _context.abrupt("return", dataloader.load(id));
192
+
193
+ case 4:
194
+ case "end":
195
+ return _context.stop();
196
+ }
197
+ }
198
+ }, _callee);
199
+ }))();
200
+ }
201
+ };
202
+ }, 'Adding the language code field from the lang plugin')));
203
+ });
204
+ };
205
+
206
+ exports.LangPlugin = LangPlugin;
package/module/env.js ADDED
@@ -0,0 +1,9 @@
1
+ import { cleanEnv, makeValidator } from 'envalid';
2
+ const array = makeValidator(x => x.split(','), '');
3
+ export default cleanEnv(process.env, {
4
+ ACCEPTED_LANGUAGES: array({
5
+ default: 'en,es'
6
+ })
7
+ }, {
8
+ dotEnvPath: null
9
+ });
@@ -0,0 +1,4 @@
1
+ export * from './plugin';
2
+ export * from './middleware';
3
+ import { LangPlugin } from './plugin';
4
+ export default LangPlugin;
@@ -0,0 +1,60 @@
1
+ import DataLoader from 'dataloader';
2
+ import langParser from 'accept-language-parser';
3
+ import { ACCEPTED_LANGUAGES } from './env';
4
+
5
+ const escapeIdentifier = str => `"${str.replace(/"/g, '""')}"`;
6
+
7
+ export const makeLanguageDataLoaderForTable = _req => {
8
+ const cache = new Map();
9
+ return (props, pgClient, languageCodes, identifer, idType, sqlField, gqlField) => {
10
+ let dataLoader = cache.get(props);
11
+
12
+ if (!dataLoader) {
13
+ const {
14
+ table,
15
+ coalescedFields,
16
+ variationsTableName,
17
+ key
18
+ } = props;
19
+ const schemaName = escapeIdentifier(table.namespaceName);
20
+ const baseTable = escapeIdentifier(table.name);
21
+ const variationTable = escapeIdentifier(variationsTableName);
22
+ const joinKey = escapeIdentifier(key);
23
+ const fields = coalescedFields.join(', ');
24
+ const b = [schemaName, baseTable].join('.');
25
+ const v = [schemaName, variationTable].join('.');
26
+ dataLoader = new DataLoader(async ids => {
27
+ const {
28
+ rows
29
+ } = await pgClient.query(`
30
+ select *
31
+ from unnest($1::${idType}[]) ids(${identifer})
32
+ inner join lateral (
33
+ select b.${identifer}, v.${sqlField} as "${gqlField}", ${fields}
34
+ from ${b} b
35
+ left join ${v} v
36
+ on (v.${joinKey} = b.${identifer} and array_position($2, ${sqlField}) is not null)
37
+ where b.${identifer} = ids.${identifer}
38
+ order by array_position($2, ${sqlField}) asc nulls last
39
+ limit 1
40
+ ) tmp on (true)
41
+ `, [ids, languageCodes]);
42
+ return ids.map(id => rows.find(r => r.id === id));
43
+ });
44
+ cache.set(props, dataLoader);
45
+ }
46
+
47
+ return dataLoader;
48
+ };
49
+ };
50
+ export const additionalGraphQLContextFromRequest = (req, res) => {
51
+ const language = langParser.pick(ACCEPTED_LANGUAGES, req.get('accept-language')); // Accept-Language: *
52
+ // Accept-Language: en-US,en;q=0.5
53
+ // Accept-Language: fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5
54
+
55
+ return {
56
+ langCodes: [language],
57
+ // in future make fallback languages
58
+ getLanguageDataLoader: makeLanguageDataLoaderForTable(req)
59
+ };
60
+ };
@@ -0,0 +1,173 @@
1
+ const escapeIdentifier = str => `"${str.replace(/"/g, '""')}"`;
2
+
3
+ export const LangPlugin = (builder, options) => {
4
+ const {
5
+ langPluginLanguageCodeColumn = 'lang_code',
6
+ langPluginLanguageCodeGqlField = 'langCode',
7
+ langPluginAllowedTypes = ['citext', 'text'],
8
+ langPluginDefaultLanguages = ['en']
9
+ } = options;
10
+ builder.hook('build', build => {
11
+ const {
12
+ pgSql: sql
13
+ } = build;
14
+ /** @type {import('graphile-build-pg').PgIntrospectionResultsByKind} */
15
+
16
+ const introspection = build.pgIntrospectionResultsByKind;
17
+ const inflection = build.inflection;
18
+ const tablesWithLanguageTables = introspection.class.filter(table => table.tags.hasOwnProperty('i18n'));
19
+ const tablesWithLanguageTablesIdInfo = introspection.class.filter(table => table.tags.hasOwnProperty('i18n')).map(table => {
20
+ return {
21
+ identifier: table.primaryKeyConstraint.keyAttributes[0].name,
22
+ idType: table.primaryKeyConstraint.keyAttributes[0].type.name
23
+ };
24
+ });
25
+ const languageVariationTables = tablesWithLanguageTables.map(table => introspection.class.find(t => t.name === table.tags.i18n && t.namespaceName === table.namespaceName));
26
+ const i18nTables = {};
27
+ const tables = {};
28
+ tablesWithLanguageTables.forEach((table, i) => {
29
+ i18nTables[table.tags.i18n] = {
30
+ table: table.name,
31
+ key: null,
32
+ // action_id
33
+ connection: inflection.connection(inflection.tableType(table)),
34
+ attrs: {},
35
+ fields: {},
36
+ keyInfo: tablesWithLanguageTablesIdInfo[i]
37
+ };
38
+ tables[table.name] = table.tags.i18n;
39
+ });
40
+ languageVariationTables.forEach(table => {
41
+ const foreignConstraintsThatMatter = table.constraints.filter(c => c.type === 'f').filter(c => c.foreignClass.name === i18nTables[table.name].table);
42
+ if (foreignConstraintsThatMatter.length !== 1) throw new Error('lang table only supports one foreign key to parent table');
43
+ if (foreignConstraintsThatMatter[0].keyAttributes.length !== 1) throw new Error('lang table only supports one non compound foreign key to parent table');
44
+ i18nTables[table.name].key = foreignConstraintsThatMatter[0].keyAttributes[0].name;
45
+ const {
46
+ identifier,
47
+ idType
48
+ } = i18nTables[table.name].keyInfo;
49
+ table.attributes.forEach(attr => {
50
+ if ([langPluginLanguageCodeColumn, identifier].includes(attr.name)) return;
51
+
52
+ if (langPluginAllowedTypes.includes(attr.type.name)) {
53
+ i18nTables[table.name].fields[inflection.column(attr)] = {
54
+ type: attr.type.name,
55
+ attr: attr.name,
56
+ isNotNull: attr.isNotNull,
57
+ column: inflection.column(attr)
58
+ };
59
+ i18nTables[table.name].attrs[attr.name] = {
60
+ type: attr.type.name,
61
+ attr: attr.name,
62
+ column: inflection.column(attr)
63
+ };
64
+ }
65
+ });
66
+ });
67
+ return build.extend(build, {
68
+ i18n: {
69
+ i18nTables,
70
+ tables
71
+ }
72
+ });
73
+ });
74
+ builder.hook('GraphQLObjectType:fields', (fields, build, context) => {
75
+ const {
76
+ graphql: {
77
+ GraphQLString,
78
+ GraphQLObjectType,
79
+ GraphQLNonNull
80
+ },
81
+ i18n: {
82
+ i18nTables,
83
+ tables
84
+ },
85
+ pgSql: sql
86
+ } = build;
87
+ const {
88
+ scope: {
89
+ pgIntrospection: table,
90
+ isPgRowType
91
+ },
92
+ fieldWithHooks
93
+ } = context;
94
+
95
+ if (!isPgRowType || !table || table.kind !== 'class') {
96
+ return fields;
97
+ }
98
+
99
+ const variationsTableName = tables[table.name];
100
+
101
+ if (!variationsTableName) {
102
+ return fields;
103
+ }
104
+
105
+ const i18nTable = i18nTables[variationsTableName];
106
+ const {
107
+ identifier,
108
+ idType
109
+ } = i18nTable.keyInfo;
110
+ const {
111
+ key,
112
+ connection,
113
+ attrs,
114
+ fields: i18nFields
115
+ } = i18nTable;
116
+ const localeFieldName = 'localeStrings';
117
+ const localeFieldsType = new GraphQLObjectType({
118
+ name: `${context.Self.name}LocaleStrings`,
119
+ description: `Locales for ${context.Self.name}`,
120
+ fields: Object.keys(i18nFields).reduce((memo, field) => {
121
+ memo[field] = {
122
+ type: i18nFields[field].isNotNull ? new GraphQLNonNull(GraphQLString) : GraphQLString,
123
+ description: `Locale for ${field}`
124
+ };
125
+ return memo;
126
+ }, {
127
+ langCode: {
128
+ type: GraphQLString // MUST BE NULLABLE
129
+
130
+ }
131
+ })
132
+ });
133
+ return build.extend(fields, {
134
+ [localeFieldName]: fieldWithHooks(localeFieldName, fieldContext => {
135
+ const {
136
+ addDataGenerator
137
+ } = fieldContext;
138
+ addDataGenerator(parsedResolveInfoFragment => {
139
+ return {
140
+ pgQuery: queryBuilder => {
141
+ queryBuilder.select(sql.fragment`${queryBuilder.getTableAlias()}.${sql.identifier(identifier)}`, identifier);
142
+ }
143
+ };
144
+ });
145
+ const coalescedFields = Object.keys(i18nFields).map(field => {
146
+ const columnName = i18nFields[field].attr;
147
+ const escColumnName = escapeIdentifier(columnName);
148
+ const escFieldName = escapeIdentifier(field);
149
+ return `coalesce(v.${escColumnName}, b.${escColumnName}) as ${escFieldName}`;
150
+ });
151
+ const props = {
152
+ table,
153
+ coalescedFields,
154
+ variationsTableName,
155
+ key
156
+ };
157
+ return {
158
+ description: `Locales for ${context.Self.name}`,
159
+ type: new GraphQLNonNull(localeFieldsType),
160
+
161
+ async resolve({
162
+ id
163
+ }, args, context) {
164
+ const languageCodes = context.langCodes ?? langPluginDefaultLanguages;
165
+ const dataloader = context.getLanguageDataLoader(props, context.pgClient, languageCodes, identifier, idType, langPluginLanguageCodeColumn, langPluginLanguageCodeGqlField);
166
+ return dataloader.load(id);
167
+ }
168
+
169
+ };
170
+ }, 'Adding the language code field from the lang plugin')
171
+ });
172
+ });
173
+ };
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "graphile-i18n",
3
+ "version": "0.0.1",
4
+ "description": "Graphile i18n plugin",
5
+ "author": "true <true>",
6
+ "homepage": "https://github.com/pyramation/graphile-i18n#readme",
7
+ "license": "SEE LICENSE IN LICENSE",
8
+ "main": "main/index.js",
9
+ "module": "module/index.js",
10
+ "directories": {
11
+ "lib": "src",
12
+ "test": "__tests__"
13
+ },
14
+ "files": [
15
+ "main",
16
+ "module"
17
+ ],
18
+ "scripts": {
19
+ "build:main": "cross-env BABEL_ENV=production babel src --out-dir main --delete-dir-on-start",
20
+ "build:module": "cross-env MODULE=true babel src --out-dir module --delete-dir-on-start",
21
+ "build": "npm run build:module && npm run build:main",
22
+ "prepublish": "npm run build",
23
+ "dev": "cross-env NODE_ENV=development babel-node src/index",
24
+ "watch": "cross-env NODE_ENV=development babel-watch src/index",
25
+ "lint": "eslint src --fix",
26
+ "test": "jest",
27
+ "test:watch": "jest --watch",
28
+ "test:debug": "node --inspect node_modules/.bin/jest --runInBand"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/pyramation/graphile-i18n"
36
+ },
37
+ "keywords": [
38
+ "postgraphile",
39
+ "graphile",
40
+ "launchql",
41
+ "plugin",
42
+ "postgres",
43
+ "graphql"
44
+ ],
45
+ "resolutions": {
46
+ "graphql": "15.5.2"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/pyramation/graphile-i18n/issues"
50
+ },
51
+ "devDependencies": {
52
+ "@babel/cli": "7.11.6",
53
+ "@babel/core": "7.11.6",
54
+ "@babel/node": "^7.10.5",
55
+ "@babel/plugin-proposal-class-properties": "7.10.4",
56
+ "@babel/plugin-proposal-export-default-from": "7.10.4",
57
+ "@babel/plugin-proposal-object-rest-spread": "7.11.0",
58
+ "@babel/plugin-transform-runtime": "7.11.5",
59
+ "@babel/preset-env": "7.11.5",
60
+ "babel-core": "7.0.0-bridge.0",
61
+ "babel-eslint": "10.1.0",
62
+ "babel-jest": "25.1.0",
63
+ "babel-watch": "^7.0.0",
64
+ "cross-env": "^7.0.2",
65
+ "eslint": "6.8.0",
66
+ "eslint-config-prettier": "^6.10.0",
67
+ "eslint-plugin-prettier": "^3.1.2",
68
+ "graphile-test": "^0.1.1",
69
+ "jest": "^24.5.0",
70
+ "jest-in-case": "^1.0.2",
71
+ "prettier": "^2.1.2",
72
+ "regenerator-runtime": "^0.13.7"
73
+ },
74
+ "dependencies": {
75
+ "@babel/runtime": "^7.11.2",
76
+ "accept-language-parser": "^1.5.0",
77
+ "dataloader": "^2.0.0",
78
+ "envalid": "^6.0.2",
79
+ "graphile-build": "^4.9.0",
80
+ "graphile-utils": "^4.9.0"
81
+ }
82
+ }