feat: nested collections (#680)

This commit is contained in:
Daniel Lautzenheiser
2023-04-04 15:12:32 -04:00
committed by GitHub
parent 22a1b8d9c0
commit d0ecae310c
54 changed files with 2671 additions and 295 deletions

View File

@ -20,10 +20,10 @@ import {
import { getBackend, invokeEvent } from './lib/registry';
import { sanitizeChar } from './lib/urlHelper';
import {
CURSOR_COMPATIBILITY_SYMBOL,
Cursor,
asyncLock,
blobToFileObj,
Cursor,
CURSOR_COMPATIBILITY_SYMBOL,
getPathDepth,
localForage,
} from './lib/util';
@ -39,6 +39,7 @@ import {
selectMediaFolders,
} from './lib/util/collection.util';
import { selectMediaFilePath, selectMediaFilePublicPath } from './lib/util/media.util';
import { selectCustomPath, slugFromCustomPath } from './lib/util/nested.util';
import { set } from './lib/util/object.util';
import { dateParsers, expandPath, extractTemplateVars } from './lib/widgets/stringTemplate';
import createEntry from './valueObjects/createEntry';
@ -59,6 +60,7 @@ import type {
Field,
FilterRule,
ImplementationEntry,
PersistArgs,
SearchQueryResponse,
SearchResponse,
UnknownField,
@ -230,9 +232,9 @@ interface AuthStore {
logout: () => void;
}
interface BackendOptions {
interface BackendOptions<EF extends BaseField> {
backendName: string;
config: Config;
config: Config<EF>;
authStore?: AuthStore;
}
@ -258,16 +260,7 @@ interface BackupEntry {
i18n?: Record<string, { raw: string }>;
}
interface PersistArgs {
config: Config;
collection: Collection;
entryDraft: EntryDraft;
assetProxies: AssetProxy[];
usedSlugs: string[];
status?: string;
}
function collectionDepth(collection: Collection) {
function collectionDepth<EF extends BaseField>(collection: Collection<EF>) {
let depth;
depth =
('nested' in collection && collection.nested?.depth) || getPathDepth(collection.path ?? '');
@ -279,17 +272,17 @@ function collectionDepth(collection: Collection) {
return depth;
}
export class Backend<BC extends BackendClass = BackendClass> {
export class Backend<EF extends BaseField = UnknownField, BC extends BackendClass = BackendClass> {
implementation: BC;
backendName: string;
config: Config;
config: Config<EF>;
authStore?: AuthStore;
user?: User | null;
backupSync: AsyncLock;
constructor(
implementation: BackendInitializer,
{ backendName, authStore, config }: BackendOptions,
implementation: BackendInitializer<EF>,
{ backendName, authStore, config }: BackendOptions<EF>,
) {
// We can't reliably run this on exit, so we do cleanup on load.
this.deleteAnonymousBackup();
@ -401,9 +394,15 @@ export class Backend<BC extends BackendClass = BackendClass> {
entryData: EntryData,
config: Config,
usedSlugs: string[],
customPath: string | undefined,
) {
const slugConfig = config.slug;
const slug = slugFormatter(collection, entryData, slugConfig);
let slug: string;
if (customPath) {
slug = slugFromCustomPath(collection, customPath);
} else {
slug = slugFormatter(collection, entryData, slugConfig);
}
let i = 1;
let uniqueSlug = slug;
@ -417,7 +416,10 @@ export class Backend<BC extends BackendClass = BackendClass> {
return uniqueSlug;
}
processEntries(loadedEntries: ImplementationEntry[], collection: Collection): Entry[] {
processEntries<EF extends BaseField>(
loadedEntries: ImplementationEntry[],
collection: Collection<EF>,
): Entry[] {
const entries = loadedEntries.map(loadedEntry =>
createEntry(
collection.name,
@ -486,13 +488,13 @@ export class Backend<BC extends BackendClass = BackendClass> {
// repeats the process. Once there is no available "next" action, it
// returns all the collected entries. Used to retrieve all entries
// for local searches and queries.
async listAllEntries<T extends BaseField = UnknownField>(collection: Collection<T>) {
async listAllEntries<EF extends BaseField>(collection: Collection<EF>) {
if ('folder' in collection && collection.folder && this.implementation.allEntriesByFolder) {
const depth = collectionDepth(collection as Collection);
const extension = selectFolderEntryExtension(collection as Collection);
const depth = collectionDepth(collection);
const extension = selectFolderEntryExtension(collection);
return this.implementation
.allEntriesByFolder(collection.folder as string, extension, depth)
.then(entries => this.processEntries(entries, collection as Collection));
.then(entries => this.processEntries(entries, collection));
}
const response = await this.listEntries(collection as Collection);
@ -565,8 +567,8 @@ export class Backend<BC extends BackendClass = BackendClass> {
return { entries: hits, pagination: 1 };
}
async query<T extends BaseField = UnknownField>(
collection: Collection<T>,
async query<EF extends BaseField>(
collection: Collection<EF>,
searchFields: string[],
searchTerm: string,
file?: string,
@ -714,7 +716,11 @@ export class Backend<BC extends BackendClass = BackendClass> {
return localForage.removeItem(getEntryBackupKey());
}
async getEntry(state: RootState, collection: Collection, slug: string) {
async getEntry<EF extends BaseField>(
state: RootState<EF>,
collection: Collection<EF>,
slug: string,
) {
const path = selectEntryPath(collection, slug) as string;
const label = selectFileEntryLabel(collection, slug);
const extension = selectFolderEntryExtension(collection);
@ -762,7 +768,7 @@ export class Backend<BC extends BackendClass = BackendClass> {
return Promise.reject(err);
}
entryWithFormat(collection: Collection) {
entryWithFormat<EF extends BaseField>(collection: Collection<EF>) {
return (entry: Entry): Entry => {
const format = resolveFormat(collection, entry);
if (entry && entry.raw !== undefined) {
@ -777,7 +783,11 @@ export class Backend<BC extends BackendClass = BackendClass> {
};
}
async processEntry(state: RootState, collection: Collection, entry: Entry) {
async processEntry<EF extends BaseField>(
state: RootState<EF>,
collection: Collection<EF>,
entry: Entry,
) {
const configState = state.config;
if (!configState.config) {
throw new Error('Config not loaded');
@ -826,6 +836,8 @@ export class Backend<BC extends BackendClass = BackendClass> {
const newEntry = entryDraft.entry.newRecord ?? false;
const customPath = selectCustomPath(draft.entry, collection);
let dataFile: DataFile;
if (newEntry) {
if (!selectAllowNewEntries(collection)) {
@ -836,8 +848,9 @@ export class Backend<BC extends BackendClass = BackendClass> {
entryDraft.entry.data,
config,
usedSlugs,
customPath,
);
const path = selectEntryPath(collection, slug) ?? '';
const path = customPath || (selectEntryPath(collection, slug) ?? '');
dataFile = {
path,
slug,
@ -849,8 +862,9 @@ export class Backend<BC extends BackendClass = BackendClass> {
const slug = entryDraft.entry.slug;
dataFile = {
path: entryDraft.entry.path,
slug,
slug: customPath ? slugFromCustomPath(collection, customPath) : slug,
raw: this.entryToRaw(collection, entryDraft.entry),
newPath: customPath,
};
}
@ -938,7 +952,11 @@ export class Backend<BC extends BackendClass = BackendClass> {
return this.implementation.persistMedia(file, options);
}
async deleteEntry(state: RootState, collection: Collection, slug: string) {
async deleteEntry<EF extends BaseField>(
state: RootState<EF>,
collection: Collection<EF>,
slug: string,
) {
const configState = state.config;
if (!configState.config) {
throw new Error('Config not loaded');
@ -1012,7 +1030,7 @@ export class Backend<BC extends BackendClass = BackendClass> {
}
}
export function resolveBackend(config?: Config) {
export function resolveBackend<EF extends BaseField>(config?: Config<EF>) {
if (!config?.backend.name) {
throw new Error('No backend defined in configuration');
}
@ -1020,22 +1038,22 @@ export function resolveBackend(config?: Config) {
const { name } = config.backend;
const authStore = new LocalStorageAuthStore();
const backend = getBackend(name);
const backend = getBackend<EF>(name);
if (!backend) {
throw new Error(`Backend not found: ${name}`);
} else {
return new Backend(backend, { backendName: name, authStore, config });
return new Backend<EF, BackendClass>(backend, { backendName: name, authStore, config });
}
}
export const currentBackend = (function () {
let backend: Backend;
return <T extends BaseField = UnknownField>(config: Config<T>) => {
return <EF extends BaseField = UnknownField>(config: Config<EF>) => {
if (backend) {
return backend;
}
return (backend = resolveBackend(config as Config));
return (backend = resolveBackend(config) as unknown as Backend);
};
})();