directus

Форк
0
/
get-schema.ts 
223 строки · 6.7 Кб
1
import { useEnv } from '@directus/env';
2
import type { SchemaInspector } from '@directus/schema';
3
import { createInspector } from '@directus/schema';
4
import { systemCollectionRows } from '@directus/system-data';
5
import type { Filter, SchemaOverview } from '@directus/types';
6
import { parseJSON, toArray } from '@directus/utils';
7
import type { Knex } from 'knex';
8
import { mapValues } from 'lodash-es';
9
import { useBus } from '../bus/index.js';
10
import { getSchemaCache, setSchemaCache } from '../cache.js';
11
import { ALIAS_TYPES } from '../constants.js';
12
import getDatabase from '../database/index.js';
13
import { useLock } from '../lock/index.js';
14
import { useLogger } from '../logger.js';
15
import { RelationsService } from '../services/relations.js';
16
import getDefaultValue from './get-default-value.js';
17
import { getSystemFieldRowsWithAuthProviders } from './get-field-system-rows.js';
18
import getLocalType from './get-local-type.js';
19

20
const logger = useLogger();
21

22
export async function getSchema(
23
	options?: {
24
		database?: Knex;
25

26
		/**
27
		 * To bypass any cached schema if bypassCache is enabled.
28
		 * Used to ensure schema snapshot/apply is not using outdated schema
29
		 */
30
		bypassCache?: boolean;
31
	},
32
	attempt = 0,
33
): Promise<SchemaOverview> {
34
	const MAX_ATTEMPTS = 3;
35

36
	const env = useEnv();
37

38
	if (attempt >= MAX_ATTEMPTS) {
39
		throw new Error(`Failed to get Schema information: hit infinite loop`);
40
	}
41

42
	if (options?.bypassCache || env['CACHE_SCHEMA'] === false) {
43
		const database = options?.database || getDatabase();
44
		const schemaInspector = createInspector(database);
45

46
		return await getDatabaseSchema(database, schemaInspector);
47
	}
48

49
	const cached = await getSchemaCache();
50

51
	if (cached) {
52
		return cached;
53
	}
54

55
	const lock = useLock();
56
	const bus = useBus();
57

58
	const lockKey = 'schemaCache--preparing';
59
	const messageKey = 'schemaCache--done';
60
	const processId = await lock.increment(lockKey);
61

62
	if (processId >= (env['CACHE_SCHEMA_MAX_ITERATIONS'] as number)) {
63
		await lock.delete(lockKey);
64
	}
65

66
	const currentProcessShouldHandleOperation = processId === 1;
67

68
	if (currentProcessShouldHandleOperation === false) {
69
		logger.trace('Schema cache is prepared in another process, waiting for result.');
70

71
		return new Promise((resolve, reject) => {
72
			const TIMEOUT = 10000;
73

74
			const timeout: NodeJS.Timeout = setTimeout(() => {
75
				logger.trace('Did not receive schema callback message in time. Pulling schema...');
76
				callback().catch(reject);
77
			}, TIMEOUT);
78

79
			bus.subscribe(messageKey, callback);
80

81
			async function callback() {
82
				try {
83
					if (timeout) clearTimeout(timeout);
84

85
					const schema = await getSchema(options, attempt + 1);
86
					resolve(schema);
87
				} catch (error) {
88
					reject(error);
89
				} finally {
90
					bus.unsubscribe(messageKey, callback);
91
				}
92
			}
93
		});
94
	}
95

96
	try {
97
		const database = options?.database || getDatabase();
98
		const schemaInspector = createInspector(database);
99

100
		const schema = await getDatabaseSchema(database, schemaInspector);
101
		await setSchemaCache(schema);
102
		return schema;
103
	} finally {
104
		await lock.delete(lockKey);
105
		bus.publish(messageKey, { ready: true });
106
	}
107
}
108

109
async function getDatabaseSchema(database: Knex, schemaInspector: SchemaInspector): Promise<SchemaOverview> {
110
	const env = useEnv();
111

112
	const result: SchemaOverview = {
113
		collections: {},
114
		relations: [],
115
	};
116

117
	const systemFieldRows = getSystemFieldRowsWithAuthProviders();
118

119
	const schemaOverview = await schemaInspector.overview();
120

121
	const collections = [
122
		...(await database
123
			.select('collection', 'singleton', 'note', 'sort_field', 'accountability')
124
			.from('directus_collections')),
125
		...systemCollectionRows,
126
	];
127

128
	for (const [collection, info] of Object.entries(schemaOverview)) {
129
		if (toArray(env['DB_EXCLUDE_TABLES']).includes(collection)) {
130
			logger.trace(`Collection "${collection}" is configured to be excluded and will be ignored`);
131
			continue;
132
		}
133

134
		if (!info.primary) {
135
			logger.warn(`Collection "${collection}" doesn't have a primary key column and will be ignored`);
136
			continue;
137
		}
138

139
		if (collection.includes(' ')) {
140
			logger.warn(`Collection "${collection}" has a space in the name and will be ignored`);
141
			continue;
142
		}
143

144
		const collectionMeta = collections.find((collectionMeta) => collectionMeta.collection === collection);
145

146
		result.collections[collection] = {
147
			collection,
148
			primary: info.primary,
149
			singleton:
150
				collectionMeta?.singleton === true || collectionMeta?.singleton === 'true' || collectionMeta?.singleton === 1,
151
			note: collectionMeta?.note || null,
152
			sortField: collectionMeta?.sort_field || null,
153
			accountability: collectionMeta ? collectionMeta.accountability : 'all',
154
			fields: mapValues(schemaOverview[collection]?.columns, (column) => {
155
				return {
156
					field: column.column_name,
157
					defaultValue: getDefaultValue(column) ?? null,
158
					nullable: column.is_nullable ?? true,
159
					generated: column.is_generated ?? false,
160
					type: getLocalType(column),
161
					dbType: column.data_type,
162
					precision: column.numeric_precision || null,
163
					scale: column.numeric_scale || null,
164
					special: [],
165
					note: null,
166
					validation: null,
167
					alias: false,
168
				};
169
			}),
170
		};
171
	}
172

173
	const fields = [
174
		...(await database
175
			.select<
176
				{
177
					id: number;
178
					collection: string;
179
					field: string;
180
					special: string;
181
					note: string | null;
182
					validation: string | Record<string, any> | null;
183
				}[]
184
			>('id', 'collection', 'field', 'special', 'note', 'validation')
185
			.from('directus_fields')),
186
		...systemFieldRows,
187
	].filter((field) => (field.special ? toArray(field.special) : []).includes('no-data') === false);
188

189
	for (const field of fields) {
190
		if (!result.collections[field.collection]) continue;
191

192
		const existing = result.collections[field.collection]?.fields[field.field];
193
		const column = schemaOverview[field.collection]?.columns[field.field];
194
		const special = field.special ? toArray(field.special) : [];
195

196
		if (ALIAS_TYPES.some((type) => special.includes(type)) === false && !existing) continue;
197

198
		const type = (existing && getLocalType(column, { special })) || 'alias';
199
		let validation = field.validation ?? null;
200

201
		if (validation && typeof validation === 'string') validation = parseJSON(validation);
202

203
		result.collections[field.collection]!.fields[field.field] = {
204
			field: field.field,
205
			defaultValue: existing?.defaultValue ?? null,
206
			nullable: existing?.nullable ?? true,
207
			generated: existing?.generated ?? false,
208
			type: type,
209
			dbType: existing?.dbType || null,
210
			precision: existing?.precision || null,
211
			scale: existing?.scale || null,
212
			special: special,
213
			note: field.note,
214
			alias: existing?.alias ?? true,
215
			validation: (validation as Filter) ?? null,
216
		};
217
	}
218

219
	const relationsService = new RelationsService({ knex: database, schema: result });
220
	result.relations = await relationsService.readAll();
221

222
	return result;
223
}
224

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.