-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathgenerate-schema.ts
More file actions
219 lines (195 loc) · 7.23 KB
/
Copy pathgenerate-schema.ts
File metadata and controls
219 lines (195 loc) · 7.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
import type { ZodObject, ZodSchema } from "astro/zod";
import { z } from "astro/zod";
import type {
ExpandedFields,
PocketBaseCollection
} from "../types/pocketbase-collection.type";
import type { PocketBaseLoaderOptions } from "../types/pocketbase-loader-options.type";
import { getRemoteSchema } from "./get-remote-schema";
import { parseSchema } from "./parse-schema";
import { readLocalSchema } from "./read-local-schema";
import { transformFiles } from "./transform-files";
/**
* Basic schema for every PocketBase collection.
*/
const BASIC_SCHEMA = {
id: z.string(),
collectionId: z.string(),
collectionName: z.string()
};
/**
* Types of fields that can be used as an ID.
*/
const VALID_ID_TYPES = ["text", "number", "email", "url", "date"];
/**
* Generate a schema for the collection based on the collection's schema in PocketBase.
* By default, a basic schema is returned if no other schema is available.
* If superuser credentials are provided, the schema is fetched from the PocketBase API.
* If a path to a local schema file is provided, the schema is read from the file.
*
* @param options Options for the loader. See {@link PocketBaseLoaderOptions} for more details.
*/
export async function generateSchema(
options: PocketBaseLoaderOptions
): Promise<ZodSchema> {
let collection: PocketBaseCollection | undefined;
const expandedFields: ExpandedFields = {};
// Try to get the schema directly from the PocketBase instance
collection = await getRemoteSchema(options);
const hasSuperuserRights = !!collection || !!options.superuserCredentials;
// If the schema is not available, try to read it from a local schema file
if (!collection && options.localSchema) {
collection = await readLocalSchema(
options.localSchema,
options.collectionName
);
}
// If the schema is still not available, return the basic schema
if (!collection) {
console.error(
`No schema available for "${options.collectionName}". Only basic types are available. Please check your configuration and provide a valid schema file or superuser credentials.`
);
// Return the basic schema since every collection has at least these fields
return z.object(BASIC_SCHEMA);
}
// Parse the schema
const fields = parseSchema(
collection,
options.jsonSchemas,
hasSuperuserRights,
options.improveTypes ?? false
);
// Check if custom id field is present
if (options.idField) {
// Find the id field in the schema
const idField = collection.fields.find(
(field) => field.name === options.idField
);
// Check if the id field is present and of a valid type
if (!idField) {
console.error(
`The id field "${options.idField}" is not present in the schema of the collection "${options.collectionName}".`
);
} else if (!VALID_ID_TYPES.includes(idField.type)) {
console.error(
`The id field "${options.idField}" for collection "${
options.collectionName
}" is of type "${
idField.type
}" which is not recommended. Please use one of the following types: ${VALID_ID_TYPES.join(
", "
)}.`
);
}
}
// Check if the content field is present
if (
typeof options.contentFields === "string" &&
!fields[options.contentFields]
) {
console.error(
`The content field "${options.contentFields}" is not present in the schema of the collection "${options.collectionName}".`
);
} else if (Array.isArray(options.contentFields)) {
for (const field of options.contentFields) {
if (!fields[field]) {
console.error(
`The content field "${field}" is not present in the schema of the collection "${options.collectionName}".`
);
}
}
}
// Check if the updated field is present
if (options.updatedField) {
if (!fields[options.updatedField]) {
console.error(
`The field "${options.updatedField}" is not present in the schema of the collection "${options.collectionName}".\nThis will lead to errors when trying to fetch only updated entries.`
);
} else {
const updatedField = collection.fields.find(
(field) => field.name === options.updatedField
);
if (
!updatedField ||
updatedField.type !== "autodate" ||
!updatedField.onUpdate
) {
console.warn(
`The field "${options.updatedField}" is not of type "autodate" with the value "Update" or "Create/Update".\nMake sure that the field is automatically updated when the entry is updated!`
);
}
}
}
if (options.expand) {
for (const expandedFieldName of options.expand) {
const expandedFieldDefinition = collection.fields.find(
(field) => field.name === expandedFieldName
);
if (!expandedFieldDefinition) {
console.error(
`The provided field in the expand property "${expandedFieldName}" is not present in the schema of the collection "${options.collectionName}".\nThis will lead to use unable to provide a definition for this field.`
);
} else {
if (!expandedFieldDefinition.collectionId) {
console.error(
`The provided field in the expand property "${expandedFieldName}" does not have an associated collection linked to it, we need this in order to know the shape of the related schema.`
);
} else {
const expandedchema = await generateSchema({
collectionName: expandedFieldDefinition.collectionId,
superuserCredentials: options.superuserCredentials,
localSchema: options.localSchema,
jsonSchemas: options.jsonSchemas,
improveTypes: options.improveTypes,
url: options.url
});
expandedFields[expandedFieldName] = z.union([
expandedchema,
z.array(expandedchema)
]);
}
}
}
}
const expandSchema = {
expand: buildExpandSchema(expandedFields).optional()
};
const schema = z.object({
...BASIC_SCHEMA,
...fields,
...expandSchema
});
// Get all file fields
const fileFields = collection.fields
.filter((field) => field.type === "file")
// Only show hidden fields if the user has superuser rights
.filter((field) => !field.hidden || hasSuperuserRights);
if (fileFields.length === 0) {
return schema;
}
// Transform file names to file urls
return schema.transform((entry) => {
if (Array.isArray(entry)) {
return entry.map((e) => transformFiles(options.url, fileFields, e));
}
return transformFiles(options.url, fileFields, entry);
});
}
/**
* Builds a Zod object schema from expandedFields, where each property is either a ZodSchema or an array of ZodSchema.
*/
export function buildExpandSchema(
expandedFields: Record<string, ZodSchema | Array<ZodSchema>>
): ZodObject<Record<string, ZodSchema | z.ZodArray<ZodSchema>>> {
const shape: Record<string, ZodSchema | z.ZodArray<ZodSchema>> = {};
for (const key in expandedFields) {
const value = expandedFields[key];
if (Array.isArray(value)) {
// If it's an array, wrap the first element as a ZodArray (assuming all elements are the same schema)
shape[key] = z.array(value[0]);
} else {
shape[key] = value;
}
}
return z.object(shape);
}