Skip to content

Commit 3d45884

Browse files
committed
Support multiple uploads (#9)
1 parent 6fa4894 commit 3d45884

11 files changed

+128
-56
lines changed

src/generate.spec.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,34 @@ import { generateTypes } from "./generate";
33

44
describe("Output testing", () => {
55
it("should parse kitchen sink correctly", () => {
6-
const collections = loadConfig("kitchen-sink.yml");
6+
const config = loadConfig("kitchen-sink.yml");
77

8-
expect(generateTypes(collections, { label: false })).toMatchSnapshot();
8+
expect(generateTypes(config, { label: false })).toMatchSnapshot();
99
});
1010

1111
it("should parse kitchen sink with label option", () => {
12-
const collections = loadConfig("kitchen-sink.yml");
12+
const config = loadConfig("kitchen-sink.yml");
1313

14-
expect(generateTypes(collections, { label: true })).toMatchSnapshot();
14+
expect(generateTypes(config, { label: true })).toMatchSnapshot();
1515
});
1616

1717
it("should support capitalization of type names", () => {
18-
const collections = loadConfig("kitchen-sink.yml");
18+
const config = loadConfig("kitchen-sink.yml");
1919

20-
expect(generateTypes(collections, { capitalize: true })).toMatchSnapshot();
20+
expect(generateTypes(config, { capitalize: true })).toMatchSnapshot();
2121
});
2222

2323
it("should support custom delimiter for type names", () => {
24-
const collections = loadConfig("kitchen-sink.yml");
24+
const config = loadConfig("kitchen-sink.yml");
2525

26-
expect(generateTypes(collections, { delimiter: "-" })).toMatchSnapshot();
26+
expect(generateTypes(config, { delimiter: "-" })).toMatchSnapshot();
2727
});
2828

2929
it("should support label, capitalization and delimiter at the same time", () => {
30-
const collections = loadConfig("kitchen-sink.yml");
30+
const config = loadConfig("kitchen-sink.yml");
3131

3232
expect(
33-
generateTypes(collections, { label: true, capitalize: true, delimiter: "" }),
33+
generateTypes(config, { label: true, capitalize: true, delimiter: "" }),
3434
).toMatchSnapshot();
3535
});
3636
});

src/generate.ts

+9-7
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { pullCollection } from "./collection";
22
import { appendExport, formatType } from "./output";
33
import { resolveRelations, resolveWidget, transformType } from "./widget";
4-
import type { Collection, NetlifyTsOptions } from "./types";
4+
import type { NetlifyCMSConfig, NetlifyTsOptions } from "./types";
55

6-
export const generateTypes = (
7-
collections: Collection[],
8-
options: NetlifyTsOptions = {},
9-
): string => {
10-
return collections
6+
export const generateTypes = (config: NetlifyCMSConfig, options: NetlifyTsOptions = {}): string => {
7+
const externalMediaLibrary = hasExternalMediaLibrary(config);
8+
9+
return config.collections
1110
.flatMap(pullCollection)
12-
.map(resolveWidget)
11+
.map(resolveWidget({ externalMediaLibrary }))
1312
.reduce(
1413
transformType({
1514
label: !!options.label,
@@ -26,3 +25,6 @@ export const generateTypes = (
2625
.replace(/^/, "/* eslint-disable */\n/* tslint:disable */\n\n")
2726
.concat("\n");
2827
};
28+
29+
export const hasExternalMediaLibrary = (config: NetlifyCMSConfig): boolean =>
30+
!!config.media_library?.name && !!config.media_library?.config;

src/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ const createNetlifyTypes = (
66
input: string | NetlifyCMSConfig,
77
options: NetlifyTsOptions = {},
88
): string => {
9-
const collections = loadConfig(input);
9+
const config = loadConfig(input);
1010

11-
return generateTypes(collections, options);
11+
return generateTypes(config, options);
1212
};
1313

1414
export default createNetlifyTypes;

src/input.spec.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ const mockConfigObject = yaml.load(
99

1010
describe("Load configuration", () => {
1111
it("should return collections from yaml config file", () => {
12-
const result = loadConfig("kitchen-sink.yml");
12+
const { collections } = loadConfig("kitchen-sink.yml");
1313

14-
expect(result.length).toBeDefined();
15-
expect(result).toEqual(mockConfigObject.collections);
14+
expect(collections.length).toBeDefined();
15+
expect(collections).toEqual(mockConfigObject.collections);
1616
});
1717

1818
it("should return collections from config object", () => {
19-
const result = loadConfig(mockConfigObject);
19+
const { collections } = loadConfig(mockConfigObject);
2020

21-
expect(result.length).toBeDefined();
22-
expect(result).toEqual(mockConfigObject.collections);
21+
expect(collections.length).toBeDefined();
22+
expect(collections).toEqual(mockConfigObject.collections);
2323
});
2424

2525
it("should throw if invalid filetype", () => {
@@ -33,7 +33,7 @@ describe("Load configuration", () => {
3333
});
3434

3535
it("should throw if malformed config object", () => {
36-
const t = () => loadConfig({ foo: "bar" } as NetlifyCMSConfig);
36+
const t = () => loadConfig({ foo: "bar" } as unknown as NetlifyCMSConfig);
3737
expect(t).toThrow("Failed loading collections from config");
3838
});
3939
});

src/input.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import yaml from "js-yaml";
22
import fs from "fs";
33
import path from "path";
4-
import type { Collection, NetlifyCMSConfig } from "./types";
4+
import type { NetlifyCMSConfig } from "./types";
55

6-
export const loadConfig = (config: string | NetlifyCMSConfig): Collection[] => {
6+
export const loadConfig = (config: string | NetlifyCMSConfig): NetlifyCMSConfig => {
77
let data;
88

99
if (typeof config === "object") {
@@ -18,7 +18,7 @@ export const loadConfig = (config: string | NetlifyCMSConfig): Collection[] => {
1818
throw new Error("Failed loading collections from config");
1919
}
2020

21-
return data.collections;
21+
return data;
2222
};
2323

2424
const loadConfigFile = (fileName: string): NetlifyCMSConfig => {

src/types/config.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { Collection } from "./fields";
1+
import type { Collection } from "./fields";
2+
import type { NetlifyMediaLibrary } from "./media-library";
23

34
export interface NetlifyCMSConfig {
4-
collections?: Collection[];
5+
collections: Collection[];
6+
media_library?: NetlifyMediaLibrary;
57
}

src/types/fields.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ interface BaseField {
55
label_singular?: string;
66
}
77

8+
interface MediaLibraryField {
9+
media_library?: {
10+
config?: {
11+
multiple?: boolean;
12+
};
13+
};
14+
}
15+
816
interface CommonField extends BaseField {
917
widget: "string" | "text" | "markdown" | "map" | "date" | "datetime" | "color" | "hidden";
1018
}
@@ -35,12 +43,12 @@ interface NumberField extends BaseField {
3543
value_type?: "int" | "float";
3644
}
3745

38-
interface ImageField extends BaseField {
46+
interface ImageField extends BaseField, MediaLibraryField {
3947
widget: "image";
4048
allow_multiple?: boolean;
4149
}
4250

43-
interface FileField extends BaseField {
51+
interface FileField extends BaseField, MediaLibraryField {
4452
widget: "file";
4553
allow_multiple?: boolean;
4654
}

src/types/media-library.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface NetlifyMediaLibrary {
2+
name?: string;
3+
config?: Record<string, string>;
4+
}

src/widget/resolve.spec.ts

+47-6
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,72 @@ import { resolveWidget, resolveType } from "./resolve";
33
describe("Resolve widget shape", () => {
44
describe("multiple property", () => {
55
it('should be false if "multiple" prop falsy', () => {
6-
expect(resolveWidget({ name: "name", widget: "string" }).multiple).toBe(false);
6+
expect(resolveWidget()({ name: "name", widget: "string" }).multiple).toBe(false);
77
});
88

99
it('should be true if "multiple" prop true', () => {
1010
expect(
11-
resolveWidget({ name: "name", widget: "select", multiple: true, options: [] }).multiple,
11+
resolveWidget()({ name: "name", widget: "select", multiple: true, options: [] }).multiple,
1212
).toBe(true);
1313
});
1414

1515
it('should be true if "list" widget type', () => {
16-
expect(resolveWidget({ name: "name", widget: "list" }).multiple).toBe(true);
16+
expect(resolveWidget()({ name: "name", widget: "list" }).multiple).toBe(true);
17+
});
18+
19+
it('should be true if "image" widget with external media library', () => {
20+
expect(
21+
resolveWidget({ externalMediaLibrary: true })({
22+
name: "name",
23+
widget: "image",
24+
media_library: { config: { multiple: true } },
25+
}).multiple,
26+
).toBe(true);
27+
});
28+
29+
it('should be true if "file" widget with external media library', () => {
30+
expect(
31+
resolveWidget({ externalMediaLibrary: true })({
32+
name: "name",
33+
widget: "file",
34+
media_library: { config: { multiple: true } },
35+
}).multiple,
36+
).toBe(true);
37+
});
38+
39+
it('should be false if "file" widget without external media library', () => {
40+
expect(
41+
resolveWidget({ externalMediaLibrary: false })({
42+
name: "name",
43+
widget: "file",
44+
media_library: { config: { multiple: true } },
45+
}).multiple,
46+
).toBe(false);
47+
});
48+
49+
it('should be false if "file" widget with external media library but no "multiple" config', () => {
50+
expect(
51+
resolveWidget({ externalMediaLibrary: true })({
52+
name: "name",
53+
widget: "file",
54+
}).multiple,
55+
).toBe(false);
1756
});
1857
});
1958

2059
describe("required property", () => {
2160
it("should be true by default", () => {
22-
expect(resolveWidget({ name: "name", widget: "string" }).required).toBe(true);
61+
expect(resolveWidget()({ name: "name", widget: "string" }).required).toBe(true);
2362
});
2463

2564
it("should be true if value truthy", () => {
26-
expect(resolveWidget({ name: "name", widget: "string", required: true }).required).toBe(true);
65+
expect(resolveWidget()({ name: "name", widget: "string", required: true }).required).toBe(
66+
true,
67+
);
2768
});
2869

2970
it("should be false if value is false", () => {
30-
expect(resolveWidget({ name: "name", widget: "string", required: false }).required).toBe(
71+
expect(resolveWidget()({ name: "name", widget: "string", required: false }).required).toBe(
3172
false,
3273
);
3374
});

src/widget/resolve.ts

+25-17
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
import type { Field, Widget } from "../types";
22

3-
export const resolveWidget = (field: Field): Widget => {
4-
return {
5-
name: field.name,
6-
required: field.required !== false,
7-
label: field.label,
8-
singularLabel: field.label_singular,
9-
multiple:
10-
field.widget === "list" ||
11-
((field.widget === "select" || field.widget === "relation") && !!field.multiple),
12-
type: resolveType(field),
3+
interface ResolveOptions {
4+
externalMediaLibrary?: boolean;
5+
}
6+
7+
export const resolveWidget =
8+
(options: ResolveOptions = {}) =>
9+
(field: Field): Widget => {
10+
return {
11+
name: field.name,
12+
required: field.required !== false,
13+
label: field.label,
14+
singularLabel: field.label_singular,
15+
multiple:
16+
field.widget === "list" ||
17+
((field.widget === "select" || field.widget === "relation") && !!field.multiple) ||
18+
(!!options.externalMediaLibrary &&
19+
(field.widget === "file" || field.widget === "image") &&
20+
!!field.media_library?.config?.multiple),
21+
type: resolveType(field, options),
22+
};
1323
};
14-
};
1524

16-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
17-
export const resolveType = (field: Field): Widget["type"] => {
25+
export const resolveType = (field: Field, options: ResolveOptions = {}): Widget["type"] => {
1826
switch (field.widget) {
1927
case "string":
2028
case "text":
@@ -43,7 +51,7 @@ export const resolveType = (field: Field): Widget["type"] => {
4351
];
4452
case "list":
4553
if (field.field) {
46-
const child = resolveWidget(field.field);
54+
const child = resolveWidget(options)(field.field);
4755

4856
if (typeof child.type === "string" && field.field.required === false) {
4957
child.type += "?";
@@ -53,20 +61,20 @@ export const resolveType = (field: Field): Widget["type"] => {
5361
}
5462

5563
if (field.fields) {
56-
return field.fields.map(resolveWidget);
64+
return field.fields.map(resolveWidget(options));
5765
}
5866

5967
if (field.types) {
6068
const type = field.typeKey || "type";
61-
return [[type, ...field.types.map(resolveWidget)]];
69+
return [[type, ...field.types.map(resolveWidget(options))]];
6270
}
6371

6472
return "string";
6573
case "select":
6674
return field.options.map((option) => (typeof option === "object" ? option.value : option));
6775
case "object":
6876
case "root":
69-
return field.fields?.map(resolveWidget);
77+
return field.fields?.map(resolveWidget(options));
7078
case "relation":
7179
return `~${field.collection}/${field.value_field}`;
7280
case "hidden":

src/widget/transform.spec.ts

+7
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ describe("Widget transformation", () => {
3434
[],
3535
]);
3636
});
37+
38+
it("should support arrays", () => {
39+
expect(parse({ name: "names", type: "string", required: true, multiple: true })).toEqual([
40+
["names: string[];"],
41+
[],
42+
]);
43+
});
3744
});
3845

3946
describe("boolean", () => {

0 commit comments

Comments
 (0)