Skip to content

Commit 4e285ea

Browse files
authored
Allow forbidding certain values from being exported (#2692)
2 parents 030bf23 + a31f6ae commit 4e285ea

12 files changed

Lines changed: 172 additions & 23 deletions

File tree

.changeset/hip-moose-enter.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"graphile-build": patch
3+
"graphile-utils": patch
4+
"graphile-export": patch
5+
"@dataplan/pg": patch
6+
"tamedevil": patch
7+
---
8+
9+
Allow forbidding certain objects/functions from being exported, and raise error
10+
as early as possible.

.changeset/some-dancers-matter.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"graphile-utils": patch
3+
"postgraphile": patch
4+
---
5+
6+
Forbid export of 'build', this has required a slight degradation of the
7+
makeAddPgTableConditionPlugin/addPgTableCondition signature for people using the
8+
legacy signature - namely the entire build object is no longer available in the
9+
callback that is the fourth argument. (Only have 3 arguments to your call?
10+
You're not impacted!) In the unlikely event this causes you any issues, your
11+
best bet is to move to the `apply()` approach (only use the 3 documented
12+
arguments), but we can also potentially expand the parts of build that are made
13+
available.

grafast/dataplan-pg/src/datasource.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,15 @@ export function EXPORTABLE<T, TScope extends any[]>(
6060
args: [...TScope],
6161
nameHint?: string,
6262
): T {
63+
const forbiddenIndex = args.findIndex(isForbidden);
64+
if (forbiddenIndex >= 0) {
65+
throw new Error(
66+
`${nameHint ?? "Anonymous"} EXPORTABLE call's args[${forbiddenIndex}] is not allowed to be exported.`,
67+
);
68+
}
6369
const fn: T = factory(...args);
6470
if (
65-
(typeof fn === "function" || (typeof fn === "object" && fn !== null)) &&
71+
((typeof fn === "object" && fn !== null) || typeof fn === "function") &&
6672
!("$exporter$factory" in fn)
6773
) {
6874
Object.defineProperties(fn, {
@@ -74,6 +80,15 @@ export function EXPORTABLE<T, TScope extends any[]>(
7480
return fn;
7581
}
7682

83+
export function isForbidden(thing: unknown): thing is { $$export: false } {
84+
return (
85+
(typeof thing === "object" || typeof thing === "function") &&
86+
thing !== null &&
87+
"$$export" in thing &&
88+
thing.$$export === false
89+
);
90+
}
91+
7792
/** @deprecated Use DataplanPg.PgResourceUniqueExtensions instead */
7893
export type PgResourceUniqueExtensions = DataplanPg.PgResourceUniqueExtensions;
7994

grafast/dataplan-pg/src/examples/exampleSchema.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,15 @@ export function EXPORTABLE<T, TScope extends any[]>(
132132
args: [...TScope],
133133
nameHint?: string,
134134
): T {
135+
const forbiddenIndex = args.findIndex(isForbidden);
136+
if (forbiddenIndex >= 0) {
137+
throw new Error(
138+
`${nameHint ?? "Anonymous"} EXPORTABLE call's args[${forbiddenIndex}] is not allowed to be exported.`,
139+
);
140+
}
135141
const fn: T = factory(...args);
136142
if (
137-
(typeof fn === "function" || (typeof fn === "object" && fn !== null)) &&
143+
((typeof fn === "object" && fn !== null) || typeof fn === "function") &&
138144
!("$exporter$factory" in fn)
139145
) {
140146
Object.defineProperties(fn, {
@@ -146,6 +152,15 @@ export function EXPORTABLE<T, TScope extends any[]>(
146152
return fn;
147153
}
148154

155+
export function isForbidden(thing: unknown): thing is { $$export: false } {
156+
return (
157+
(typeof thing === "object" || typeof thing === "function") &&
158+
thing !== null &&
159+
"$$export" in thing &&
160+
thing.$$export === false
161+
);
162+
}
163+
149164
// These are what the generics extend from
150165

151166
declare global {

graphile-build/graphile-build/src/makeNewBuild.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ export default function makeNewBuild(
123123
}
124124

125125
const build: GraphileBuild.BuildBase = {
126+
// FORBID EXPORTING!
127+
...({ $$export: false } as object),
128+
126129
lib,
127130

128131
// Legacy support for things from lib

graphile-build/graphile-build/src/utils.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,15 @@ export function EXPORTABLE<T, TScope extends any[]>(
1515
args: [...TScope],
1616
nameHint?: string,
1717
): T {
18+
const forbiddenIndex = args.findIndex(isForbidden);
19+
if (forbiddenIndex >= 0) {
20+
throw new Error(
21+
`${nameHint ?? "Anonymous"} EXPORTABLE call's args[${forbiddenIndex}] is not allowed to be exported.`,
22+
);
23+
}
1824
const fn: T = factory(...args);
1925
if (
20-
(typeof fn === "function" || (typeof fn === "object" && fn !== null)) &&
26+
((typeof fn === "object" && fn !== null) || typeof fn === "function") &&
2127
!("$exporter$factory" in fn)
2228
) {
2329
Object.defineProperties(fn, {
@@ -29,6 +35,15 @@ export function EXPORTABLE<T, TScope extends any[]>(
2935
return fn;
3036
}
3137

38+
export function isForbidden(thing: unknown): thing is { $$export: false } {
39+
return (
40+
(typeof thing === "object" || typeof thing === "function") &&
41+
thing !== null &&
42+
"$$export" in thing &&
43+
thing.$$export === false
44+
);
45+
}
46+
3247
export function EXPORTABLE_OBJECT_CLONE<T extends object>(obj: T): T {
3348
if (Object.getPrototypeOf(obj) === Object.prototype) {
3449
const keys = Object.keys(obj);

graphile-build/graphile-utils/src/exportable.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@ export function EXPORTABLE<T, TScope extends any[]>(
33
args: [...TScope],
44
nameHint?: string,
55
): T {
6+
const forbiddenIndex = args.findIndex(isForbidden);
7+
if (forbiddenIndex >= 0) {
8+
throw new Error(
9+
`${nameHint ?? "Anonymous"} EXPORTABLE call's args[${forbiddenIndex}] is not allowed to be exported.`,
10+
);
11+
}
612
const fn: T = factory(...args);
713
if (
8-
(typeof fn === "function" || (typeof fn === "object" && fn !== null)) &&
14+
((typeof fn === "object" && fn !== null) || typeof fn === "function") &&
915
!("$exporter$factory" in fn)
1016
) {
1117
Object.defineProperties(fn, {
@@ -16,3 +22,12 @@ export function EXPORTABLE<T, TScope extends any[]>(
1622
}
1723
return fn;
1824
}
25+
26+
export function isForbidden(thing: unknown): thing is { $$export: false } {
27+
return (
28+
(typeof thing === "object" || typeof thing === "function") &&
29+
thing !== null &&
30+
"$$export" in thing &&
31+
thing.$$export === false
32+
);
33+
}

graphile-build/graphile-utils/src/makeAddPgTableConditionPlugin.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ export function addPgTableCondition(
1111
conditionFieldSpecGenerator: (
1212
build: GraphileBuild.Build,
1313
) => GrafastInputFieldConfig,
14+
// DEPRECATED! Use `apply` instead.
1415
conditionGenerator?: (
1516
value: unknown,
1617
helpers: {
1718
sql: typeof sql;
1819
sqlTableAlias: SQL;
1920
sqlValueWithCodec: typeof sqlValueWithCodec;
20-
build: GraphileBuild.Build;
21+
// We can't afford to make the entire of build EXPORTABLE, and people
22+
// really ought to move to using the `apply` method, so...
23+
build: ReturnType<typeof pruneBuild>;
2124
/** @internal We might expose this in future if needed */
2225
condition: PgCondition;
2326
},
@@ -85,21 +88,23 @@ export function addPgTableCondition(
8588
);
8689
}
8790
// build applyPlan
91+
const _build = pruneBuild(build);
8892
conditionFieldSpec.apply = EXPORTABLE(
89-
(build, conditionGenerator, sql, sqlValueWithCodec) =>
93+
(_build, conditionGenerator, sql, sqlValueWithCodec) =>
9094
function apply(condition: PgCondition, val) {
9195
const expression = conditionGenerator!(val, {
9296
sql,
9397
sqlTableAlias: condition.alias,
9498
sqlValueWithCodec,
95-
build,
99+
build: _build,
96100
condition,
97101
});
98102
if (expression) {
99103
condition.where(expression);
100104
}
101105
},
102-
[build, conditionGenerator, sql, sqlValueWithCodec],
106+
[_build, conditionGenerator, sql, sqlValueWithCodec],
107+
"addPgTableCondition_generator",
103108
);
104109
}
105110
const meta = build._pluginMeta[displayName]!;
@@ -125,3 +130,23 @@ export function addPgTableCondition(
125130

126131
/** @deprecated renamed to addPgTableCondition */
127132
export const makeAddPgTableConditionPlugin = addPgTableCondition;
133+
134+
function pruneBuild(build: GraphileBuild.Build) {
135+
const {
136+
sql,
137+
grafast,
138+
graphql,
139+
dataplanPg,
140+
input: { pgRegistry },
141+
} = build;
142+
return EXPORTABLE(
143+
(dataplanPg, grafast, graphql, pgRegistry, sql) => ({
144+
sql,
145+
grafast,
146+
graphql,
147+
dataplanPg,
148+
input: { pgRegistry },
149+
}),
150+
[dataplanPg, grafast, graphql, pgRegistry, sql],
151+
);
152+
}

utils/graphile-export/src/exportSchema.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ import {
4747
import type { GraphQLSchemaNormalizedConfig } from "graphql/type/schema";
4848
import type { PgSQL, SQL } from "pg-sql2";
4949

50+
import { isForbidden } from "./helpers.js";
5051
import type { ExportOptions } from "./interfaces.js";
5152
import { optimize } from "./optimize/index.js";
5253
import { reservedWords } from "./reservedWords.js";
54+
import { isImportable, isNotNullish } from "./utils.js";
5355
import { wellKnown } from "./wellKnown.js";
5456

5557
// Cannot import sql because it's optional
@@ -174,20 +176,6 @@ const templateOptions: TemplateBuilderOptions = {
174176
plugins: ["typescript"],
175177
};
176178

177-
export function isNotNullish<T>(input: T | null | undefined): input is T {
178-
return input != null;
179-
}
180-
181-
function isImportable(
182-
thing: unknown,
183-
): thing is { $$export: { moduleName: string; exportName: string } } {
184-
return (
185-
(typeof thing === "object" || typeof thing === "function") &&
186-
thing !== null &&
187-
"$$export" in (thing as object | AnyFunction)
188-
);
189-
}
190-
191179
type AnyFunction = {
192180
(...args: any[]): any;
193181
displayName?: string;
@@ -974,6 +962,11 @@ function _convertToAST(
974962
depth: number,
975963
reference: t.Expression,
976964
): t.Expression {
965+
if (isForbidden(thing)) {
966+
throw new Error(
967+
`The value at ${locationHint} is forbidden from being exported; please be more specific in your EXPORTABLE factories!`,
968+
);
969+
}
977970
const handleSubvalue = (
978971
value: any,
979972
tKey: t.Expression,

utils/graphile-export/src/helpers.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ export function EXPORTABLE<T, TScope extends any[]>(
33
args: [...TScope],
44
nameHint?: string,
55
): T {
6+
const forbiddenIndex = args.findIndex(isForbidden);
7+
if (forbiddenIndex >= 0) {
8+
throw new Error(
9+
`${nameHint ?? "Anonymous"} EXPORTABLE call's args[${forbiddenIndex}] is not allowed to be exported.`,
10+
);
11+
}
612
const fn: T = factory(...args);
713
if (
814
((typeof fn === "object" && fn !== null) || typeof fn === "function") &&
@@ -16,3 +22,12 @@ export function EXPORTABLE<T, TScope extends any[]>(
1622
}
1723
return fn;
1824
}
25+
26+
export function isForbidden(thing: unknown): thing is { $$export: false } {
27+
return (
28+
(typeof thing === "object" || typeof thing === "function") &&
29+
thing !== null &&
30+
"$$export" in thing &&
31+
thing.$$export === false
32+
);
33+
}

0 commit comments

Comments
 (0)