Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new import/export comand #92

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
name: Tests

on:
push:
branches:
- main
- feature/export
on: [push]

jobs:
run-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install deps
run: npm install
- name: Jast run
run: npm test
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
- run: yarn install
- run: yarn test
9 changes: 1 addition & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,7 @@ This command exports data from the Flotiq account to local JSON files. If the ke
`flotiq import [projectName] [flotiqApiKey]`

This command imports Content Types and Content Objects to your Flotiq account using the API key.
Source directory must include directory with `ContentType[0-9]` folders, each of them containing ContentTypeDefinition.json file, and `contentObject[0-9].json` files.

The number at the end of the directory or file name defines the file import order.
The `./images` directory in a particular starter stores images that will be imported into your Media Library.
Source directory must include directory with `ContentType[Name]` folders, each of them containing ContentTypeDefinition.json file, and `contentObject[Name].json` files.

The command can import data output of the `flotiq export` command.

Expand All @@ -65,10 +62,6 @@ The command can import data output of the `flotiq export` command.
* `projectName` - project name or project path (if you wish to start or import data from the directory you are in, use `.`)
* `flotiqApiKey` - read and write API key to your Flotiq account

#### Note

It is also possible to perform the import and export manually, using the Flotiq API without relying on the CLI importer. However, when doing this, you need to ensure that image URLs are updated, as images will receive new IDs during the migration process. This requires careful handling to avoid broken links in your Content Objects. For more details on how to properly handle this, refer to the [Flotiq API documentation](https://flotiq.com/docs/).

### Launch a Flotiq starter project

`flotiq start [projectName] [flotiqStarterUrl] [flotiqApiKey]`
Expand Down
177 changes: 177 additions & 0 deletions commands/exporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#!/usr/bin/env node

const fs = require("fs/promises");
const path = require("path");
const fetch = require("node-fetch");
const logger = require("./../src/logger");
const { camelize } = require("./../src/util");
const FlotiqApi = require('./../src/flotiq-api')
const config = require("../src/configuration/config");

exports.command = "export";
exports.description = "Export flotiq entities to JSON structure";
exports.builder = {
target: {
description: "Export directory",
alias: "directory",
type: "string",
demand: true,
},
ctd: {
description: "Coma-delimited list of CTD to export",
type: "string",
},
skipContent: {
description: "Dump only CTD"
}
};

async function exporter(directory, flotiqApiUrl, flotiqApiKey, skipContent, ctd) {
try {
const files = await fs.readdir(directory);

if (files.length > 0) {
logger.error(`${directory} exists, but isn't empty`);
return false;
}
} catch (e) {
// Skip
}

await fs.mkdir(directory, { recursive: true });

const flotiqApi = new FlotiqApi(flotiqApiUrl, flotiqApiKey);

let ContentTypeDefinitions = await flotiqApi.fetchContentTypeDefs();

if (ctd) {
ctd.split(",").forEach((c) => {
if (!ContentTypeDefinitions.map((d) => d.name).includes(c)) {
throw new Error(`Invalid ctd "${c}"`);
}
});
ContentTypeDefinitions = ContentTypeDefinitions.filter((def) =>
ctd.split(",").includes(def.name)
);
}

if (ContentTypeDefinitions.length === 0) {
logger.info("Nothing to do");
return true;
}

for (const contentTypeDefinition of ContentTypeDefinitions) {
logger.info(`Saving CTD for ${contentTypeDefinition.label}`);

const ctdPath = path.join(
directory,
`${contentTypeDefinition.internal ? 'Internal' : ''}ContentType${camelize(contentTypeDefinition.name)}`
);

await fs.mkdir(ctdPath, { recursive: true });

const contentTypeDefinitionToPersist = Object.fromEntries(
Object.entries(contentTypeDefinition).filter(([key]) => {
return ![
"id",
// "internal",
"deletedAt",
"createdAt",
"updatedAt",
].includes(key);
})
);

await fs.writeFile(
path.join(ctdPath, "ContentTypeDefinition.json"),
JSON.stringify(contentTypeDefinitionToPersist, null, 2)
);

if (!skipContent) {

const ContentObjects = await flotiqApi.fetchContentObjects(contentTypeDefinition.name);

if (ContentObjects.length === 0) {
logger.info(`No content to save for ${contentTypeDefinition.label}`);
continue;
}

logger.info(`Saving content for ${contentTypeDefinition.label}`);

await fs.writeFile(
path.join(
ctdPath,
`contentObject${camelize(contentTypeDefinition.name)}.json`
),
ContentObjects
.map((obj) => ({ ...obj, internal: undefined }))
.sort((a, b) => a.id < b.id ? -1 : 1)
.map(JSON.stringify).join("\n")
);

if (contentTypeDefinition.name === '_media') {
for (const mediaFile of ContentObjects) {
const outputPath = path.join(
ctdPath,
`${mediaFile.id}.${mediaFile.extension}`
);

const url = new URL(flotiqApiUrl);

await fetch(`${url.origin}${mediaFile.url}`)
.then(x => x.arrayBuffer())
.then(x => fs.writeFile(outputPath, Buffer.from(x)));
}
}
}
}
return true;
}

async function handler(argv) {

const dirStat = await fs.lstat(argv.directory);

if (!dirStat.isDirectory()) {
logger.error(`${argv.directory} exists, but isn't directory`);
return false;
}

await exporter(
argv.directory,
`${config.apiUrl}/api/v1`,
argv.flotiqApiKey,
false
)
}

module.exports = {
command: 'export [directory] [flotiqApiKey]',
describe: 'Export objects from Flotiq to directory',
builder: (yargs) => {
return yargs
.option("directory", {
description: "Directory path to import data.",
alias: "",
type: "string",
default: "",
demandOption: false,
})
.option("flotiqApiKey", {
description: "Flotiq Read and write API KEY.",
alias: "",
type: "string",
default: false,
demandOption: false,
})
.option("only-definitions", {
Copy link
Contributor

@WHLukasz WHLukasz Dec 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only-definitions flag is not working anymore. We should either add suport for it in new exporter, or remove it from here & from readme

description: "Export only content type definitions, ignore content objects",
alias: "",
type: "boolean",
default: false,
demandOption: false,
})
},
handler,
exporter
}
Loading