So I will show you the code that worked for me. I have a few helpers to help along the way. I’ve put this in my frontend and I’m probably going to use a back door port on the inside of the front end which is only accessible on my Docker network so that Squidex can send me the webhook but my visitors can’t hit it.
All of this code should set up the sync function.
let config = {
//set up client
squidexAppName: '',
squidexClientId: '',
squidexClientSecret: '',
squidexEnvironment: '',
//...
}
export const enum SCHEMAS {
BRANDS = 'brands',
PAGES = 'pages',
MARKETPLACES = 'marketplaces',
PRODUCT_CATEGORIES = 'product-categories',
PRODUCTS = 'products',
SELLERS = 'sellers',
SLUGS = 'slugs',
//...
};
export enum SupportedLocales {
'en-US' = 'en-US',
'es-US' = 'es-US',
'fr-CA' = 'fr-CA',
//...
};
export interface Multilingual<T> {
[key: string]: T,
'en-US': T,
'es-US': T,
'fr-CA': T,
//...
};
export interface NonMultilingual<T> {
[key: string]: T,
iv: T,
}
export interface ContentDto<T> extends Squidex.ContentDto {
data?: T;
}
export interface ContentsDto<T> extends Squidex.ContentsDto {
/** The generic content items. */
items: ContentDto<T>[];
}
export interface Brand {
slug: Multilingual<string>,
//...
}
export interface Marketplace {
slug: Multilingual<string>,
//...
}
export interface Page {
slug: Multilingual<string>,
//...
}
export interface Product {
slug: Multilingual<string>,
//...
}
export interface ProductCategory {
slug: Multilingual<string>,
//...
}
export interface Seller {
slug: Multilingual<string>,
//...
}
/** This is our cache collection. **/
export interface Slug {
locale: NonMultilingual<SupportedLocales|string>,
localizedSlug: NonMultilingual<string>,
referenceSchema: NonMultilingual<SCHEMAS|string[]>,
reference: NonMultilingual<string[]>,
}
export const TIMEOUT_IN_SECONDS = 10;
export const client = new SquidexClient({
appName: config.squidexAppName!,
clientId: config.squidexClientId!,
clientSecret: config.squidexClientSecret!,
environment: config.squidexEnvironment!,
tokenStore: new SquidexClient.InMemoryTokenStore(),
// tokenStore: new SquidexStorageTokenStore() // Keep the tokens in the local store.
// tokenStore: new SquidexStorageTokenStore(sessionStorage, "CustomKey")
});
export const getContents = async <T>(schema: SCHEMAS|string) => (
await client.contents.getContents(schema, { }, { timeoutInSeconds: TIMEOUT_IN_SECONDS })
) as ContentsDto<T>;
export const getContentsUsingJsonQuery = async <T>(schema: SCHEMAS|string, jsonQuery: string|undefined = undefined) => (
await client.contents.getContents(schema, { q: jsonQuery }, { timeoutInSeconds: TIMEOUT_IN_SECONDS })
) as ContentsDto<T>;
export const getAllSlugs = async () =>
await getContents<Slug>(SCHEMAS.SLUGS);
Then this is what I did to sync it. Feel free to soak this method into your thoughts, because for me it is doing exactly what I need it to. If you wanted to set this up more generally you would extrapolate the process and restrict it to a single projection. But since I needed to roll my own, I set the whole thing up as a batch operation.
export async function performSyncLocalizedSlugs() {
console.log("Begin sync localized slugs.")
let allSlugs = await getAllSlugs();
let allPages = await getContentsUsingJsonQuery<Page>(SCHEMAS.PAGES);
let allBrands = await getContentsUsingJsonQuery<Brand>(SCHEMAS.BRANDS);
let allProducts = await getContentsUsingJsonQuery<Product>(SCHEMAS.PRODUCTS);
let allProductCategories = await getContentsUsingJsonQuery<ProductCategory>(SCHEMAS.PRODUCT_CATEGORIES);
let allSellers = await getContentsUsingJsonQuery<Seller>(SCHEMAS.SELLERS);
const locales = Object.values(SupportedLocales);
const findSlugInMultilingual = function<T>(slug: Slug, schema: SCHEMAS|string, contents: ContentsDto<T>) {
for (let i = 0; i < contents.items.length; i++) {
let item = contents.items[i];
for (let l = 0; l < locales.length; l++) {
let locale = locales[l];
let testSlug = (item.data! as any).slug[locale]
if (testSlug) {
if (slug.locale.iv === locale
&& slug.localizedSlug.iv === testSlug
&& slug.referenceSchema.iv === schema
&& slug.reference.iv.length === 1
&& slug.reference.iv[0] === item.id) {
return item;
}
}
}
}
}
const findSlugInSlugs = function(locale: SupportedLocales|string, slug: Multilingual<string>, schema: SCHEMAS|string, referenceId: string) {
for (let i = 0; i < allSlugs.items.length; i++) {
let testSlug = allSlugs.items[i].data!;
if (testSlug.localizedSlug.iv === slug[locale]
&& testSlug.locale.iv === locale
&& testSlug.referenceSchema.iv === schema
&& testSlug.reference.iv.length === 1
&& testSlug.reference.iv[0] === referenceId) {
return allSlugs.items[i];
}
}
}
let batchAddSlugsQueue: Slug[] = [];
allPages.items.forEach((page) => {
for (let l = 0; l < locales.length; l++) {
let locale = locales[l];
let foundSlugDto = findSlugInSlugs(locale, (page.data! as Page).slug, SCHEMAS.PAGES, page.id);
if (!foundSlugDto) {
//cache slug for page
batchAddSlugsQueue.push({
locale: { iv: locale },
localizedSlug: { iv: (page.data! as Page).slug[locale] },
referenceSchema: { iv: SCHEMAS.PAGES },
reference: { iv: [page.id] }
});
}
}
});
allBrands.items.forEach((brand) => {
for (let l = 0; l < locales.length; l++) {
let locale = locales[l];
let foundSlugDto = findSlugInSlugs(locale, (brand.data! as Brand).slug, SCHEMAS.BRANDS, brand.id);
if (!foundSlugDto) {
//cache slug for brand
batchAddSlugsQueue.push({
locale: { iv: locale },
localizedSlug: { iv: (brand.data! as Brand).slug[locale] },
referenceSchema: { iv: SCHEMAS.BRANDS },
reference: { iv: [brand.id] }
});
}
}
});
allProducts.items.forEach((product) => {
for (let l = 0; l < locales.length; l++) {
let locale = locales[l];
let foundSlugDto = findSlugInSlugs(locale, (product.data! as Product).slug, SCHEMAS.PRODUCTS, product.id);
if (!foundSlugDto) {
//cache slug for product
batchAddSlugsQueue.push({
locale: { iv: locale },
localizedSlug: { iv: (product.data! as Product).slug[locale] },
referenceSchema: { iv: SCHEMAS.PRODUCTS },
reference: { iv: [product.id] }
});
}
}
});
allProductCategories.items.forEach((productCategory) => {
for (let l = 0; l < locales.length; l++) {
let locale = locales[l];
let foundSlugDto = findSlugInSlugs(locale, (productCategory.data! as ProductCategory).slug, SCHEMAS.PRODUCT_CATEGORIES, productCategory.id);
if (!foundSlugDto) {
//cache slug for product category
batchAddSlugsQueue.push({
locale: { iv: locale },
localizedSlug: { iv: (productCategory.data! as ProductCategory).slug[locale] },
referenceSchema: { iv: SCHEMAS.PRODUCT_CATEGORIES },
reference: { iv: [productCategory.id] }
});
}
}
});
let batchRemoveSlugsQueue: string[] = [];
allSlugs.items.forEach((slugDto) => {
const doesSlugExistInPages = findSlugInMultilingual<Page>(slugDto.data! as Slug, SCHEMAS.PAGES, allPages);
const doesSlugExistInBrands = findSlugInMultilingual<Brand>(slugDto.data! as Slug, SCHEMAS.BRANDS, allBrands);
const doesSlugExistInProducts = findSlugInMultilingual<Product>(slugDto.data! as Slug, SCHEMAS.PRODUCTS, allProducts);
const doesSlugExistInProductCategories = findSlugInMultilingual<ProductCategory>(slugDto.data! as Slug, SCHEMAS.PRODUCT_CATEGORIES, allProductCategories);
const doesSlugExistInSellers = findSlugInMultilingual<Seller>(slugDto.data! as Slug, SCHEMAS.SELLERS, allSellers);
const doesSlugExistElsewhere = doesSlugExistInPages||doesSlugExistInBrands||doesSlugExistInProducts||doesSlugExistInProductCategories||doesSlugExistInSellers;
const shouldPruneOrphanSlug = !doesSlugExistElsewhere;
if (shouldPruneOrphanSlug) {
//prune orphan slugs from cache
batchRemoveSlugsQueue.push(slugDto.id);
}
});
const MAX_TIME_TO_POST_SLUGS = 60;//s
console.log("Add", batchAddSlugsQueue.length, "slugs");
console.log("Remove by id", batchRemoveSlugsQueue.length, "slugs");
let bulkAddResult = await core.client.contents.postContents(SCHEMAS.SLUGS, { datas: batchAddSlugsQueue as unknown as ContentData[], publish: true }, { timeoutInSeconds: MAX_TIME_TO_POST_SLUGS });
batchRemoveSlugsQueue.forEach(async (removeId) => {
await client.contents.deleteContent(SCHEMAS.SLUGS, removeId)
})
console.log("End sync localized slugs.")
}
performSyncLocalizedSlugs();