introspectron 0.2.12 → 2.0.2

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/process.js ADDED
@@ -0,0 +1,278 @@
1
+ // @ts-nocheck
2
+ import { deepClone, parseTags } from './utils';
3
+ const removeQuotes = (str) => {
4
+ const trimmed = str.trim();
5
+ if (trimmed[0] === '"') {
6
+ if (trimmed[trimmed.length - 1] !== '"') {
7
+ throw new Error(`We failed to parse a quoted identifier '${str}'. Please avoid putting quotes or commas in smart comment identifiers (or file a PR to fix the parser).`);
8
+ }
9
+ return trimmed.substr(1, trimmed.length - 2);
10
+ }
11
+ else {
12
+ // PostgreSQL lower-cases unquoted columns, so we should too.
13
+ return trimmed.toLowerCase();
14
+ }
15
+ };
16
+ const parseSqlColumnArray = (str) => {
17
+ if (!str) {
18
+ throw new Error(`Cannot parse '${str}'`);
19
+ }
20
+ const parts = str.split(',');
21
+ return parts.map(removeQuotes);
22
+ };
23
+ const parseSqlColumnString = (str) => {
24
+ if (!str) {
25
+ throw new Error(`Cannot parse '${str}'`);
26
+ }
27
+ return removeQuotes(str);
28
+ };
29
+ function parseConstraintSpec(rawSpec) {
30
+ const [spec, ...tagComponents] = rawSpec.split(/\|/);
31
+ const parsed = parseTags(tagComponents.join('\n'));
32
+ return {
33
+ spec,
34
+ tags: parsed.tags,
35
+ description: parsed.text
36
+ };
37
+ }
38
+ function smartCommentConstraints(introspectionResults) {
39
+ const attributesByNames = (tbl, cols, debugStr) => {
40
+ const attributes = introspectionResults.attribute
41
+ .filter((a) => a.classId === tbl.id)
42
+ .sort((a, b) => a.num - b.num);
43
+ if (!cols) {
44
+ const pk = introspectionResults.constraint.find((c) => c.classId == tbl.id && c.type === 'p');
45
+ if (pk) {
46
+ return pk.keyAttributeNums.map((n) => attributes.find((a) => a.num === n));
47
+ }
48
+ else {
49
+ throw new Error(`No columns specified for '${tbl.namespaceName}.${tbl.name}' (oid: ${tbl.id}) and no PK found (${debugStr}).`);
50
+ }
51
+ }
52
+ return cols.map((colName) => {
53
+ const attr = attributes.find((a) => a.name === colName);
54
+ if (!attr) {
55
+ throw new Error(`Could not find attribute '${colName}' in '${tbl.namespaceName}.${tbl.name}'`);
56
+ }
57
+ return attr;
58
+ });
59
+ };
60
+ // First: primary keys
61
+ introspectionResults.class.forEach((klass) => {
62
+ const namespace = introspectionResults.namespace.find((n) => n.id === klass.namespaceId);
63
+ if (!namespace) {
64
+ return;
65
+ }
66
+ if (klass.tags.primaryKey) {
67
+ if (typeof klass.tags.primaryKey !== 'string') {
68
+ throw new Error(`@primaryKey configuration of '${klass.namespaceName}.${klass.name}' is invalid; please specify just once "@primaryKey col1,col2"`);
69
+ }
70
+ const { spec: pkSpec, tags, description } = parseConstraintSpec(klass.tags.primaryKey);
71
+ const columns = parseSqlColumnArray(pkSpec);
72
+ const attributes = attributesByNames(klass, columns, `@primaryKey ${klass.tags.primaryKey}`);
73
+ attributes.forEach((attr) => {
74
+ attr.tags.notNull = true;
75
+ });
76
+ const keyAttributeNums = attributes.map((a) => a.num);
77
+ // Now we need to fake a constraint for this:
78
+ const fakeConstraint = {
79
+ kind: 'constraint',
80
+ isFake: true,
81
+ isIndexed: true, // otherwise it gets ignored by ignoreIndexes
82
+ id: Math.random(),
83
+ name: `FAKE_${klass.namespaceName}_${klass.name}_primaryKey`,
84
+ type: 'p', // primary key
85
+ classId: klass.id,
86
+ foreignClassId: null,
87
+ comment: null,
88
+ description,
89
+ keyAttributeNums,
90
+ foreignKeyAttributeNums: null,
91
+ tags
92
+ };
93
+ introspectionResults.constraint.push(fakeConstraint);
94
+ }
95
+ });
96
+ // Now primary keys are in place, we can apply foreign keys
97
+ introspectionResults.class.forEach((klass) => {
98
+ const namespace = introspectionResults.namespace.find((n) => n.id === klass.namespaceId);
99
+ if (!namespace) {
100
+ return;
101
+ }
102
+ const getType = () => introspectionResults.type.find((t) => t.id === klass.typeId);
103
+ const foreignKey = klass.tags.foreignKey || getType().tags.foreignKey;
104
+ if (foreignKey) {
105
+ const foreignKeys = typeof foreignKey === 'string' ? [foreignKey] : foreignKey;
106
+ if (!Array.isArray(foreignKeys)) {
107
+ throw new Error(`Invalid foreign key smart comment specified on '${klass.namespaceName}.${klass.name}'`);
108
+ }
109
+ foreignKeys.forEach((fkSpecRaw, index) => {
110
+ if (typeof fkSpecRaw !== 'string') {
111
+ throw new Error(`Invalid foreign key spec (${index}) on '${klass.namespaceName}.${klass.name}'`);
112
+ }
113
+ const { spec: fkSpec, tags, description } = parseConstraintSpec(fkSpecRaw);
114
+ const matches = fkSpec.match(/^\(([^()]+)\) references ([^().]+)(?:\.([^().]+))?(?:\s*\(([^()]+)\))?$/i);
115
+ if (!matches) {
116
+ throw new Error(`Invalid foreignKey syntax for '${klass.namespaceName}.${klass.name}'; expected something like "(col1,col2) references schema.table (c1, c2)", you passed '${fkSpecRaw}'`);
117
+ }
118
+ const [, rawColumns, rawSchemaOrTable, rawTableOnly, rawForeignColumns] = matches;
119
+ const rawSchema = rawTableOnly
120
+ ? rawSchemaOrTable
121
+ : `"${klass.namespaceName}"`;
122
+ const rawTable = rawTableOnly || rawSchemaOrTable;
123
+ const columns = parseSqlColumnArray(rawColumns);
124
+ const foreignSchema = parseSqlColumnString(rawSchema);
125
+ const foreignTable = parseSqlColumnString(rawTable);
126
+ const foreignColumns = rawForeignColumns
127
+ ? parseSqlColumnArray(rawForeignColumns)
128
+ : null;
129
+ const foreignKlass = introspectionResults.class.find((k) => k.name === foreignTable && k.namespaceName === foreignSchema);
130
+ if (!foreignKlass) {
131
+ throw new Error(`@foreignKey smart comment referenced non-existant table/view '${foreignSchema}'.'${foreignTable}'. Note that this reference must use *database names* (i.e. it does not respect @name). (${fkSpecRaw})`);
132
+ }
133
+ const foreignNamespace = introspectionResults.namespace.find((n) => n.id === foreignKlass.namespaceId);
134
+ if (!foreignNamespace) {
135
+ return;
136
+ }
137
+ const keyAttributeNums = attributesByNames(klass, columns, `@foreignKey ${fkSpecRaw}`).map((a) => a.num);
138
+ const foreignKeyAttributeNums = attributesByNames(foreignKlass, foreignColumns, `@foreignKey ${fkSpecRaw}`).map((a) => a.num);
139
+ // Now we need to fake a constraint for this:
140
+ const fakeConstraint = {
141
+ kind: 'constraint',
142
+ isFake: true,
143
+ isIndexed: true, // otherwise it gets ignored by ignoreIndexes
144
+ id: Math.random(),
145
+ name: `FAKE_${klass.namespaceName}_${klass.name}_foreignKey_${index}`,
146
+ type: 'f', // foreign key
147
+ classId: klass.id,
148
+ foreignClassId: foreignKlass.id,
149
+ comment: null,
150
+ description,
151
+ keyAttributeNums,
152
+ foreignKeyAttributeNums,
153
+ tags
154
+ };
155
+ introspectionResults.constraint.push(fakeConstraint);
156
+ });
157
+ }
158
+ });
159
+ }
160
+ export const introspectionResultsFromRaw = (rawResults, pgAugmentIntrospectionResults) => {
161
+ const introspectionResultsByKind = deepClone(rawResults);
162
+ const xByY = (arrayOfX, attrKey) => arrayOfX.reduce((memo, x) => {
163
+ memo[x[attrKey]] = x;
164
+ return memo;
165
+ }, {});
166
+ const xByYAndZ = (arrayOfX, attrKey, attrKey2) => arrayOfX.reduce((memo, x) => {
167
+ if (!memo[x[attrKey]])
168
+ memo[x[attrKey]] = {};
169
+ memo[x[attrKey]][x[attrKey2]] = x;
170
+ return memo;
171
+ }, {});
172
+ introspectionResultsByKind.namespaceById = xByY(introspectionResultsByKind.namespace, 'id');
173
+ introspectionResultsByKind.classById = xByY(introspectionResultsByKind.class, 'id');
174
+ introspectionResultsByKind.typeById = xByY(introspectionResultsByKind.type, 'id');
175
+ introspectionResultsByKind.attributeByClassIdAndNum = xByYAndZ(introspectionResultsByKind.attribute, 'classId', 'num');
176
+ introspectionResultsByKind.extensionById = xByY(introspectionResultsByKind.extension, 'id');
177
+ const relate = (array, newAttr, lookupAttr, lookup, missingOk = false) => {
178
+ array.forEach((entry) => {
179
+ const key = entry[lookupAttr];
180
+ if (Array.isArray(key)) {
181
+ entry[newAttr] = key
182
+ .map((innerKey) => {
183
+ const result = lookup[innerKey];
184
+ if (innerKey && !result) {
185
+ if (missingOk) {
186
+ return;
187
+ }
188
+ throw new Error(`Could not look up '${newAttr}' by '${lookupAttr}' ('${innerKey}') on '${JSON.stringify(entry)}'`);
189
+ }
190
+ return result;
191
+ })
192
+ .filter((_) => _);
193
+ }
194
+ else {
195
+ const result = lookup[key];
196
+ if (key && !result) {
197
+ if (missingOk) {
198
+ return;
199
+ }
200
+ throw new Error(`Could not look up '${newAttr}' by '${lookupAttr}' on '${JSON.stringify(entry)}'`);
201
+ }
202
+ entry[newAttr] = result;
203
+ }
204
+ });
205
+ };
206
+ const augment = (introspectionResults) => {
207
+ [pgAugmentIntrospectionResults, smartCommentConstraints].forEach((fn) => fn ? fn(introspectionResults) : null);
208
+ };
209
+ augment(introspectionResultsByKind);
210
+ relate(introspectionResultsByKind.class, 'namespace', 'namespaceId', introspectionResultsByKind.namespaceById, true // Because it could be a type defined in a different namespace - which is fine so long as we don't allow querying it directly
211
+ );
212
+ relate(introspectionResultsByKind.class, 'type', 'typeId', introspectionResultsByKind.typeById);
213
+ relate(introspectionResultsByKind.attribute, 'class', 'classId', introspectionResultsByKind.classById);
214
+ relate(introspectionResultsByKind.attribute, 'type', 'typeId', introspectionResultsByKind.typeById);
215
+ relate(introspectionResultsByKind.procedure, 'namespace', 'namespaceId', introspectionResultsByKind.namespaceById);
216
+ relate(introspectionResultsByKind.type, 'class', 'classId', introspectionResultsByKind.classById, true);
217
+ relate(introspectionResultsByKind.type, 'domainBaseType', 'domainBaseTypeId', introspectionResultsByKind.typeById, true // Because not all types are domains
218
+ );
219
+ relate(introspectionResultsByKind.type, 'arrayItemType', 'arrayItemTypeId', introspectionResultsByKind.typeById, true // Because not all types are arrays
220
+ );
221
+ relate(introspectionResultsByKind.constraint, 'class', 'classId', introspectionResultsByKind.classById);
222
+ relate(introspectionResultsByKind.constraint, 'foreignClass', 'foreignClassId', introspectionResultsByKind.classById, true // Because many constraints don't apply to foreign classes
223
+ );
224
+ relate(introspectionResultsByKind.extension, 'namespace', 'namespaceId', introspectionResultsByKind.namespaceById, true // Because the extension could be a defined in a different namespace
225
+ );
226
+ relate(introspectionResultsByKind.extension, 'configurationClasses', 'configurationClassIds', introspectionResultsByKind.classById, true // Because the configuration table could be a defined in a different namespace
227
+ );
228
+ relate(introspectionResultsByKind.index, 'class', 'classId', introspectionResultsByKind.classById);
229
+ // Reverse arrayItemType -> arrayType
230
+ introspectionResultsByKind.type.forEach((type) => {
231
+ if (type.arrayItemType) {
232
+ type.arrayItemType.arrayType = type;
233
+ }
234
+ });
235
+ // Table/type columns / constraints
236
+ introspectionResultsByKind.class.forEach((klass) => {
237
+ klass.attributes = introspectionResultsByKind.attribute.filter((attr) => attr.classId === klass.id);
238
+ klass.canUseAsterisk = !klass.attributes.some((attr) => attr.columnLevelSelectGrant);
239
+ klass.constraints = introspectionResultsByKind.constraint.filter((constraint) => constraint.classId === klass.id);
240
+ klass.foreignConstraints = introspectionResultsByKind.constraint.filter((constraint) => constraint.foreignClassId === klass.id);
241
+ klass.primaryKeyConstraint = klass.constraints.find((constraint) => constraint.type === 'p');
242
+ });
243
+ // Constraint attributes
244
+ introspectionResultsByKind.constraint.forEach((constraint) => {
245
+ if (constraint.keyAttributeNums && constraint.class) {
246
+ constraint.keyAttributes = constraint.keyAttributeNums.map((nr) => constraint.class.attributes.find((attr) => attr.num === nr));
247
+ }
248
+ else {
249
+ constraint.keyAttributes = [];
250
+ }
251
+ if (constraint.foreignKeyAttributeNums && constraint.foreignClass) {
252
+ constraint.foreignKeyAttributes = constraint.foreignKeyAttributeNums.map((nr) => constraint.foreignClass.attributes.find((attr) => attr.num === nr));
253
+ }
254
+ else {
255
+ constraint.foreignKeyAttributes = [];
256
+ }
257
+ });
258
+ // Detect which columns and constraints are indexed
259
+ introspectionResultsByKind.index.forEach((index) => {
260
+ const columns = index.attributeNums.map((nr) => index.class.attributes.find((attr) => attr.num === nr));
261
+ // Indexed column (for orderBy / filter):
262
+ if (columns[0]) {
263
+ columns[0].isIndexed = true;
264
+ }
265
+ if (columns[0] && columns.length === 1 && index.isUnique) {
266
+ columns[0].isUnique = true;
267
+ }
268
+ // Indexed constraints (for reverse relations):
269
+ index.class.constraints
270
+ .filter((constraint) => constraint.type === 'f')
271
+ .forEach((constraint) => {
272
+ if (constraint.keyAttributeNums.every((nr, idx) => index.attributeNums[idx] === nr)) {
273
+ constraint.isIndexed = true;
274
+ }
275
+ });
276
+ });
277
+ return introspectionResultsByKind;
278
+ };
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  /*
2
3
  * IMPORTANT: when editing this file, ensure all operators (e.g. `@>`) are
3
4
  * specified in the correct namespace (e.g. `operator(pg_catalog.@>)`). It looks
@@ -7,18 +8,15 @@
7
8
  * NOTE: I'm not doing this with `=` because that way lies madness.
8
9
  */
9
10
  export const makeIntrospectionQuery = (serverVersionNum, options = {}) => {
10
- const {
11
- pgLegacyFunctionsOnly = false,
12
- pgIgnoreRBAC = true
13
- } = options;
14
- const unionRBAC = `
11
+ const { pgLegacyFunctionsOnly = false, pgIgnoreRBAC = true } = options;
12
+ const unionRBAC = `
15
13
  union all
16
14
  select pg_roles.oid _oid, pg_roles.*
17
15
  from pg_roles, accessible_roles, pg_auth_members
18
16
  where pg_auth_members.roleid = pg_roles.oid
19
17
  and pg_auth_members.member = accessible_roles._oid
20
18
  `;
21
- return `\
19
+ return `\
22
20
  -- @see https://www.postgresql.org/docs/9.5/static/catalogs.html
23
21
  -- @see https://github.com/graphile/graphile-engine/blob/master/packages/graphile-build-pg/src/plugins/introspectionQuery.js
24
22
  --
@@ -88,13 +86,17 @@ export const makeIntrospectionQuery = (serverVersionNum, options = {}) => {
88
86
  -- TODO: Variadic arguments.
89
87
  pro.provariadic = 0 and
90
88
  -- Filter our aggregate functions and window functions.
91
- ${serverVersionNum >= 110000 ? "pro.prokind = 'f'" : 'pro.proisagg = false and pro.proiswindow = false'} and
92
- ${pgLegacyFunctionsOnly ? `\
89
+ ${serverVersionNum >= 110000
90
+ ? "pro.prokind = 'f'"
91
+ : 'pro.proisagg = false and pro.proiswindow = false'} and
92
+ ${pgLegacyFunctionsOnly
93
+ ? `\
93
94
  -- We want to make sure the argument mode for all of our arguments is
94
95
  -- \`IN\` which means \`proargmodes\` will be null.
95
96
  pro.proargmodes is null and
96
97
  -- Do not select procedures that return \`RECORD\` (oid 2249).
97
- pro.prorettype operator(pg_catalog.<>) 2249 and` : `\
98
+ pro.prorettype operator(pg_catalog.<>) 2249 and`
99
+ : `\
98
100
  -- We want to make sure the argument modes for all of our arguments are
99
101
  -- \`IN\`, \`OUT\`, \`INOUT\`, or \`TABLE\` (not \`VARIADIC\`).
100
102
  (pro.proargmodes is null or pro.proargmodes operator(pg_catalog.<@) array['i','o','b','t']::"char"[]) and
@@ -372,7 +374,8 @@ export const makeIntrospectionQuery = (serverVersionNum, options = {}) => {
372
374
  idx.indpred is not null as "isPartial", -- if true, index is not on on rows.
373
375
  idx.indkey as "attributeNums",
374
376
  am.amname as "indexType",
375
- ${serverVersionNum >= 90600 ? `\
377
+ ${serverVersionNum >= 90600
378
+ ? `\
376
379
  (
377
380
  select array_agg(pg_index_column_has_property(idx.indexrelid,n::int2,'asc'))
378
381
  from unnest(idx.indkey) with ordinality as ord(key,n)
@@ -380,7 +383,8 @@ export const makeIntrospectionQuery = (serverVersionNum, options = {}) => {
380
383
  (
381
384
  select array_agg(pg_index_column_has_property(idx.indexrelid,n::int2,'nulls_first'))
382
385
  from unnest(idx.indkey) with ordinality as ord(key,n)
383
- ) as "attributePropertiesNullsFirst",` : ''}
386
+ ) as "attributePropertiesNullsFirst",`
387
+ : ''}
384
388
  dsc.description as "description"
385
389
  from
386
390
  pg_catalog.pg_index as idx
@@ -414,4 +418,4 @@ export const makeIntrospectionQuery = (serverVersionNum, options = {}) => {
414
418
  select row_to_json(x) as object from indexes as x
415
419
  ;
416
420
  `;
417
- };
421
+ };
package/esm/utils.js ADDED
@@ -0,0 +1,42 @@
1
+ // @ts-nocheck
2
+ export const parseTags = (str) => {
3
+ return str.split(/\r?\n/).reduce((prev, curr) => {
4
+ if (prev.text !== '') {
5
+ return { ...prev, text: `${prev.text}\n${curr}` };
6
+ }
7
+ const match = curr.match(/^@[a-zA-Z][a-zA-Z0-9_]*($|\s)/);
8
+ if (!match) {
9
+ return { ...prev, text: curr };
10
+ }
11
+ const key = match[0].substr(1).trim();
12
+ const value = match[0] === curr ? true : curr.replace(match[0], '');
13
+ return {
14
+ ...prev,
15
+ tags: {
16
+ ...prev.tags,
17
+ [key]: !Object.prototype.hasOwnProperty.call(prev.tags, key)
18
+ ? value
19
+ : Array.isArray(prev.tags[key])
20
+ ? [...prev.tags[key], value]
21
+ : [prev.tags[key], value]
22
+ }
23
+ };
24
+ }, {
25
+ tags: {},
26
+ text: ''
27
+ });
28
+ };
29
+ export const deepClone = (value) => {
30
+ if (Array.isArray(value)) {
31
+ return value.map((val) => deepClone(val));
32
+ }
33
+ else if (typeof value === 'object' && value) {
34
+ return Object.keys(value).reduce((memo, k) => {
35
+ memo[k] = deepClone(value[k]);
36
+ return memo;
37
+ }, {});
38
+ }
39
+ else {
40
+ return value;
41
+ }
42
+ };
package/gql.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export declare const parseGraphQuery: (introQuery: any) => {
2
+ queries: any;
3
+ mutations: any;
4
+ };