Skip to content

Commit 78949b3

Browse files
committed
feat: Add validation for function prefixes and source/prefix pairs
This change introduces two new validation rules for function configurations in `firebase.json`: 1. The `prefix` property, if specified, must only contain lowercase letters, numbers, and hyphens. 2. The combination of `source` and `prefix` must be unique across all function configurations. These changes ensure that function deployments with prefixes are valid and that there are no conflicts when deploying multiple functions from the same source directory.
1 parent 347fa56 commit 78949b3

File tree

2 files changed

+81
-15
lines changed

2 files changed

+81
-15
lines changed

src/functions/projectConfig.spec.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,37 @@ describe("projectConfig", () => {
4545
it("passes validation for multi-instance config with same source", () => {
4646
const config = [
4747
{ source: "foo", codebase: "bar" },
48-
{ source: "foo", codebase: "baz" },
48+
{ source: "foo", codebase: "baz", prefix: "prefix-two" },
4949
];
5050
expect(projectConfig.validate(config as projectConfig.NormalizedConfig)).to.deep.equal(
5151
config,
5252
);
5353
});
5454

55-
it("fails validation for multi-instance config with missing codebase", () => {
56-
const config = [{ source: "foo", codebase: "bar" }, { source: "foo" }];
55+
it("passes validation for multi-instance config with one missing codebase", () => {
56+
const config = [{ source: "foo", codebase: "bar", prefix: "bar-prefix" }, { source: "foo" }];
57+
const expected = [
58+
{ source: "foo", codebase: "bar", prefix: "bar-prefix" },
59+
{ source: "foo", codebase: "default" },
60+
];
61+
expect(projectConfig.validate(config as projectConfig.NormalizedConfig)).to.deep.equal(
62+
expected,
63+
);
64+
});
65+
66+
it("fails validation for multi-instance config with missing codebase and a default codebase", () => {
67+
const config = [{ source: "foo", codebase: "default" }, { source: "foo" }];
5768
expect(() => projectConfig.validate(config as projectConfig.NormalizedConfig)).to.throw(
5869
FirebaseError,
59-
/Each functions config must have a unique 'codebase' field/,
70+
/functions.codebase must be unique but 'default' was used more than once./,
71+
);
72+
});
73+
74+
it("fails validation for multi-instance config with multiple missing codebases", () => {
75+
const config = [{ source: "foo" }, { source: "foo" }];
76+
expect(() => projectConfig.validate(config as projectConfig.NormalizedConfig)).to.throw(
77+
FirebaseError,
78+
/functions.codebase must be unique but 'default' was used more than once./,
6079
);
6180
});
6281

@@ -85,6 +104,31 @@ describe("projectConfig", () => {
85104
).to.throw(FirebaseError, /Invalid codebase name/);
86105
});
87106

107+
it("fails validation given prefix with invalid characters", () => {
108+
expect(() => projectConfig.validate([{ ...TEST_CONFIG_0, prefix: "abc.efg" }])).to.throw(
109+
FirebaseError,
110+
/Invalid prefix/,
111+
);
112+
});
113+
114+
it("fails validation given prefix with capital letters", () => {
115+
expect(() => projectConfig.validate([{ ...TEST_CONFIG_0, prefix: "ABC" }])).to.throw(
116+
FirebaseError,
117+
/Invalid prefix/,
118+
);
119+
});
120+
121+
it("fails validation given a duplicate source/prefix pair", () => {
122+
const config = [
123+
{ source: "foo", codebase: "bar", prefix: "a" },
124+
{ source: "foo", codebase: "baz", prefix: "a" },
125+
];
126+
expect(() => projectConfig.validate(config as projectConfig.NormalizedConfig)).to.throw(
127+
FirebaseError,
128+
/More than one functions config specifies the same source directory/,
129+
);
130+
});
131+
88132
it("should allow a single function in an array to have a default codebase", () => {
89133
const config = [{ source: "foo" }];
90134
const expected = [{ source: "foo", codebase: "default" }];

src/functions/projectConfig.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { FunctionsConfig, FunctionConfig } from "../firebaseConfig";
22
import { FirebaseError } from "../error";
33

44
export type NormalizedConfig = [FunctionConfig, ...FunctionConfig[]];
5-
export type ValidatedSingle = FunctionConfig & { source: string; codebase: string };
5+
export type ValidatedSingle = FunctionConfig & { source:string; codebase: string };
66
export type ValidatedConfig = [ValidatedSingle, ...ValidatedSingle[]];
77

88
export const DEFAULT_CODEBASE = "default";
@@ -37,6 +37,20 @@ export function validateCodebase(codebase: string): void {
3737
}
3838
}
3939

40+
/**
41+
* Check that the prefix contains only allowed characters.
42+
*/
43+
export function validatePrefix(prefix: string): void {
44+
if (prefix.length > 30) {
45+
throw new FirebaseError("Invalid prefix. Prefix must be 30 characters or less.");
46+
}
47+
if (!/^[a-z0-9-]+$/.test(prefix)) {
48+
throw new FirebaseError(
49+
"Invalid prefix. Prefix can contain only lowercase letters, numeric characters, and dashes.",
50+
);
51+
}
52+
}
53+
4054
function validateSingle(config: FunctionConfig): ValidatedSingle {
4155
if (!config.source) {
4256
throw new FirebaseError("codebase source must be specified");
@@ -45,6 +59,9 @@ function validateSingle(config: FunctionConfig): ValidatedSingle {
4559
config.codebase = DEFAULT_CODEBASE;
4660
}
4761
validateCodebase(config.codebase);
62+
if (config.prefix) {
63+
validatePrefix(config.prefix);
64+
}
4865

4966
return { ...config, source: config.source, codebase: config.codebase };
5067
}
@@ -72,21 +89,26 @@ export function assertUnique(
7289
}
7390
}
7491

92+
function assertUniqueSourcePrefixPair(config: ValidatedConfig): void {
93+
const sourcePrefixPairs = new Set<string>();
94+
for (const c of config) {
95+
const key = `${c.source || ""}-${c.prefix || ""}`;
96+
if (sourcePrefixPairs.has(key)) {
97+
throw new FirebaseError(
98+
`More than one functions config specifies the same source directory ('${c.source}') and function name prefix ('${c.prefix || ""}').`,
99+
);
100+
}
101+
sourcePrefixPairs.add(key);
102+
}
103+
}
104+
75105
/**
76106
* Validate functions config.
77107
*/
78108
export function validate(config: NormalizedConfig): ValidatedConfig {
79-
if (config.length > 1) {
80-
for (const c of config) {
81-
if (!c.codebase) {
82-
throw new FirebaseError(
83-
"Each functions config must have a unique 'codebase' field when defining multiple functions.",
84-
);
85-
}
86-
}
87-
}
88109
const validated = config.map((cfg) => validateSingle(cfg)) as ValidatedConfig;
89110
assertUnique(validated, "codebase");
111+
assertUniqueSourcePrefixPair(validated);
90112
return validated;
91113
}
92114

@@ -108,4 +130,4 @@ export function configForCodebase(config: ValidatedConfig, codebase: string): Va
108130
throw new FirebaseError(`No functions config found for codebase ${codebase}`);
109131
}
110132
return codebaseCfg;
111-
}
133+
}

0 commit comments

Comments
 (0)