Skip to content

Commit 21717f7

Browse files
authored
Merge pull request #277 from cotype/feature/virtualField
feat(server): New Field Type, Virtual Fields with getter Function
2 parents d0246c2 + 1132346 commit 21717f7

File tree

13 files changed

+182
-37
lines changed

13 files changed

+182
-37
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,4 @@ or want to know more about what we consider "internal stuff", please refer
170170
to the [contribution guidelines](https://github.com/cotype/core/blob/master/CONTRIBUTING.md)
171171

172172

173-
.
173+

client/src/Edit/elements/ObjectInput.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,12 +211,17 @@ export default class ObjectInput extends Component<Props> {
211211
const f = fields[key];
212212
const { label: l, ...props } = f;
213213

214-
if ("hidden" in f || f.type === "references") return null;
214+
if (
215+
"hidden" in f ||
216+
f.type === "references" ||
217+
f.type === "virtual"
218+
)
219+
return null;
215220

216221
const component = inputs.get(f);
217222
let label = l || titleCase(key);
218223

219-
if (typeof component.getHint === "function") {
224+
if (component && typeof component.getHint === "function") {
220225
const getHint = component.getHint(fields[key]);
221226
if (getHint) label += ` ${getHint}`;
222227
}

client/src/Edit/elements/index.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ import inputs from "./inputs";
4646
import outputs from "./outputs";
4747
import PositionInput from "./PositionInput";
4848

49+
const Empty: React.FC = () => null;
50+
4951
inputs.register({
5052
boolean: BooleanInput,
5153
content: ReferenceInput,
@@ -64,7 +66,8 @@ inputs.register({
6466
date: DateInput,
6567
textarea: TextAreaInput,
6668
immutable: ImmutableInput,
67-
position: PositionInput
69+
position: PositionInput,
70+
virtual: Empty
6871
});
6972

7073
outputs.register({
@@ -83,7 +86,8 @@ outputs.register({
8386
date: DateOutput,
8487
textarea: TextOutput,
8588
immutable: ImmutableOutput,
86-
position: TextOutput
89+
position: TextOutput,
90+
virtual: Empty
8791
});
8892

8993
export const Input = ObjectInput;

client/src/Edit/elements/lists/Input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class Input extends Component<Props> {
5151
if (value && value.length) {
5252
value.forEach((item, idx) => {
5353
const component = inputs.get(props.item);
54-
const error = component.validate(item.value, props.item);
54+
const error = component && component.validate(item.value, props.item);
5555
if (error) errors[idx] = { value: error };
5656
});
5757

client/src/common/ResultItem.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,21 @@ type BasicProps = {
109109
};
110110
export const BasicResultItem = ({ item, term }: BasicProps) => {
111111
const { title, image, kind } = item;
112-
const src = image ? `/thumbs/square/${image}` : null;
112+
const src = image
113+
? image.includes("://")
114+
? image
115+
: `/thumbs/square/${image}`
116+
: null;
113117
return (
114118
<ImageItem>
115119
<ImageCircle src={src} alt={title} size={12} />
116120
<Wrapper>
117121
<TitleWrapper>
118122
<ResultTitle title={title} term={term}></ResultTitle>
119123

120-
<Kind style={{ background: colorHash.hex(String(kind)) }}>{kind}</Kind>
124+
<Kind style={{ background: colorHash.hex(String(kind)) }}>
125+
{kind}
126+
</Kind>
121127
</TitleWrapper>
122128
<DescriptionWrapper>
123129
{"description" in item && item.description && (

demo/models.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,16 @@ export const models: ModelOpts[] = [
187187
type: "references",
188188
model: "contentPages",
189189
fieldName: "ref"
190+
},
191+
testObject:{
192+
type:"object",
193+
typeName:"TestObject",
194+
fields:{
195+
test:{
196+
type:"string",
197+
required:true
198+
}
199+
}
190200
}
191201
}
192202
},
@@ -270,6 +280,23 @@ export const models: ModelOpts[] = [
270280
singleTeaser
271281
}
272282
}
283+
},
284+
virtual: {
285+
type: "virtual",
286+
outputType: "string",
287+
get: contentPage => {
288+
return "Not saved in CMS, this is virtual " + contentPage.pagetitle;
289+
}
290+
},
291+
testObject:{
292+
type:"object",
293+
typeName:"TestObject",
294+
fields:{
295+
test:{
296+
type:"string",
297+
required:true
298+
}
299+
}
273300
}
274301
}
275302
},

src/api/oapi.ts

Lines changed: 89 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Helpers to build a Swagger/OpenAPI spec.
33
*/
44
import { Model, Type } from "../../typings";
5-
import _ from "lodash";
5+
import _, { isEqual } from "lodash";
66
import {
77
OpenApiBuilder,
88
SchemaObject,
@@ -85,23 +85,44 @@ export function body(props: Props) {
8585
}
8686
};
8787
}
88-
88+
const refs: { [key: string]: SchemaObject } = {};
8989
export function createDefinition(
9090
model: Type,
91-
external?: boolean
91+
external: boolean,
92+
api: OpenApiBuilder
9293
): SchemaObject {
9394
if (!model) return empty;
9495
if (model.type in scalars) return scalars[model.type];
9596
if (model.type === "object") {
96-
return {
97+
const schema = {
9798
type: "object",
9899
properties: _.mapValues(model.fields, field =>
99-
createDefinition(field, external)
100+
createDefinition(field, external, api)
100101
),
101102
required: Object.entries(model.fields)
102-
.map(([key, value]) => ((value as any).required ? key : null))
103+
.map(([key, value]) =>
104+
((value as any).type === "virtual" && (value as any).get) ||
105+
(value as any).required
106+
? key
107+
: null
108+
)
103109
.filter(Boolean) as string[]
104110
};
111+
if (model.typeName) {
112+
const typeName = toTypeName(model.typeName);
113+
if (!refs[typeName]) {
114+
api.addSchema(typeName, schema);
115+
refs[typeName] = schema;
116+
} else {
117+
if (!isEqual(refs[typeName], schema)) {
118+
throw new Error(
119+
`Object key "typeName" is used for different element: ${typeName}`
120+
);
121+
}
122+
}
123+
return ref.schema(toTypeName(model.typeName));
124+
}
125+
return schema;
105126
}
106127

107128
if (model.type === "richtext") {
@@ -122,8 +143,12 @@ export function createDefinition(
122143
_content: {
123144
type: "string",
124145
enum:
125-
('models' in model && model.models && model.models.length) || model.model
126-
? [...('models' in model && model.models || [model.model] || [])]
146+
("models" in model && model.models && model.models.length) ||
147+
model.model
148+
? [
149+
...(("models" in model && model.models) || [model.model] ||
150+
[])
151+
]
127152
: undefined
128153
},
129154
_url: { type: "string" }
@@ -170,10 +195,19 @@ export function createDefinition(
170195
if (model.type === "union") {
171196
return {
172197
oneOf: Object.entries(model.types).map(([name, type]) => {
173-
const def = createDefinition(type, external);
174-
_.set(def, "properties._type", { type: "string", enum: [name] });
175-
_.set(def, "required", [...(def.required || []), "_type"]);
176-
return def;
198+
const def = createDefinition(type, external, api);
199+
return {
200+
allOf: [
201+
def,
202+
{
203+
type: "object",
204+
properties: {
205+
_type: { type: "string", enum: [name] }
206+
},
207+
required: ["_type"]
208+
}
209+
]
210+
};
177211
}),
178212
discriminator: {
179213
propertyName: "_type"
@@ -182,45 +216,80 @@ export function createDefinition(
182216
}
183217

184218
if (model.type === "list") {
185-
if (external) return array(createDefinition(model.item, external));
219+
if (external) {
220+
const schema = array(createDefinition(model.item, external, api));
221+
if (model.typeName) {
222+
const typeName = toTypeName(model.typeName);
223+
if (!refs[typeName]) {
224+
api.addSchema(typeName, schema);
225+
refs[typeName] = schema;
226+
} else {
227+
if (!isEqual(refs[typeName], schema)) {
228+
throw new Error(
229+
`List key "typeName" is used for different element: ${typeName}`
230+
);
231+
}
232+
}
233+
return ref.schema(toTypeName(model.typeName));
234+
}
235+
return schema;
236+
}
186237

187238
return array({
188239
type: "object",
189240
properties: {
190241
key: { type: "number" },
191-
value: createDefinition(model.item, external)
242+
value: createDefinition(model.item, external, api)
192243
}
193244
});
194245
}
195246

196247
if (model.type === "immutable") {
197-
return createDefinition(model.child, external);
248+
return createDefinition(model.child, external, api);
249+
}
250+
251+
if (model.type === "virtual") {
252+
if (!model.get) {
253+
return empty;
254+
}
255+
return {
256+
type: model.outputType
257+
};
198258
}
199259

200260
return ref.schema(model.type);
201261
}
202262

203-
export function modelSchema(model: Model, external?: boolean) {
263+
export function modelSchema(
264+
model: Model,
265+
external: boolean,
266+
api: OpenApiBuilder
267+
) {
204268
return {
205269
type: "object",
206270
properties: _.mapValues(model.fields, field =>
207-
createDefinition(field, external)
271+
createDefinition(field, external, api)
208272
),
209273
required: Object.entries(model.fields)
210-
.map(([key, value]) => ((value as any).required ? key : null))
274+
.map(([key, value]) =>
275+
((value as any).type === "virtual" && (value as any).get) ||
276+
(value as any).required
277+
? key
278+
: null
279+
)
211280
.filter(Boolean) as string[]
212281
};
213282
}
214283

215284
export function addModel(api: OpenApiBuilder, model: Model) {
216285
const TypeName = toTypeName(model.name);
217-
api.addSchema(TypeName, modelSchema(model));
286+
api.addSchema(TypeName, modelSchema(model, false, api));
218287
return ref.schema(TypeName);
219288
}
220289

221290
export function addExternalModel(api: OpenApiBuilder, model: Model) {
222291
const TypeName = toTypeName(model.name);
223-
api.addSchema(TypeName, modelSchema(model, true));
292+
api.addSchema(TypeName, modelSchema(model, true, api));
224293
return ref.schema(TypeName);
225294
}
226295

src/content/convert.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
Data,
55
ContentRefs,
66
PreviewOpts,
7-
ContentFormat
7+
ContentFormat, VirtualType
88
} from "../../typings";
99
import urlJoin from "url-join";
1010
import visit, { NO_STORE_VALUE } from "../model/visit";
@@ -185,6 +185,15 @@ export default function convert({
185185
field: { types: { [key: string]: object } }
186186
) {
187187
if (!Object.keys(field.types).includes(data._type)) return null;
188+
},
189+
virtual(
190+
_data,
191+
field: VirtualType,
192+
) {
193+
if(field.get){
194+
return field.get(content)
195+
}
196+
return undefined
188197
}
189198
});
190199
return content;

src/content/rest/describe.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,10 @@ export default (api: OpenApiBuilder, models: Models) => {
470470
// Add models schemas
471471
const refs: { [key: string]: ReferenceObject } = {};
472472
models.content.forEach(model => {
473+
if (model.collection === "iframe") {
474+
// Ignore iframe models & noFeed Models
475+
return;
476+
}
473477
refs[`${model.name}`] = addExternalModel(api, model);
474478
});
475479

@@ -482,8 +486,8 @@ export default (api: OpenApiBuilder, models: Models) => {
482486
const singleton = collection === "singleton";
483487
const tags = [plural];
484488

485-
if (collection === "iframe") {
486-
// Ignore iframe models
489+
if (collection === "iframe" || model.noFeed) {
490+
// Ignore iframe models & noFeed Models
487491
return;
488492
}
489493

src/content/rest/routes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ export default function routes(
172172
});
173173

174174
models
175-
.filter(m => m.collection !== "iframe")
175+
.filter(m => m.collection !== "iframe" && !m.noFeed)
176176
.forEach(model => {
177177
const type = model.name;
178178

0 commit comments

Comments
 (0)