commit
75197b9070
|
@ -1,7 +1,12 @@
|
||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
end_of_line = lf
|
|
||||||
insert_final_newline = false
|
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
end_of_line = LF
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
|
@ -0,0 +1,6 @@
|
||||||
|
**/node_modules
|
||||||
|
**/public
|
||||||
|
**/build
|
||||||
|
**/config
|
||||||
|
**/scripts
|
||||||
|
**/xsl
|
|
@ -0,0 +1,505 @@
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"extends": ["react-app", "airbnb"],
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
"plugins": [
|
||||||
|
"babel",
|
||||||
|
"react",
|
||||||
|
"jsx-a11y",
|
||||||
|
"import",
|
||||||
|
"react-hooks"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es6": true,
|
||||||
|
"commonjs": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true
|
||||||
|
},
|
||||||
|
"ecmaVersion": 12,
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"strapi": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"template-curly-spacing" : "off",
|
||||||
|
|
||||||
|
"indent" : "off",
|
||||||
|
|
||||||
|
"react/jsx-props-no-spreading": "off",
|
||||||
|
|
||||||
|
"react-hooks/rules-of-hooks": "error",
|
||||||
|
|
||||||
|
"react-hooks/exhaustive-deps": "off",
|
||||||
|
|
||||||
|
"react/no-unused-prop-types": "warn",
|
||||||
|
|
||||||
|
"react/jsx-no-target-blank": "error",
|
||||||
|
|
||||||
|
"no-invalid-this": "off",
|
||||||
|
|
||||||
|
"babel/no-invalid-this": "error",
|
||||||
|
|
||||||
|
"arrow-spacing": "warn",
|
||||||
|
|
||||||
|
"implicit-arrow-linebreak": "warn",
|
||||||
|
|
||||||
|
"react/no-unused-state": "warn",
|
||||||
|
|
||||||
|
"react/boolean-prop-naming": "off",
|
||||||
|
|
||||||
|
"react/destructuring-assignment": ["warn", "always", { "ignoreClassFields": true }],
|
||||||
|
|
||||||
|
"react/no-access-state-in-setstate": "warn",
|
||||||
|
|
||||||
|
"operator-linebreak": "warn",
|
||||||
|
|
||||||
|
"no-useless-constructor": "warn",
|
||||||
|
|
||||||
|
"react/no-danger": "off",
|
||||||
|
|
||||||
|
"react/jsx-indent-props": "warn",
|
||||||
|
|
||||||
|
"react/jsx-curly-brace-presence": "warn",
|
||||||
|
|
||||||
|
"react/jsx-key": "error",
|
||||||
|
|
||||||
|
"react/jsx-boolean-value": "warn",
|
||||||
|
|
||||||
|
"react/jsx-closing-tag-location": "warn",
|
||||||
|
|
||||||
|
"import/extensions": "error",
|
||||||
|
|
||||||
|
"newline-per-chained-call": "warn",
|
||||||
|
|
||||||
|
"prefer-arrow-callback": "warn",
|
||||||
|
|
||||||
|
"block-spacing": "warn",
|
||||||
|
|
||||||
|
"one-var-declaration-per-line": "warn",
|
||||||
|
|
||||||
|
"prefer-const": "warn",
|
||||||
|
|
||||||
|
"import/first": "off",
|
||||||
|
|
||||||
|
"react/jsx-max-props-per-line": 1,
|
||||||
|
|
||||||
|
"react/jsx-first-prop-new-line": "warn",
|
||||||
|
|
||||||
|
"react/jsx-equals-spacing": "warn",
|
||||||
|
|
||||||
|
"react/jsx-indent": "warn",
|
||||||
|
|
||||||
|
"react/jsx-closing-bracket-location": "off",
|
||||||
|
|
||||||
|
"import/no-mutable-exports": "error",
|
||||||
|
|
||||||
|
"import/no-extraneous-dependencies": "off",
|
||||||
|
|
||||||
|
"object-shorthand": ["off", "never"],
|
||||||
|
|
||||||
|
"object-curly-newline": "off",
|
||||||
|
|
||||||
|
"arrow-body-style": "off",
|
||||||
|
|
||||||
|
"comma-dangle": ["warn", "always-multiline"],
|
||||||
|
|
||||||
|
"import/prefer-default-export": "off",
|
||||||
|
|
||||||
|
"no-cond-assign": "warn",
|
||||||
|
|
||||||
|
"no-confusing-arrow": "off",
|
||||||
|
|
||||||
|
"no-console": "off",
|
||||||
|
|
||||||
|
"no-constant-condition": "warn",
|
||||||
|
|
||||||
|
"no-control-regex": "warn",
|
||||||
|
|
||||||
|
"no-continue": "warn",
|
||||||
|
|
||||||
|
"react/forbid-prop-types": "warn",
|
||||||
|
|
||||||
|
"no-debugger": "warn",
|
||||||
|
|
||||||
|
"no-dupe-args": "error",
|
||||||
|
|
||||||
|
"no-dupe-keys": "error",
|
||||||
|
|
||||||
|
"no-duplicate-case": "error",
|
||||||
|
|
||||||
|
"no-empty": "warn",
|
||||||
|
|
||||||
|
"no-empty-character-class": "error",
|
||||||
|
|
||||||
|
"no-ex-assign": "error",
|
||||||
|
|
||||||
|
"no-extra-boolean-cast": "warn",
|
||||||
|
|
||||||
|
"no-extra-semi": "warn",
|
||||||
|
|
||||||
|
"no-func-assign": "error",
|
||||||
|
|
||||||
|
"no-inner-declarations": "error",
|
||||||
|
|
||||||
|
"no-invalid-regexp": "error",
|
||||||
|
|
||||||
|
"no-mixed-operators": "off",
|
||||||
|
|
||||||
|
"no-irregular-whitespace": "error",
|
||||||
|
|
||||||
|
"no-negated-in-lhs": "error",
|
||||||
|
|
||||||
|
"no-obj-calls": "error",
|
||||||
|
|
||||||
|
"no-regex-spaces": "warn",
|
||||||
|
|
||||||
|
"no-sparse-arrays": "error",
|
||||||
|
|
||||||
|
"no-unreachable": "warn",
|
||||||
|
|
||||||
|
"use-isnan": "error",
|
||||||
|
|
||||||
|
"valid-jsdoc": "warn",
|
||||||
|
|
||||||
|
"valid-typeof": "error",
|
||||||
|
|
||||||
|
"array-callback-return": "off",
|
||||||
|
|
||||||
|
"block-scoped-var": "off",
|
||||||
|
|
||||||
|
"prefer-destructuring": "warn",
|
||||||
|
|
||||||
|
"complexity": "off",
|
||||||
|
|
||||||
|
"consistent-return": "off",
|
||||||
|
|
||||||
|
"curly": "warn",
|
||||||
|
|
||||||
|
"default-case": "warn",
|
||||||
|
|
||||||
|
"dot-notation": "off",
|
||||||
|
|
||||||
|
"eqeqeq": "warn",
|
||||||
|
|
||||||
|
"guard-for-in": "off",
|
||||||
|
|
||||||
|
"no-alert": "warn",
|
||||||
|
|
||||||
|
"no-caller": "error",
|
||||||
|
|
||||||
|
"no-div-regex": "off",
|
||||||
|
|
||||||
|
"no-else-return": "off",
|
||||||
|
|
||||||
|
"no-eq-null": "error",
|
||||||
|
|
||||||
|
"no-eval": "error",
|
||||||
|
|
||||||
|
"no-extend-native": "error",
|
||||||
|
|
||||||
|
"no-extra-bind": "error",
|
||||||
|
|
||||||
|
"no-fallthrough": "error",
|
||||||
|
|
||||||
|
"no-floating-decimal": "error",
|
||||||
|
|
||||||
|
"no-implied-eval": "error",
|
||||||
|
|
||||||
|
"no-iterator": "error",
|
||||||
|
|
||||||
|
"no-labels": "off",
|
||||||
|
|
||||||
|
"no-lone-blocks": "warn",
|
||||||
|
|
||||||
|
"no-loop-func": "error",
|
||||||
|
|
||||||
|
"no-multi-spaces": "warn",
|
||||||
|
|
||||||
|
"no-multi-str": "error",
|
||||||
|
|
||||||
|
"no-native-reassign": "error",
|
||||||
|
|
||||||
|
"no-new": "warn",
|
||||||
|
|
||||||
|
"no-new-func": "error",
|
||||||
|
|
||||||
|
"no-new-wrappers": "error",
|
||||||
|
|
||||||
|
"no-octal": "error",
|
||||||
|
|
||||||
|
"no-octal-escape": "error",
|
||||||
|
|
||||||
|
"no-param-reassign": "off",
|
||||||
|
|
||||||
|
"no-process-env": "off",
|
||||||
|
|
||||||
|
"no-proto": "error",
|
||||||
|
|
||||||
|
"no-redeclare": "error",
|
||||||
|
|
||||||
|
"no-return-assign": "off",
|
||||||
|
|
||||||
|
"arrow-parens": ["warn", "always", { "requireForBlockBody": false }],
|
||||||
|
|
||||||
|
"no-script-url": "error",
|
||||||
|
|
||||||
|
"no-self-compare": "error",
|
||||||
|
|
||||||
|
"no-sequences": "error",
|
||||||
|
|
||||||
|
"no-throw-literal": "error",
|
||||||
|
|
||||||
|
"no-unused-expressions": "warn",
|
||||||
|
|
||||||
|
"no-void": "error",
|
||||||
|
|
||||||
|
"no-with": "error",
|
||||||
|
|
||||||
|
"radix": "off",
|
||||||
|
|
||||||
|
"vars-on-top": "off",
|
||||||
|
|
||||||
|
"wrap-iife": "error",
|
||||||
|
|
||||||
|
"yoda": "warn",
|
||||||
|
|
||||||
|
"strict": "off",
|
||||||
|
|
||||||
|
"no-catch-shadow": "error",
|
||||||
|
|
||||||
|
"no-delete-var": "error",
|
||||||
|
|
||||||
|
"no-label-var": "error",
|
||||||
|
|
||||||
|
"no-shadow": "warn",
|
||||||
|
|
||||||
|
"no-shadow-restricted-names": "error",
|
||||||
|
|
||||||
|
"no-undef": "error",
|
||||||
|
|
||||||
|
"no-undef-init": "error",
|
||||||
|
|
||||||
|
"no-multi-assign": "warn",
|
||||||
|
|
||||||
|
"no-undefined": "error",
|
||||||
|
|
||||||
|
"no-unused-vars": ["warn", { "args": "none", "ignoreRestSiblings": true }],
|
||||||
|
|
||||||
|
"no-use-before-define": [
|
||||||
|
"error",
|
||||||
|
{ "functions": false, "classes": true, "variables": true }
|
||||||
|
],
|
||||||
|
|
||||||
|
"no-restricted-properties": "warn",
|
||||||
|
|
||||||
|
"no-restricted-syntax": "warn",
|
||||||
|
|
||||||
|
"brace-style": "off",
|
||||||
|
|
||||||
|
"camelcase": "warn",
|
||||||
|
|
||||||
|
"comma-spacing": ["warn", { "before": false, "after": true }],
|
||||||
|
|
||||||
|
"comma-style": ["warn", "last"],
|
||||||
|
|
||||||
|
"consistent-this": ["off", "_this"],
|
||||||
|
|
||||||
|
"eol-last": "warn",
|
||||||
|
|
||||||
|
"func-names": "off",
|
||||||
|
|
||||||
|
"func-style": ["warn", "declaration", { "allowArrowFunctions": true }],
|
||||||
|
|
||||||
|
"key-spacing": ["warn", { "beforeColon": false, "afterColon": true }],
|
||||||
|
|
||||||
|
"max-nested-callbacks": ["warn", 5],
|
||||||
|
|
||||||
|
"new-cap": ["warn", { "newIsCap": true, "capIsNew": false }],
|
||||||
|
|
||||||
|
"new-parens": "warn",
|
||||||
|
|
||||||
|
"newline-after-var": "off",
|
||||||
|
|
||||||
|
"no-array-constructor": "off",
|
||||||
|
|
||||||
|
"no-inline-comments": "off",
|
||||||
|
|
||||||
|
"no-lonely-if": "warn",
|
||||||
|
|
||||||
|
"no-mixed-spaces-and-tabs": "warn",
|
||||||
|
|
||||||
|
"no-multiple-empty-lines": ["warn", { "max": 2 }],
|
||||||
|
|
||||||
|
"no-nested-ternary": "warn",
|
||||||
|
|
||||||
|
"no-new-object": "off",
|
||||||
|
|
||||||
|
"no-spaced-func": "warn",
|
||||||
|
|
||||||
|
"no-ternary": "off",
|
||||||
|
|
||||||
|
"no-trailing-spaces": "warn",
|
||||||
|
|
||||||
|
"no-underscore-dangle": "off",
|
||||||
|
|
||||||
|
"no-extra-parens": "off",
|
||||||
|
|
||||||
|
"padding-line-between-statements": "off",
|
||||||
|
|
||||||
|
"one-var": ["warn", "never"],
|
||||||
|
|
||||||
|
"operator-assignment": ["off", "never"],
|
||||||
|
|
||||||
|
"class-methods-use-this": "off",
|
||||||
|
|
||||||
|
"padded-blocks": ["off", "never"],
|
||||||
|
|
||||||
|
"lines-between-class-members": ["warn", "always"],
|
||||||
|
|
||||||
|
"quote-props": ["warn", "as-needed"],
|
||||||
|
|
||||||
|
"quotes": ["off", "single"],
|
||||||
|
|
||||||
|
"semi": ["warn", "always"],
|
||||||
|
|
||||||
|
"semi-spacing": ["warn", { "before": false, "after": true }],
|
||||||
|
|
||||||
|
"sort-vars": "off",
|
||||||
|
|
||||||
|
"keyword-spacing": ["warn", { "before": true, "after": true }],
|
||||||
|
|
||||||
|
"space-before-blocks": ["warn", "always"],
|
||||||
|
|
||||||
|
"function-paren-newline": "off",
|
||||||
|
|
||||||
|
"space-before-function-paren": ["warn", { "anonymous": "never", "named": "never" }],
|
||||||
|
|
||||||
|
"object-curly-spacing": ["warn", "always"],
|
||||||
|
|
||||||
|
"array-bracket-spacing": ["warn", "never"],
|
||||||
|
|
||||||
|
"computed-property-spacing": ["warn", "never"],
|
||||||
|
|
||||||
|
"space-in-parens": ["warn", "never"],
|
||||||
|
|
||||||
|
"space-infix-ops": "warn",
|
||||||
|
|
||||||
|
"space-unary-ops": ["warn", { "words": true, "nonwords": false }],
|
||||||
|
|
||||||
|
"spaced-comment": ["warn", "always"],
|
||||||
|
|
||||||
|
"wrap-regex": "off",
|
||||||
|
|
||||||
|
"no-var": "error",
|
||||||
|
|
||||||
|
"generator-star-spacing": ["error", "before"],
|
||||||
|
|
||||||
|
"max-depth": ["warn", 4],
|
||||||
|
|
||||||
|
"max-len": ["off", 80, 2],
|
||||||
|
|
||||||
|
"max-params": ["off", 99],
|
||||||
|
|
||||||
|
"max-statements": "off",
|
||||||
|
|
||||||
|
"no-bitwise": "off",
|
||||||
|
|
||||||
|
"no-plusplus": "off",
|
||||||
|
|
||||||
|
"react/display-name": "off",
|
||||||
|
|
||||||
|
"react/jsx-tag-spacing": "warn",
|
||||||
|
|
||||||
|
"jsx-quotes": ["warn", "prefer-double"],
|
||||||
|
|
||||||
|
"react/jsx-no-undef": "error",
|
||||||
|
|
||||||
|
"react/jsx-sort-props": "off",
|
||||||
|
|
||||||
|
"react/jsx-uses-react": "error",
|
||||||
|
|
||||||
|
"react/prefer-stateless-function": "warn",
|
||||||
|
|
||||||
|
"react/jsx-uses-vars": "error",
|
||||||
|
|
||||||
|
"react/jsx-no-bind": "error",
|
||||||
|
|
||||||
|
"react/no-did-mount-set-state": "warn",
|
||||||
|
|
||||||
|
"react/no-will-update-set-state": "warn",
|
||||||
|
|
||||||
|
"react/no-did-update-set-state": "warn",
|
||||||
|
|
||||||
|
"react/no-multi-comp": "off",
|
||||||
|
|
||||||
|
"react/no-unknown-property": "warn",
|
||||||
|
|
||||||
|
"react/prop-types": "off",
|
||||||
|
|
||||||
|
"react/react-in-jsx-scope": "error",
|
||||||
|
|
||||||
|
"react/self-closing-comp": "warn",
|
||||||
|
|
||||||
|
"react/jsx-wrap-multilines": "warn",
|
||||||
|
|
||||||
|
"react/no-array-index-key": "warn",
|
||||||
|
|
||||||
|
"react/no-unescaped-entities": "warn",
|
||||||
|
|
||||||
|
"react/sort-comp": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/no-static-element-interactions": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/click-events-have-key-events": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/no-noninteractive-element-interactions": "off",
|
||||||
|
|
||||||
|
"react/jsx-one-expression-per-line": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/anchor-is-valid": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/alt-text": "warn",
|
||||||
|
|
||||||
|
"jsx-a11y/label-has-for": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"required": {
|
||||||
|
"some": ["nesting", "id"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"jsx-a11y/img-redundant-alt": "warn",
|
||||||
|
|
||||||
|
"jsx-a11y/no-autofocus": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/iframe-has-title": "warn",
|
||||||
|
|
||||||
|
"jsx-a11y/anchor-has-content": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/label-has-associated-control": "warn",
|
||||||
|
|
||||||
|
"jsx-a11y/mouse-events-have-key-events": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/interactive-supports-focus": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/no-distracting-elements": "warn",
|
||||||
|
|
||||||
|
"jsx-a11y/heading-has-content": "warn",
|
||||||
|
|
||||||
|
"jsx-a11y/html-has-lang": "warn",
|
||||||
|
|
||||||
|
"jsx-a11y/href-no-hash": "off",
|
||||||
|
|
||||||
|
"react/jsx-filename-extension": "off",
|
||||||
|
|
||||||
|
"jsx-a11y/no-noninteractive-tabindex": "warn",
|
||||||
|
|
||||||
|
"jsx-a11y/media-has-caption": "off"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
---
|
||||||
|
name: 🐛 Bug Report
|
||||||
|
about: Create a report to help improve this plugin
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Hello 👋 Thank you for submitting an issue.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Bug report
|
||||||
|
|
||||||
|
### Describe the bug
|
||||||
|
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
### Steps to reproduce the behavior
|
||||||
|
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
### Expected behavior
|
||||||
|
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
### Screenshots
|
||||||
|
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
### Code snippets
|
||||||
|
|
||||||
|
If applicable, add code samples to help explain your problem.
|
||||||
|
|
||||||
|
### System
|
||||||
|
|
||||||
|
- Node.js version: <!-- Please ensure you are using the Node LTS version (v12 / v14) -->
|
||||||
|
- NPM version:
|
||||||
|
- Strapi version:
|
||||||
|
- Plugin version:
|
||||||
|
- Database:
|
||||||
|
- Operating system:
|
||||||
|
|
||||||
|
### Additional context
|
||||||
|
|
||||||
|
Add any other context about the problem here.
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
name: 🚀 Feature Request
|
||||||
|
about: Suggest an idea to help make this plugin even better!
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Hello 👋 Thank you for submitting a feature request.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Feature request
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Quick summary what's this feature request about.
|
||||||
|
|
||||||
|
### Why is it needed?
|
||||||
|
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
### Suggested solution(s)
|
||||||
|
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
### Related issue(s)/PR(s)
|
||||||
|
|
||||||
|
Let us know if this is related to any issue/pull request.
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!--
|
||||||
|
Hello 👋 Thank you for submitting a pull request.
|
||||||
|
|
||||||
|
To help us merge your PR, make sure to follow the instructions below:
|
||||||
|
|
||||||
|
- Create or update the documentation.
|
||||||
|
- Create or update the tests.
|
||||||
|
- Refer to the issue you are closing in the PR description - fix #issue
|
||||||
|
- Specify if the PR is in WIP (work in progress) state or ready to be merged
|
||||||
|
-->
|
||||||
|
|
||||||
|
### What does it do?
|
||||||
|
|
||||||
|
Describe the technical changes you did.
|
||||||
|
|
||||||
|
### Why is it needed?
|
||||||
|
|
||||||
|
Describe the issue you are solving.
|
||||||
|
|
||||||
|
### How to test it?
|
||||||
|
|
||||||
|
Provide information about the environment and the path to verify the behaviour.
|
||||||
|
|
||||||
|
### Related issue(s)/PR(s)
|
||||||
|
|
||||||
|
Let us know if this is related to any issue/pull request
|
Binary file not shown.
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 325 KiB |
|
@ -0,0 +1,37 @@
|
||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: 'lint'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '14'
|
||||||
|
cache: 'yarn'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn --ignore-scripts --frozen-lockfile
|
||||||
|
- name: Run eslint
|
||||||
|
run: yarn run eslint
|
||||||
|
# unit:
|
||||||
|
# name: 'unit'
|
||||||
|
# needs: [lint]
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# steps:
|
||||||
|
# - uses: actions/checkout@v2
|
||||||
|
# - uses: actions/setup-node@v2
|
||||||
|
# with:
|
||||||
|
# node-version: '14'
|
||||||
|
# cache: 'yarn'
|
||||||
|
# - name: Install dependencies
|
||||||
|
# run: yarn --ignore-scripts --frozen-lockfile
|
||||||
|
# - name: Run test
|
||||||
|
# run: yarn run -s test:unit
|
|
@ -3,7 +3,6 @@ coverage
|
||||||
node_modules
|
node_modules
|
||||||
stats.json
|
stats.json
|
||||||
package-lock.json
|
package-lock.json
|
||||||
yarn.lock
|
|
||||||
files
|
files
|
||||||
|
|
||||||
# Cruft
|
# Cruft
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
_
|
|
|
@ -1,39 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
Color_Off='\033[0m'
|
|
||||||
BRed="\033[1;31m" # Red
|
|
||||||
BGreen="\033[1;32m" # Green
|
|
||||||
BYellow="\033[1;33m" # Yellow
|
|
||||||
BBlue="\033[1;34m" # Blue
|
|
||||||
|
|
||||||
MSG_FILE=$1
|
|
||||||
FILE_CONTENT="$(cat $MSG_FILE)"
|
|
||||||
REGEX='(feat: |fix: |docs: |style: |refactor: |test: |chore: )'
|
|
||||||
ERROR_MSG="Commit message format must match regex \"${REGEX}\""
|
|
||||||
|
|
||||||
if [[ $FILE_CONTENT =~ $REGEX ]]; then
|
|
||||||
if [[ $FILE_CONTENT =~ (feat: ) ]]; then
|
|
||||||
printf '%s %s' ":sparkles:" "$(cat $MSG_FILE)" >$MSG_FILE
|
|
||||||
elif [[ $FILE_CONTENT =~ (fix: ) ]]; then
|
|
||||||
printf '%s %s' ":bug:" "$(cat $MSG_FILE)" >$MSG_FILE
|
|
||||||
elif [[ $FILE_CONTENT =~ (docs: ) ]]; then
|
|
||||||
printf '%s %s' ":memo:" "$(cat $MSG_FILE)" >$MSG_FILE
|
|
||||||
elif [[ $FILE_CONTENT =~ (style: ) ]]; then
|
|
||||||
printf '%s %s' ":art:" "$(cat $MSG_FILE)" >$MSG_FILE
|
|
||||||
elif [[ $FILE_CONTENT =~ (refactor: ) ]]; then
|
|
||||||
printf '%s %s' ":recycle:" "$(cat $MSG_FILE)" >$MSG_FILE
|
|
||||||
elif [[ $FILE_CONTENT =~ (test: ) ]]; then
|
|
||||||
printf '%s %s' ":white_check_mark:" "$(cat $MSG_FILE)" >$MSG_FILE
|
|
||||||
elif [[ $FILE_CONTENT =~ (chore: ) ]]; then
|
|
||||||
printf '%s %s' ":wrench:" "$(cat $MSG_FILE)" >$MSG_FILE
|
|
||||||
fi
|
|
||||||
printf "${BGreen}Good commit!${Color_Off}"
|
|
||||||
else
|
|
||||||
printf "${BRed}Bad commit ${BBlue}\"$FILE_CONTENT\"\n"
|
|
||||||
printf "${BYellow}$ERROR_MSG\n"
|
|
||||||
printf "The semantic git commit patten is expected, for details see https://gist.github.com/boazpoolman/42629d941b5f747734d296e02ecf737d\n"
|
|
||||||
printf "commit-msg hook failed (add --no-verify to bypass)\n"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
|
@ -1,21 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
Color_Off='\033[0m'
|
|
||||||
BRed="\033[1;31m" # Red
|
|
||||||
BGreen="\033[1;32m" # Green
|
|
||||||
BYellow="\033[1;33m" # Yellow
|
|
||||||
BBlue="\033[1;34m" # Blue
|
|
||||||
|
|
||||||
LOCAL_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
|
||||||
REGEX="^(main|master|develop)|(hotfix|release|feature|test|fix|chore|docs|refactor)\/[a-z0-9._-]+$"
|
|
||||||
ERROR_MSG="Branch name must match regex \"${REGEX}\""
|
|
||||||
|
|
||||||
if [[ ! $LOCAL_BRANCH =~ $REGEX ]]; then
|
|
||||||
printf "${BRed}Bad branch name ${BBlue}\"$LOCAL_BRANCH\"\n"
|
|
||||||
printf "${BYellow}$ERROR_MSG\n"
|
|
||||||
printf "The git-flow branch patten is expected, for details see https://gist.github.com/boazpoolman/42629d941b5f747734d296e02ecf737d\n"
|
|
||||||
printf "pre-commit hook failed (add --no-verify to bypass)\n"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
[@boazpoolman](https://twitter.com/boazpoolman) on twitter.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
|
@ -0,0 +1,97 @@
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project.
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
To get started with the project, make sure you have a local instance of Strapi running.
|
||||||
|
See the [Strapi docs](https://github.com/strapi/strapi#getting-started) on how to setup a Strapi project.
|
||||||
|
|
||||||
|
#### 1. Fork the [repository](https://github.com/boazpoolman/strapi-plugin-config-sync)
|
||||||
|
|
||||||
|
[Go to the repository](https://github.com/boazpoolman/strapi-plugin-config-sync) and fork it to your own GitHub account.
|
||||||
|
|
||||||
|
#### 2. Clone from your repository into the plugins folder
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd YOUR_STRAPI_PROJECT/src/plugins
|
||||||
|
git clone git@github.com:YOUR_USERNAME/strapi-plugin-config-sync.git config-sync
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Install the dependencies
|
||||||
|
|
||||||
|
Go to the plugin and install it's dependencies.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd YOUR_STRAPI_PROJECT/src/plugins/config-sync/ && yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Enable the plugin
|
||||||
|
|
||||||
|
Add the following lines to the `config/plugins.js` file in your Strapi project.
|
||||||
|
|
||||||
|
```
|
||||||
|
const path = require('path');
|
||||||
|
// ...
|
||||||
|
{
|
||||||
|
'config-sync': {
|
||||||
|
enabled: true,
|
||||||
|
resolve: path.resolve(__dirname, '../src/plugins/config-sync'),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Rebuild your Strapi project
|
||||||
|
|
||||||
|
Rebuild your strapi project to build the admin part of the plugin.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd YOUR_STRAPI_PROJECT && yarn build --clean
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. Running the administration panel in development mode
|
||||||
|
|
||||||
|
**Start the administration panel server for development**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd YOUR_STRAPI_PROJECT && yarn develop --watch-admin
|
||||||
|
```
|
||||||
|
|
||||||
|
The administration panel will be available at http://localhost:8080/admin
|
||||||
|
|
||||||
|
### Commit message convention
|
||||||
|
|
||||||
|
We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages:
|
||||||
|
|
||||||
|
- `fix`: bug fixes, e.g. fix crash due to deprecated method.
|
||||||
|
- `feat`: new features, e.g. add new method to the module.
|
||||||
|
- `refactor`: code refactor, e.g. migrate from class components to hooks.
|
||||||
|
- `docs`: changes into documentation, e.g. add usage example for the module..
|
||||||
|
- `test`: adding or updating tests, eg add integration tests using detox.
|
||||||
|
- `chore`: tooling changes, e.g. change CI config.
|
||||||
|
|
||||||
|
### Linting and tests
|
||||||
|
|
||||||
|
[ESLint](https://eslint.org/)
|
||||||
|
|
||||||
|
We use [ESLint](https://eslint.org/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing.
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
The `package.json` file contains various scripts for common tasks:
|
||||||
|
|
||||||
|
- `yarn eslint`: lint files with ESLint.
|
||||||
|
- `yarn eslint:fix`: auto-fix ESLint issues.
|
||||||
|
- `yarn test:unit`: run unit tests with Jest.
|
||||||
|
|
||||||
|
### Sending a pull request
|
||||||
|
|
||||||
|
> **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github).
|
||||||
|
|
||||||
|
When you're sending a pull request:
|
||||||
|
|
||||||
|
- Prefer small pull requests focused on one change.
|
||||||
|
- Verify that linters and tests are passing.
|
||||||
|
- Review the documentation to make sure it looks good.
|
||||||
|
- Follow the pull request template when opening a pull request.
|
||||||
|
- For pull requests that change the API or implementation, discuss with maintainers first by opening an issue.
|
326
README.md
326
README.md
|
@ -1,41 +1,252 @@
|
||||||
# Strapi Plugin Config Sync
|
<div align="center">
|
||||||
|
<h1>Strapi config-sync plugin</h1>
|
||||||
|
|
||||||
A lot of configuration of your Strapi project is stored in the database. Like core_store, user permissions, user roles & webhooks. Things you might want to have the same on all environments. But when you update them locally, you will have to manually update them on all other environments too.
|
<p style="margin-top: 0;">CLI & GUI for syncing config data across environments.</p>
|
||||||
|
|
||||||
That's where this plugin comes in to play. It allows you to export these configs as individual JSON files for each config, and write them somewhere in your project. With the configs written in your filesystem you can keep track of them through version control (git), and easily pull and import them across environments.
|
<p>
|
||||||
|
<a href="https://www.npmjs.org/package/strapi-plugin-config-sync">
|
||||||
|
<img src="https://img.shields.io/npm/v/strapi-plugin-config-sync/latest.svg" alt="NPM Version" />
|
||||||
|
</a>
|
||||||
|
<a href="https://www.npmjs.org/package/strapi-plugin-config-sync">
|
||||||
|
<img src="https://img.shields.io/npm/dm/strapi-plugin-config-sync" alt="Monthly download on NPM" />
|
||||||
|
</a>
|
||||||
|
<a href="https://codecov.io/gh/boazpoolman/strapi-plugin-config-sync">
|
||||||
|
<img src="https://img.shields.io/github/workflow/status/boazpoolman/strapi-plugin-config-sync/Tests/master" alt="CI build status" />
|
||||||
|
</a>
|
||||||
|
<a href="https://codecov.io/gh/boazpoolman/strapi-plugin-config-sync">
|
||||||
|
<img src="https://codecov.io/gh/boazpoolman/strapi-plugin-config-sync/coverage.svg?branch=master" alt="codecov.io" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
Importing, exporting and keeping track of config changes is done in the admin page of the plugin.
|
## ✨ Features
|
||||||
|
|
||||||
**THIS PLUGIN IS NOT STABLE**
|
- **CLI** (`config-sync` CLI for syncing the config from the command line)
|
||||||
|
- **GUI** (Settings page for syncing the config in Strapi admin)
|
||||||
|
- **Partial sync** (Import or export only specific portions of config)
|
||||||
|
- **Exclude configs** (Exclude specific config from the sync)
|
||||||
|
|
||||||
**PLEASE USE WITH CARE**
|
## ⏳ Installation
|
||||||
|
|
||||||
<img src=".github/config-diff.png" alt="Strapi config-sync changes" />
|
Install the plugin in your Strapi project.
|
||||||
|
|
||||||
## Installation
|
```bash
|
||||||
|
# using yarn
|
||||||
|
yarn add strapi-plugin-config-sync
|
||||||
|
|
||||||
Use `npm` or `yarn` to install and build the plugin.
|
# using npm
|
||||||
|
npm install strapi-plugin-config-sync --save
|
||||||
|
```
|
||||||
|
|
||||||
yarn add strapi-plugin-config-sync
|
Add the export path to the `watchIgnoreFiles` list in the `config/admin.js` file.
|
||||||
yarn build
|
|
||||||
yarn develop
|
|
||||||
|
|
||||||
Add the export path to the `watchIgnoreFiles` list in `config/server.js`.
|
|
||||||
This way your app won't reload when you export the config in development.
|
This way your app won't reload when you export the config in development.
|
||||||
|
|
||||||
##### `config/server.js`:
|
##### `config/admin.js`:
|
||||||
|
```
|
||||||
|
module.exports = ({ env }) => ({
|
||||||
|
// ...
|
||||||
|
watchIgnoreFiles: [
|
||||||
|
'**/config-sync/files/**',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
admin: {
|
After successful installation you have to rebuild the admin UI so it'll include this plugin. To rebuild and restart Strapi run:
|
||||||
auth: {
|
|
||||||
// ...
|
|
||||||
},
|
|
||||||
watchIgnoreFiles: [
|
|
||||||
'**/config-sync/files/**',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# using yarn
|
||||||
|
yarn build --clean
|
||||||
|
yarn develop
|
||||||
|
|
||||||
## Settings
|
# using npm
|
||||||
|
npm run build --clean
|
||||||
|
npm run develop
|
||||||
|
```
|
||||||
|
|
||||||
|
The **Config Sync** plugin should appear in the **Plugins** section of Strapi sidebar after you run app again.
|
||||||
|
|
||||||
|
Enjoy 🎉
|
||||||
|
|
||||||
|
## 🖐 Requirements
|
||||||
|
|
||||||
|
Complete installation requirements are the exact same as for Strapi itself and can be found in the [Strapi documentation](https://strapi.io/documentation).
|
||||||
|
|
||||||
|
**Supported Strapi versions**:
|
||||||
|
|
||||||
|
- Strapi 4.0.0 (recently tested)
|
||||||
|
- Strapi ^4.x
|
||||||
|
- Strapi ^3.4.x (use `strapi-plugin-config-sync@0.1.6`)
|
||||||
|
|
||||||
|
(This plugin may work with older Strapi versions, but these are not tested nor officially supported at this time.)
|
||||||
|
|
||||||
|
**We recommend always using the latest version of Strapi to start your new projects**.
|
||||||
|
|
||||||
|
## 💡 Motivation
|
||||||
|
In Strapi we come across what I would call config types. These are models of which the records are stored in our database, just like content types. Though the big difference here is that your code ofter relies on the database records of these types.
|
||||||
|
|
||||||
|
Having said that, it makes sense that these records can be exported, added to git, and be migrated across environments. This way we can make sure we have all the data our code relies on, on each environment.
|
||||||
|
|
||||||
|
Examples of these types are:
|
||||||
|
|
||||||
|
- Admin roles _(admin::role)_
|
||||||
|
- User roles _(plugin::users-permissions.role)_
|
||||||
|
- Admin settings _(strapi::core-store)_
|
||||||
|
- I18n locale _(plugin::i18n.locale)_
|
||||||
|
|
||||||
|
This plugin gives you the tools to sync this data. You can export the data as JSON files on one env, and import them on every other env. By writing this data as JSON files you can easily track them in your version control system (git).
|
||||||
|
|
||||||
|
_With great power comes great responsibility - Spider-Man_
|
||||||
|
|
||||||
|
## 🔌 Command line interface (CLI)
|
||||||
|
|
||||||
|
Add the `config-sync` command as a script to the `package.json` of your Strapi project:
|
||||||
|
|
||||||
|
```
|
||||||
|
"scripts": {
|
||||||
|
// ...
|
||||||
|
"cs": "config-sync"
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
You can now run all the `config-sync` commands like this:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# using yarn
|
||||||
|
yarn cs --help
|
||||||
|
|
||||||
|
# using npm
|
||||||
|
npm run cs --help
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⬆️ Import ⬇️ Export
|
||||||
|
|
||||||
|
> _Command:_ `import` _Alias:_ `i`
|
||||||
|
>
|
||||||
|
> _Command:_ `export` _Alias:_ `e`
|
||||||
|
|
||||||
|
These commands are used to sync the config in your Strapi project.
|
||||||
|
|
||||||
|
_Example:_
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# using yarn
|
||||||
|
yarn cs import
|
||||||
|
yarn cs export
|
||||||
|
|
||||||
|
# using npm
|
||||||
|
npm run cs import
|
||||||
|
npm run cs export
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Flag: `--yes` `-y`
|
||||||
|
|
||||||
|
Use this flag to skip the confirm prompt and go straight to syncing the config.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[command] --yes
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Flag: `--type` `-t`
|
||||||
|
|
||||||
|
Use this flag to specify the type of config you want to sync.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[command] --type user-role
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Flag: `--partial` `-p`
|
||||||
|
|
||||||
|
Use this flag to sync a specific set of configs by giving the CLI a comma-separated string of config names.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[command] --partial user-role.public,i18n-locale.en
|
||||||
|
```
|
||||||
|
### ↔️ Diff
|
||||||
|
|
||||||
|
> _Command:_ `diff` | _Alias:_ `d`
|
||||||
|
|
||||||
|
This command is used to see the difference between the config as found in the sync directory, and the config as found in the database.
|
||||||
|
|
||||||
|
_Example:_
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# using yarn
|
||||||
|
yarn cs diff
|
||||||
|
|
||||||
|
# using npm
|
||||||
|
npm run cs diff
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🖥️ Admin panel (GUI)
|
||||||
|
This plugin ships with a settings page which can be accessed from the admin panel of Strapi. On this page you can pretty much do the same as you can from the CLI. You can import, export and see the difference between the config as found in the sync directory, and the config as found in the database.
|
||||||
|
|
||||||
|
**Pro tip:**
|
||||||
|
By clicking on one of the items in the diff table you can see the exact difference between sync dir and database in a git-style diff viewer.
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/boazpoolman/strapi-plugin-config-sync/master/.github/config-diff.png" alt="Config diff in admin" />
|
||||||
|
|
||||||
|
## ⌨️ Usage / Workflow
|
||||||
|
This plugin works best when you use `git` for the version control of your Strapi project. When you do so, with this plugin you are able to version control your config data through files.
|
||||||
|
|
||||||
|
_The following workflows are assuming you're using `git`._
|
||||||
|
|
||||||
|
### Local development
|
||||||
|
When building a new feature locally for your Strapi project you'd use the following workflow:
|
||||||
|
|
||||||
|
- Build the feature.
|
||||||
|
- Export the config.
|
||||||
|
- Commit and push the files to git.
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
When deploying the newly created feature - to either a server, or a co-worker's machine - you'd use the following workflow:
|
||||||
|
|
||||||
|
- Pull the latest file changes to the environment.
|
||||||
|
- (Re)start your Strapi instance.
|
||||||
|
- Import the config.
|
||||||
|
|
||||||
|
### Production deployment
|
||||||
|
When deploying to production you'd use the same deployment workflow as described above. But before you do, you have to take some extra precautions to ensure no data will be lost:
|
||||||
|
|
||||||
|
- Run `yarn cs diff` to verify there are no config changes that could be overwritten.
|
||||||
|
- If there have been changes made;
|
||||||
|
- Export these before you pull the new config.
|
||||||
|
- Commit and push the exported files to git.
|
||||||
|
- If needed; merge into the branch you were about to pull.
|
||||||
|
- Continue with the regular deployment workflow.
|
||||||
|
|
||||||
|
Try to avoid making config changes directly on production. You wouldn't want to change something like API permissions (roles) on production without it being in your version control.
|
||||||
|
|
||||||
|
## 🚀 Config types
|
||||||
|
|
||||||
|
### Admin role
|
||||||
|
|
||||||
|
> Prefix: `admin-role` | UID: `code` | Query string: `admin::role`
|
||||||
|
|
||||||
|
### User role
|
||||||
|
|
||||||
|
> Prefix: `user-role` | UID: `type` | Query string: `plugin::users-permissions.role`
|
||||||
|
|
||||||
|
### Core store
|
||||||
|
|
||||||
|
> Prefix: `core-store` | UID: `key` | Query string: `strapi::core-store`
|
||||||
|
|
||||||
|
### I18n locale
|
||||||
|
|
||||||
|
> Prefix: `i81n-locale` | UID: `code` | Query string: `plugin::i18n.locale`
|
||||||
|
|
||||||
|
## 🔍 Naming convention
|
||||||
|
All the config files written in the sync directory have the same naming convention. It goes as follows:
|
||||||
|
|
||||||
|
[config-type].[config-name].json
|
||||||
|
|
||||||
|
- `config-type` - Corresponds to the `prefix` of the config type.
|
||||||
|
- `config-name` - The unique identifier of the config.
|
||||||
|
- For `core-store` config this is the `key` value.
|
||||||
|
- For `user-role` config this is the `type` value.
|
||||||
|
- For `admin-role` config this is the `code` value.
|
||||||
|
- For `i18n-locale` config this is the `code` value
|
||||||
|
|
||||||
|
## 🔧 Settings
|
||||||
The settings of the plugin can be overridden in the `config/plugins.js` file.
|
The settings of the plugin can be overridden in the `config/plugins.js` file.
|
||||||
In the example below you can see how, and also what the default settings are.
|
In the example below you can see how, and also what the default settings are.
|
||||||
|
|
||||||
|
@ -43,18 +254,22 @@ In the example below you can see how, and also what the default settings are.
|
||||||
module.exports = ({ env }) => ({
|
module.exports = ({ env }) => ({
|
||||||
// ...
|
// ...
|
||||||
'config-sync': {
|
'config-sync': {
|
||||||
destination: "extensions/config-sync/files/",
|
enabled: true,
|
||||||
minify: false,
|
config: {
|
||||||
importOnBootstrap: false,
|
destination: "extensions/config-sync/files/",
|
||||||
include: [
|
minify: false,
|
||||||
"core-store",
|
importOnBootstrap: false,
|
||||||
"role-permissions"
|
include: [
|
||||||
],
|
"core-store",
|
||||||
exclude: [
|
"user-role"
|
||||||
"core-store.plugin_users-permissions_grant"
|
"admin-role"
|
||||||
]
|
"i18n-locale"
|
||||||
|
],
|
||||||
|
exclude: [
|
||||||
|
"core-store.plugin_users-permissions_grant"
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// ...
|
|
||||||
});
|
});
|
||||||
|
|
||||||
| Property | Type | Description |
|
| Property | Type | Description |
|
||||||
|
@ -62,42 +277,27 @@ In the example below you can see how, and also what the default settings are.
|
||||||
| destination | string | The path for reading and writing the sync files. |
|
| destination | string | The path for reading and writing the sync files. |
|
||||||
| minify | bool | When enabled all the exported JSON files will be minified. |
|
| minify | bool | When enabled all the exported JSON files will be minified. |
|
||||||
| importOnBootstrap | bool | Allows you to let the config be imported automaticly when strapi is bootstrapping (on `strapi start`). This setting should only be used in production, and should be handled very carefully as it can unintendedly overwrite the changes in your database. PLEASE USE WITH CARE. |
|
| importOnBootstrap | bool | Allows you to let the config be imported automaticly when strapi is bootstrapping (on `strapi start`). This setting should only be used in production, and should be handled very carefully as it can unintendedly overwrite the changes in your database. PLEASE USE WITH CARE. |
|
||||||
| include | array | Configs types you want to include in the syncing process. Allowed values: `core-store`, `role-permissions`, `webhooks`. |
|
| include | array | Types you want to include in the syncing process. Allowed values: `core-store`, `user-role`, `admin-role`, `i18n-locale`. |
|
||||||
| exclude | array | Specify the names of configs you want to exclude from the syncing process. By default the API tokens for users-permissions, which are stored in core_store, are excluded. This setting expects the config names to comply with the naming convention. |
|
| exclude | array | Specify the names of configs you want to exclude from the syncing process. By default the API tokens for users-permissions, which are stored in core_store, are excluded. This setting expects the config names to comply with the naming convention. |
|
||||||
|
|
||||||
## Naming convention
|
## 🤝 Contributing
|
||||||
All the config files written in the file destination have the same naming convention. It goes as follows:
|
|
||||||
|
|
||||||
[config-type].[config-name].json
|
Feel free to fork and make a pull request of this plugin. All the input is welcome!
|
||||||
|
|
||||||
- `config-type` - Corresponds to the value in from the include setting.
|
|
||||||
- `config-name` - The unique identifier of the config.
|
|
||||||
- For `core-store` config this is the `key` value.
|
|
||||||
- For `role-permissions` config this is the `type` value.
|
|
||||||
- For `webhooks` config this is the `id` value
|
|
||||||
|
|
||||||
|
|
||||||
## TODOs
|
|
||||||
- ~~Exporting of user roles & permissions~~
|
|
||||||
- ~~Exporting of webhooks~~
|
|
||||||
- ~~Specify which tables you want to track in the plugin configurations~~
|
|
||||||
- Exporting of EE roles & permissions
|
|
||||||
- Add partial import/export functionality
|
|
||||||
- Add CLI commands for importing/exporting
|
|
||||||
- ~~Track config deletions~~
|
|
||||||
|
|
||||||
## ⭐️ Show your support
|
## ⭐️ Show your support
|
||||||
|
|
||||||
Give a star if this project helped you.
|
Give a star if this project helped you.
|
||||||
|
|
||||||
## Credits
|
## 🔗 Links
|
||||||
Shout out to [@ScottAgirs](https://github.com/ScottAgirs) for making [strapi-plugin-migrate](https://github.com/ijsto/strapi-plugin-migrate) as it was a big help while making the config-sync plugin.
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
- [MIT License](LICENSE.md)
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- [NPM package](https://www.npmjs.com/package/strapi-plugin-config-sync)
|
- [NPM package](https://www.npmjs.com/package/strapi-plugin-config-sync)
|
||||||
- [GitHub repository](https://github.com/boazpoolman/strapi-plugin-config-sync)
|
- [GitHub repository](https://github.com/boazpoolman/strapi-plugin-config-sync)
|
||||||
|
|
||||||
|
## 🌎 Community support
|
||||||
|
|
||||||
|
- For general help using Strapi, please refer to [the official Strapi documentation](https://strapi.io/documentation/).
|
||||||
|
- For support with this plugin you can DM me in the Strapi Discord [channel](https://discord.strapi.io/).
|
||||||
|
|
||||||
|
## 📝 Resources
|
||||||
|
|
||||||
|
- [MIT License](LICENSE.md)
|
||||||
|
|
|
@ -1,15 +1,20 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import { Button } from '@buffetjs/core';
|
import { Button } from '@strapi/design-system/Button';
|
||||||
|
import { Map } from 'immutable';
|
||||||
|
import { useNotification } from '@strapi/helper-plugin';
|
||||||
|
|
||||||
import ConfirmModal from '../ConfirmModal';
|
import ConfirmModal from '../ConfirmModal';
|
||||||
import { exportAllConfig, importAllConfig } from '../../state/actions/Config';
|
import { exportAllConfig, importAllConfig } from '../../state/actions/Config';
|
||||||
|
|
||||||
const ActionButtons = ({ diff }) => {
|
const ActionButtons = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const toggleNotification = useNotification();
|
||||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||||
const [actionType, setActionType] = useState('');
|
const [actionType, setActionType] = useState('');
|
||||||
|
const partialDiff = useSelector((state) => state.getIn(['config', 'partialDiff'], Map({}))).toJS();
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
setActionType('');
|
setActionType('');
|
||||||
|
@ -23,23 +28,25 @@ const ActionButtons = ({ diff }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionButtonsStyling>
|
<ActionButtonsStyling>
|
||||||
<Button disabled={isEmpty(diff.diff)} color="primary" label="Import" onClick={() => openModal('import')} />
|
<Button disabled={isEmpty(partialDiff)} onClick={() => openModal('import')}>Import</Button>
|
||||||
<Button disabled={isEmpty(diff.diff)} color="primary" label="Export" onClick={() => openModal('export')} />
|
<Button disabled={isEmpty(partialDiff)} onClick={() => openModal('export')}>Export</Button>
|
||||||
{!isEmpty(diff.diff) && (
|
{!isEmpty(partialDiff) && (
|
||||||
<h4 style={{ display: 'inline' }}>{Object.keys(diff.diff).length} {Object.keys(diff.diff).length === 1 ? "config change" : "config changes"}</h4>
|
<h4 style={{ display: 'inline' }}>{Object.keys(partialDiff).length} {Object.keys(partialDiff).length === 1 ? "config change" : "config changes"}</h4>
|
||||||
)}
|
)}
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={modalIsOpen}
|
isOpen={modalIsOpen}
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
type={actionType}
|
type={actionType}
|
||||||
onSubmit={() => actionType === 'import' ? dispatch(importAllConfig()) : dispatch(exportAllConfig())}
|
onSubmit={() => actionType === 'import' ? dispatch(importAllConfig(partialDiff, toggleNotification)) : dispatch(exportAllConfig(partialDiff, toggleNotification))}
|
||||||
/>
|
/>
|
||||||
</ActionButtonsStyling>
|
</ActionButtonsStyling>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const ActionButtonsStyling = styled.div`
|
const ActionButtonsStyling = styled.div`
|
||||||
padding: 10px 0 20px 0;
|
padding: 10px 0 20px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
> button {
|
> button {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
|
|
@ -1,49 +1,44 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer';
|
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer';
|
||||||
import { AttributeIcon } from '@buffetjs/core';
|
|
||||||
import {
|
|
||||||
HeaderModal,
|
|
||||||
HeaderModalTitle,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
} from 'strapi-helper-plugin';
|
|
||||||
|
|
||||||
const ConfigDiff = ({ isOpen, onClose, onToggle, oldValue, newValue, configName }) => {
|
import { ModalLayout, ModalBody, ModalHeader } from '@strapi/design-system/ModalLayout';
|
||||||
|
import { ButtonText } from '@strapi/design-system/Text';
|
||||||
|
import { Grid, GridItem } from '@strapi/design-system/Grid';
|
||||||
|
import { Typography } from '@strapi/design-system/Typography';
|
||||||
|
|
||||||
|
const ConfigDiff = ({ isOpen, onClose, oldValue, newValue, configName }) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<ModalLayout
|
||||||
isOpen={isOpen}
|
onClose={onClose}
|
||||||
onClosed={onClose}
|
labelledBy="title"
|
||||||
onToggle={onToggle}
|
|
||||||
>
|
>
|
||||||
<HeaderModal>
|
<ModalHeader>
|
||||||
<section style={{ alignItems: 'center' }}>
|
<ButtonText textColor="neutral800" as="h2" id="title">
|
||||||
<AttributeIcon type='enum' />
|
Config changes for {configName}
|
||||||
<HeaderModalTitle style={{ marginLeft: 15 }}>Config changes for {configName}</HeaderModalTitle>
|
</ButtonText>
|
||||||
</section>
|
</ModalHeader>
|
||||||
</HeaderModal>
|
<ModalBody>
|
||||||
<ModalBody style={{
|
<Grid paddingBottom={4} style={{ textAlign: 'center' }}>
|
||||||
paddingTop: '0.5rem',
|
<GridItem col={6}>
|
||||||
paddingBottom: '3rem'
|
<Typography variant="delta">Sync directory</Typography>
|
||||||
}}>
|
</GridItem>
|
||||||
<div className="container-fluid">
|
<GridItem col={6}>
|
||||||
<section style={{ marginTop: 20 }}>
|
<Typography variant="delta">Database</Typography>
|
||||||
<ReactDiffViewer
|
</GridItem>
|
||||||
oldValue={JSON.stringify(oldValue, null, 2)}
|
</Grid>
|
||||||
newValue={JSON.stringify(newValue, null, 2)}
|
<ReactDiffViewer
|
||||||
splitView={true}
|
oldValue={JSON.stringify(oldValue, null, 2)}
|
||||||
compareMethod={DiffMethod.WORDS}
|
newValue={JSON.stringify(newValue, null, 2)}
|
||||||
/>
|
splitView
|
||||||
</section>
|
compareMethod={DiffMethod.WORDS}
|
||||||
</div>
|
/>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
</ModalLayout>
|
||||||
<section style={{ alignItems: 'center' }}>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
</ModalFooter>
|
|
||||||
</Modal>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ConfigDiff;
|
export default ConfigDiff;
|
|
@ -1,52 +1,65 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import { Tr, Td } from '@strapi/design-system/Table';
|
||||||
|
import { BaseCheckbox } from '@strapi/design-system/BaseCheckbox';
|
||||||
|
|
||||||
const CustomRow = ({ row }) => {
|
const CustomRow = ({ row, checked, updateValue }) => {
|
||||||
const { config_name, config_type, state, onClick } = row;
|
const { configName, configType, state, onClick } = row;
|
||||||
|
|
||||||
|
const stateStyle = (stateStr) => {
|
||||||
|
const style = {
|
||||||
|
display: 'inline-flex',
|
||||||
|
padding: '0 10px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
height: '24px',
|
||||||
|
alignItems: 'center',
|
||||||
|
fontWeight: '500',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (stateStr === 'Only in DB') {
|
||||||
|
style.backgroundColor = '#cbf2d7';
|
||||||
|
style.color = '#1b522b';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateStr === 'Only in sync dir') {
|
||||||
|
style.backgroundColor = '#f0cac7';
|
||||||
|
style.color = '#3d302f';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateStr === 'Different') {
|
||||||
|
style.backgroundColor = '#e8e6b7';
|
||||||
|
style.color = '#4a4934';
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr onClick={() => onClick(config_type, config_name)}>
|
<Tr
|
||||||
<td>
|
onClick={(e) => {
|
||||||
<p>{config_name}</p>
|
if (e.target.type !== 'checkbox') {
|
||||||
</td>
|
onClick(configType, configName);
|
||||||
<td>
|
}
|
||||||
<p>{config_type}</p>
|
}}
|
||||||
</td>
|
style={{ cursor: 'pointer' }}
|
||||||
<td>
|
>
|
||||||
|
<Td>
|
||||||
|
<BaseCheckbox
|
||||||
|
aria-label={`Select ${configName}`}
|
||||||
|
value={checked}
|
||||||
|
onValueChange={updateValue}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<p>{configName}</p>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<p>{configType}</p>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
<p style={stateStyle(state)}>{state}</p>
|
<p style={stateStyle(state)}>{state}</p>
|
||||||
</td>
|
</Td>
|
||||||
</tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateStyle = (state) => {
|
export default CustomRow;
|
||||||
let style = {
|
|
||||||
display: 'inline-flex',
|
|
||||||
padding: '0 10px',
|
|
||||||
borderRadius: '12px',
|
|
||||||
height: '24px',
|
|
||||||
alignItems: 'center',
|
|
||||||
fontWeight: '500',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (state === 'Only in DB') {
|
|
||||||
style.backgroundColor = '#cbf2d7';
|
|
||||||
style.color = '#1b522b';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state === 'Only in sync dir') {
|
|
||||||
style.backgroundColor = '#f0cac7';
|
|
||||||
style.color = '#3d302f';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state === 'Different') {
|
|
||||||
style.backgroundColor = '#e8e6b7';
|
|
||||||
style.color = '#4a4934';
|
|
||||||
}
|
|
||||||
|
|
||||||
return style;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export default CustomRow
|
|
||||||
|
|
|
@ -1,48 +1,43 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Table } from '@buffetjs/core';
|
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import { Table, Thead, Tbody, Tr, Th } from '@strapi/design-system/Table';
|
||||||
|
import { TableLabel } from '@strapi/design-system/Text';
|
||||||
|
import { BaseCheckbox } from '@strapi/design-system/BaseCheckbox';
|
||||||
|
import { Loader } from '@strapi/design-system/Loader';
|
||||||
|
|
||||||
import ConfigDiff from '../ConfigDiff';
|
import ConfigDiff from '../ConfigDiff';
|
||||||
import FirstExport from '../FirstExport';
|
import FirstExport from '../FirstExport';
|
||||||
|
import NoChanges from '../NoChanges';
|
||||||
import ConfigListRow from './ConfigListRow';
|
import ConfigListRow from './ConfigListRow';
|
||||||
|
import { setConfigPartialDiffInState } from '../../state/actions/Config';
|
||||||
const headers = [
|
|
||||||
{
|
|
||||||
name: 'Config name',
|
|
||||||
value: 'config_name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Config type',
|
|
||||||
value: 'config_type',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'State',
|
|
||||||
value: 'state',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const ConfigList = ({ diff, isLoading }) => {
|
const ConfigList = ({ diff, isLoading }) => {
|
||||||
const [openModal, setOpenModal] = useState(false);
|
const [openModal, setOpenModal] = useState(false);
|
||||||
const [originalConfig, setOriginalConfig] = useState({});
|
const [originalConfig, setOriginalConfig] = useState({});
|
||||||
const [newConfig, setNewConfig] = useState({});
|
const [newConfig, setNewConfig] = useState({});
|
||||||
const [configName, setConfigName] = useState('');
|
const [cName, setCname] = useState('');
|
||||||
const [rows, setRows] = useState([]);
|
const [rows, setRows] = useState([]);
|
||||||
|
const [checkedItems, setCheckedItems] = useState([]);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const getConfigState = (configName) => {
|
const getConfigState = (configName) => {
|
||||||
if (
|
if (
|
||||||
diff.fileConfig[configName] &&
|
diff.fileConfig[configName]
|
||||||
diff.databaseConfig[configName]
|
&& diff.databaseConfig[configName]
|
||||||
) {
|
) {
|
||||||
return 'Different'
|
return 'Different';
|
||||||
} else if (
|
} else if (
|
||||||
diff.fileConfig[configName] &&
|
diff.fileConfig[configName]
|
||||||
!diff.databaseConfig[configName]
|
&& !diff.databaseConfig[configName]
|
||||||
) {
|
) {
|
||||||
return 'Only in sync dir'
|
return 'Only in sync dir';
|
||||||
} else if (
|
} else if (
|
||||||
!diff.fileConfig[configName] &&
|
!diff.fileConfig[configName]
|
||||||
diff.databaseConfig[configName]
|
&& diff.databaseConfig[configName]
|
||||||
) {
|
) {
|
||||||
return 'Only in DB'
|
return 'Only in DB';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -52,38 +47,65 @@ const ConfigList = ({ diff, isLoading }) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let formattedRows = [];
|
const formattedRows = [];
|
||||||
Object.keys(diff.diff).map((configName) => {
|
const newCheckedItems = [];
|
||||||
const type = configName.split('.')[0]; // Grab the first part of the filename.
|
Object.keys(diff.diff).map((name) => {
|
||||||
const name = configName.split(/\.(.+)/)[1]; // Grab the rest of the filename minus the file extension.
|
const type = name.split('.')[0]; // Grab the first part of the filename.
|
||||||
|
const formattedName = name.split(/\.(.+)/)[1]; // Grab the rest of the filename minus the file extension.
|
||||||
|
|
||||||
|
newCheckedItems.push(true);
|
||||||
|
|
||||||
formattedRows.push({
|
formattedRows.push({
|
||||||
config_name: name,
|
configName: formattedName,
|
||||||
config_type: type,
|
configType: type,
|
||||||
state: getConfigState(configName),
|
state: getConfigState(name),
|
||||||
onClick: (config_type, config_name) => {
|
onClick: (configType, configName) => {
|
||||||
setOriginalConfig(diff.fileConfig[`${config_type}.${config_name}`]);
|
setOriginalConfig(diff.fileConfig[`${configType}.${configName}`]);
|
||||||
setNewConfig(diff.databaseConfig[`${config_type}.${config_name}`]);
|
setNewConfig(diff.databaseConfig[`${configType}.${configName}`]);
|
||||||
setConfigName(`${config_type}.${config_name}`);
|
setCname(`${configType}.${configName}`);
|
||||||
setOpenModal(true);
|
setOpenModal(true);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
setCheckedItems(newCheckedItems);
|
||||||
|
|
||||||
setRows(formattedRows);
|
setRows(formattedRows);
|
||||||
}, [diff]);
|
}, [diff]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newPartialDiff = [];
|
||||||
|
checkedItems.map((item, index) => {
|
||||||
|
if (item && rows[index]) newPartialDiff.push(`${rows[index].configType}.${rows[index].configName}`);
|
||||||
|
});
|
||||||
|
dispatch(setConfigPartialDiffInState(newPartialDiff));
|
||||||
|
}, [checkedItems]);
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
setOriginalConfig({});
|
setOriginalConfig({});
|
||||||
setNewConfig({});
|
setNewConfig({});
|
||||||
setConfigName('');
|
setCname('');
|
||||||
setOpenModal(false);
|
setOpenModal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isLoading && !isEmpty(diff.message)) {
|
if (isLoading) {
|
||||||
return <FirstExport />
|
return (
|
||||||
|
<div style={{ textAlign: 'center', marginTop: 40 }}>
|
||||||
|
<Loader>Loading content...</Loader>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isLoading && !isEmpty(diff.message)) {
|
||||||
|
return <FirstExport />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoading && isEmpty(diff.diff)) {
|
||||||
|
return <NoChanges />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allChecked = checkedItems && checkedItems.every(Boolean);
|
||||||
|
const isIndeterminate = checkedItems.some(Boolean) && !allChecked;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ConfigDiff
|
<ConfigDiff
|
||||||
|
@ -91,18 +113,46 @@ const ConfigList = ({ diff, isLoading }) => {
|
||||||
oldValue={originalConfig}
|
oldValue={originalConfig}
|
||||||
newValue={newConfig}
|
newValue={newConfig}
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
onToggle={closeModal}
|
configName={cName}
|
||||||
configName={configName}
|
|
||||||
/>
|
|
||||||
<Table
|
|
||||||
headers={headers}
|
|
||||||
customRow={ConfigListRow}
|
|
||||||
rows={!isLoading ? rows : []}
|
|
||||||
isLoading={isLoading}
|
|
||||||
tableEmptyText="No config changes. You are up to date!"
|
|
||||||
/>
|
/>
|
||||||
|
<Table colCount={4} rowCount={rows.length + 1}>
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>
|
||||||
|
<BaseCheckbox
|
||||||
|
aria-label="Select all entries"
|
||||||
|
indeterminate={isIndeterminate}
|
||||||
|
onValueChange={(value) => setCheckedItems(checkedItems.map(() => value))}
|
||||||
|
value={allChecked}
|
||||||
|
/>
|
||||||
|
</Th>
|
||||||
|
<Th>
|
||||||
|
<TableLabel>Config name</TableLabel>
|
||||||
|
</Th>
|
||||||
|
<Th>
|
||||||
|
<TableLabel>Config type</TableLabel>
|
||||||
|
</Th>
|
||||||
|
<Th>
|
||||||
|
<TableLabel>State</TableLabel>
|
||||||
|
</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{rows.map((row, index) => (
|
||||||
|
<ConfigListRow
|
||||||
|
key={row.configName}
|
||||||
|
row={row}
|
||||||
|
checked={checkedItems[index]}
|
||||||
|
updateValue={() => {
|
||||||
|
checkedItems[index] = !checkedItems[index];
|
||||||
|
setCheckedItems([...checkedItems]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ConfigList;
|
export default ConfigList;
|
|
@ -1,34 +1,58 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { useIntl } from 'react-intl';
|
||||||
ModalConfirm,
|
|
||||||
} from 'strapi-helper-plugin';
|
|
||||||
|
|
||||||
import getTrad from '../../helpers/getTrad';
|
import { Dialog, DialogBody, DialogFooter } from '@strapi/design-system/Dialog';
|
||||||
|
import { Flex } from '@strapi/design-system/Flex';
|
||||||
|
import { Text } from '@strapi/design-system/Text';
|
||||||
|
import { Stack } from '@strapi/design-system/Stack';
|
||||||
|
import { Button } from '@strapi/design-system/Button';
|
||||||
|
import ExclamationMarkCircle from '@strapi/icons/ExclamationMarkCircle';
|
||||||
|
|
||||||
const ConfirmModal = ({ isOpen, onClose, onSubmit, type }) => {
|
const ConfirmModal = ({ isOpen, onClose, onSubmit, type }) => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalConfirm
|
<Dialog
|
||||||
confirmButtonLabel={{
|
onClose={onClose}
|
||||||
id: getTrad(`popUpWarning.button.${type}`),
|
title="Confirmation"
|
||||||
}}
|
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
toggle={onClose}
|
>
|
||||||
onClosed={onClose}
|
<DialogBody icon={<ExclamationMarkCircle />}>
|
||||||
onConfirm={() => {
|
<Stack size={2}>
|
||||||
onClose();
|
<Flex justifyContent="center">
|
||||||
onSubmit();
|
<Text id="confirm-description" style={{ textAlign: 'center' }}>
|
||||||
}}
|
{formatMessage({ id: `config-sync.popUpWarning.warning.${type}_1` })}<br />
|
||||||
type="success"
|
{formatMessage({ id: `config-sync.popUpWarning.warning.${type}_2` })}
|
||||||
content={{
|
</Text>
|
||||||
id: getTrad(`popUpWarning.warning.${type}`),
|
</Flex>
|
||||||
values: {
|
</Stack>
|
||||||
br: () => <br />,
|
</DialogBody>
|
||||||
},
|
<DialogFooter
|
||||||
}}
|
startAction={(
|
||||||
/>
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
variant="tertiary"
|
||||||
|
>
|
||||||
|
{formatMessage({ id: 'config-sync.popUpWarning.button.cancel' })}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
endAction={(
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
onClose();
|
||||||
|
onSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatMessage({ id: `config-sync.popUpWarning.button.${type}` })}
|
||||||
|
</Button>
|
||||||
|
)} />
|
||||||
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ConfirmModal;
|
export default ConfirmModal;
|
|
@ -1,24 +0,0 @@
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const ContainerFluid = styled.div`
|
|
||||||
padding: 18px 30px;
|
|
||||||
> div:first-child {
|
|
||||||
max-height: 33px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonOutline {
|
|
||||||
height: 30px;
|
|
||||||
padding: 0 15px;
|
|
||||||
border: 1px solid #dfe0e1;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 13px;
|
|
||||||
&:before {
|
|
||||||
margin-right: 10px;
|
|
||||||
content: '\f08e';
|
|
||||||
font-family: 'FontAwesome';
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default ContainerFluid;
|
|
|
@ -1,33 +1,34 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { Button } from '@buffetjs/core';
|
import { NoContent, useNotification } from '@strapi/helper-plugin';
|
||||||
|
import { Button } from '@strapi/design-system/Button';
|
||||||
|
|
||||||
import { exportAllConfig } from '../../state/actions/Config';
|
import { exportAllConfig } from '../../state/actions/Config';
|
||||||
import ConfirmModal from '../ConfirmModal';
|
import ConfirmModal from '../ConfirmModal';
|
||||||
|
|
||||||
const FirstExport = () => {
|
const FirstExport = () => {
|
||||||
|
const toggleNotification = useNotification();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div>
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
flexDirection: 'column',
|
|
||||||
textAlign: 'center',
|
|
||||||
height: '300px',
|
|
||||||
}}>
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={modalIsOpen}
|
isOpen={modalIsOpen}
|
||||||
onClose={() => setModalIsOpen(false)}
|
onClose={() => setModalIsOpen(false)}
|
||||||
type={'export'}
|
type="export"
|
||||||
onSubmit={() => dispatch(exportAllConfig())}
|
onSubmit={() => dispatch(exportAllConfig([], toggleNotification))}
|
||||||
|
/>
|
||||||
|
<NoContent
|
||||||
|
content={{
|
||||||
|
id: 'emptyState',
|
||||||
|
defaultMessage:
|
||||||
|
'Looks like this is your first time using config-sync for this project.',
|
||||||
|
}}
|
||||||
|
action={<Button onClick={() => setModalIsOpen(true)}>Make the initial export</Button>}
|
||||||
/>
|
/>
|
||||||
<h3>Looks like this is your first time using config-sync for this project.</h3>
|
|
||||||
<p>Make the initial export!</p>
|
|
||||||
<Button color="primary" onClick={() => setModalIsOpen(true)}>Initial export</Button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default FirstExport;
|
export default FirstExport;
|
|
@ -5,21 +5,22 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
import { Header } from '@buffetjs/custom';
|
import { useIntl } from 'react-intl';
|
||||||
import { useGlobalContext } from 'strapi-helper-plugin';
|
|
||||||
|
import { HeaderLayout } from '@strapi/design-system/Layout';
|
||||||
|
import { Box } from '@strapi/design-system/Box';
|
||||||
|
|
||||||
const HeaderComponent = () => {
|
const HeaderComponent = () => {
|
||||||
const { formatMessage } = useGlobalContext();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
const headerProps = {
|
|
||||||
title: {
|
|
||||||
label: formatMessage({ id: 'config-sync.Header.Title' }),
|
|
||||||
},
|
|
||||||
content: formatMessage({ id: 'config-sync.Header.Description' }),
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Header {...headerProps} />
|
<Box background="neutral100">
|
||||||
|
<HeaderLayout
|
||||||
|
title={formatMessage({ id: 'config-sync.Header.Title' })}
|
||||||
|
subtitle={formatMessage({ id: 'config-sync.Header.Description' })}
|
||||||
|
as="h2"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { NoContent } from '@strapi/helper-plugin';
|
||||||
|
|
||||||
|
const NoChanges = () => (
|
||||||
|
<NoContent
|
||||||
|
content={{
|
||||||
|
id: 'emptyState',
|
||||||
|
defaultMessage:
|
||||||
|
'No differences between DB and sync directory. You are up-to-date!',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default NoChanges;
|
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* PluginIcon
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Icon } from '@strapi/design-system/Icon';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
|
import pluginPkg from '../../../../package.json';
|
||||||
|
|
||||||
|
const PluginIcon = () => <Icon as={() => <FontAwesomeIcon icon={pluginPkg.strapi.icon} />} width="16px" />;
|
||||||
|
|
||||||
|
export default PluginIcon;
|
|
@ -1 +1 @@
|
||||||
export const __DEBUG__ = strapi.env === 'development';
|
export const __DEBUG__ = true; // TODO: set actual env.
|
||||||
|
|
|
@ -7,20 +7,21 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import ContainerFluid from '../../components/Container';
|
import { CheckPagePermissions } from '@strapi/helper-plugin';
|
||||||
import Header from '../../components/Header';
|
|
||||||
|
|
||||||
|
import pluginPermissions from '../../permissions';
|
||||||
|
import Header from '../../components/Header';
|
||||||
import { store } from "../../helpers/configureStore";
|
import { store } from "../../helpers/configureStore";
|
||||||
import ConfigPage from '../ConfigPage';
|
import ConfigPage from '../ConfigPage';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<CheckPagePermissions permissions={pluginPermissions.settings}>
|
||||||
<ContainerFluid>
|
<Provider store={store}>
|
||||||
<Header />
|
<Header />
|
||||||
<ConfigPage />
|
<ConfigPage />
|
||||||
</ContainerFluid>
|
</Provider>
|
||||||
</Provider>
|
</CheckPagePermissions>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,40 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { Map } from 'immutable';
|
import { Map } from 'immutable';
|
||||||
|
import { Box } from '@strapi/design-system/Box';
|
||||||
|
import { useNotification } from '@strapi/helper-plugin';
|
||||||
|
import { Alert } from '@strapi/design-system/Alert';
|
||||||
|
import { Typography } from '@strapi/design-system/Typography';
|
||||||
|
|
||||||
import { getAllConfigDiff } from '../../state/actions/Config';
|
import { getAllConfigDiff } from '../../state/actions/Config';
|
||||||
import ConfigList from '../../components/ConfigList';
|
import ConfigList from '../../components/ConfigList';
|
||||||
import ActionButtons from '../../components/ActionButtons';
|
import ActionButtons from '../../components/ActionButtons';
|
||||||
|
|
||||||
const ConfigPage = () => {
|
const ConfigPage = () => {
|
||||||
|
const toggleNotification = useNotification();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const isLoading = useSelector((state) => state.getIn(['config', 'isLoading'], Map({})));
|
const isLoading = useSelector((state) => state.getIn(['config', 'isLoading'], Map({})));
|
||||||
const configDiff = useSelector((state) => state.getIn(['config', 'configDiff'], Map({})));
|
const configDiff = useSelector((state) => state.getIn(['config', 'configDiff'], Map({})));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(getAllConfigDiff());
|
dispatch(getAllConfigDiff(toggleNotification));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Box paddingLeft={8} paddingRight={8} paddingBottom={8}>
|
||||||
<ActionButtons diff={configDiff.toJS()} />
|
{process.env.NODE_ENV === 'production' && (
|
||||||
|
<Box paddingBottom={4}>
|
||||||
|
<Alert variant="danger">
|
||||||
|
<Typography variant="omega" fontWeight="bold">You're in the production environment</Typography><br />
|
||||||
|
Please be careful when syncing your config in production.<br />
|
||||||
|
Make sure you are not overriding critical config changes on import.
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<ActionButtons />
|
||||||
<ConfigList isLoading={isLoading} diff={configDiff.toJS()} />
|
<ConfigList isLoading={isLoading} diff={configDiff.toJS()} />
|
||||||
</div>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ConfigPage;
|
export default ConfigPage;
|
|
@ -1,14 +1,12 @@
|
||||||
import { createStore, applyMiddleware, compose } from 'redux';
|
import { createStore, applyMiddleware, compose } from 'redux';
|
||||||
import { createLogger } from 'redux-logger';
|
|
||||||
import thunkMiddleware from 'redux-thunk';
|
import thunkMiddleware from 'redux-thunk';
|
||||||
import { Map } from 'immutable';
|
import { Map } from 'immutable';
|
||||||
|
|
||||||
import rootReducer from '../state/reducers';
|
import rootReducer from '../state/reducers';
|
||||||
import loggerConfig from '../config/logger';
|
|
||||||
import { __DEBUG__ } from '../config/constants';
|
import { __DEBUG__ } from '../config/constants';
|
||||||
|
|
||||||
const configureStore = () => {
|
const configureStore = () => {
|
||||||
let initialStoreState = Map();
|
const initialStoreState = Map();
|
||||||
|
|
||||||
const enhancers = [];
|
const enhancers = [];
|
||||||
const middlewares = [
|
const middlewares = [
|
||||||
|
@ -27,24 +25,6 @@ const configureStore = () => {
|
||||||
if (devtools) {
|
if (devtools) {
|
||||||
console.info('[setup] ✓ Enabling Redux DevTools Extension');
|
console.info('[setup] ✓ Enabling Redux DevTools Extension');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.info('[setup] ✓ Enabling state logger');
|
|
||||||
const loggerMiddleware = createLogger({
|
|
||||||
level: 'info',
|
|
||||||
collapsed: true,
|
|
||||||
stateTransformer: (state) => state.toJS(),
|
|
||||||
predicate: (getState, action) => {
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const showBlacklisted = state.getIn(['debug', 'logs', 'blacklisted']);
|
|
||||||
if (loggerConfig.blacklist.indexOf(action.type) !== -1 && !showBlacklisted) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return state.getIn(['debug', 'logs', 'enabled']);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
middlewares.push(loggerMiddleware);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const composedEnhancers = devtools || compose;
|
const composedEnhancers = devtools || compose;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import pluginId from './pluginId';
|
import pluginId from './pluginId';
|
||||||
|
|
||||||
const getTrad = id => `${pluginId}.${id}`;
|
const getTrad = (id) => `${pluginId}.${id}`;
|
||||||
|
|
||||||
export default getTrad;
|
export default getTrad;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const pluginPkg = require('../../../package.json');
|
const pluginPkg = require('../../../package.json');
|
||||||
|
|
||||||
const pluginId = pluginPkg.name.replace(
|
const pluginId = pluginPkg.name.replace(
|
||||||
/^strapi-plugin-/i,
|
/^strapi-plugin-/i,
|
||||||
''
|
''
|
||||||
|
|
|
@ -1,53 +1,62 @@
|
||||||
import React from 'react';
|
import { prefixPluginTranslations } from '@strapi/helper-plugin';
|
||||||
import pluginPkg from '../../package.json';
|
import pluginPkg from '../../package.json';
|
||||||
import pluginId from './helpers/pluginId';
|
import pluginId from './helpers/pluginId';
|
||||||
import App from './containers/App';
|
import pluginIcon from './components/PluginIcon';
|
||||||
import Initializer from './containers/Initializer';
|
import pluginPermissions from './permissions';
|
||||||
import trads from './translations';
|
// import getTrad from './helpers/getTrad';
|
||||||
|
|
||||||
function Comp(props) {
|
const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
|
||||||
return <App {...props} />;
|
const { name } = pluginPkg.strapi;
|
||||||
}
|
|
||||||
|
|
||||||
export default strapi => {
|
export default {
|
||||||
const pluginDescription =
|
register(app) {
|
||||||
pluginPkg.strapi.description || pluginPkg.description;
|
app.registerPlugin({
|
||||||
|
description: pluginDescription,
|
||||||
|
id: pluginId,
|
||||||
|
isReady: true,
|
||||||
|
isRequired: pluginPkg.strapi.required || false,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
|
||||||
const icon = pluginPkg.strapi.icon;
|
app.addMenuLink({
|
||||||
const name = pluginPkg.strapi.name;
|
to: `/plugins/${pluginId}`,
|
||||||
|
icon: pluginIcon,
|
||||||
|
intlLabel: {
|
||||||
|
id: `${pluginId}.plugin.name`,
|
||||||
|
defaultMessage: 'Config Sync',
|
||||||
|
},
|
||||||
|
Component: async () => {
|
||||||
|
const component = await import(
|
||||||
|
/* webpackChunkName: "config-sync-settings-page" */ './containers/App'
|
||||||
|
);
|
||||||
|
|
||||||
const plugin = {
|
return component;
|
||||||
icon,
|
},
|
||||||
name,
|
permissions: pluginPermissions['menu-link'],
|
||||||
destination: `/plugins/${pluginId}`,
|
});
|
||||||
blockerComponent: null,
|
},
|
||||||
blockerComponentProps: {},
|
bootstrap(app) {},
|
||||||
description: pluginDescription,
|
async registerTrads({ locales }) {
|
||||||
id: pluginId,
|
const importedTrads = await Promise.all(
|
||||||
initializer: Initializer,
|
locales.map((locale) => {
|
||||||
injectedComponents: [],
|
return import(
|
||||||
isReady: false,
|
/* webpackChunkName: "config-sync-translation-[request]" */ `./translations/${locale}.json`
|
||||||
layout: null,
|
)
|
||||||
leftMenuLinks: [],
|
.then(({ default: data }) => {
|
||||||
leftMenuSections: [],
|
return {
|
||||||
mainComponent: Comp,
|
data: prefixPluginTranslations(data, pluginId),
|
||||||
name: pluginPkg.strapi.name,
|
locale,
|
||||||
preventComponentRendering: false,
|
};
|
||||||
trads,
|
})
|
||||||
menu: {
|
.catch(() => {
|
||||||
pluginsSectionLinks: [
|
return {
|
||||||
{
|
data: {},
|
||||||
destination: `/plugins/${pluginId}`, // Endpoint of the link
|
locale,
|
||||||
icon,
|
};
|
||||||
name,
|
});
|
||||||
label: {
|
})
|
||||||
id: `${pluginId}.plugin.name`, // Refers to a i18n
|
);
|
||||||
defaultMessage: 'Config Sync',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return strapi.registerPlugin(plugin);
|
return Promise.resolve(importedTrads);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
const pluginPermissions = {
|
||||||
|
// This permission regards the main component (App) and is used to tell
|
||||||
|
// If the plugin link should be displayed in the menu
|
||||||
|
// And also if the plugin is accessible. This use case is found when a user types the url of the
|
||||||
|
// plugin directly in the browser
|
||||||
|
'menu-link': [{ action: 'plugin::config-sync.menu-link', subject: null }],
|
||||||
|
settings: [{ action: 'plugin::config-sync.settings.read', subject: null }],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default pluginPermissions;
|
|
@ -4,21 +4,21 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { request } from 'strapi-helper-plugin';
|
import { request } from '@strapi/helper-plugin';
|
||||||
import { Map } from 'immutable';
|
|
||||||
|
|
||||||
export function getAllConfigDiff() {
|
export function getAllConfigDiff(toggleNotification) {
|
||||||
return async function(dispatch) {
|
return async function(dispatch) {
|
||||||
dispatch(setLoadingState(true));
|
dispatch(setLoadingState(true));
|
||||||
try {
|
try {
|
||||||
const configDiff = await request('/config-sync/diff', { method: 'GET' });
|
const configDiff = await request('/config-sync/diff', { method: 'GET' });
|
||||||
|
dispatch(setConfigPartialDiffInState([]));
|
||||||
dispatch(setConfigDiffInState(configDiff));
|
dispatch(setConfigDiffInState(configDiff));
|
||||||
dispatch(setLoadingState(false));
|
dispatch(setLoadingState(false));
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
strapi.notification.error('notification.error');
|
toggleNotification({ type: 'warning', message: { id: 'notification.error' } });
|
||||||
dispatch(setLoadingState(false));
|
dispatch(setLoadingState(false));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SET_CONFIG_DIFF_IN_STATE = 'SET_CONFIG_DIFF_IN_STATE';
|
export const SET_CONFIG_DIFF_IN_STATE = 'SET_CONFIG_DIFF_IN_STATE';
|
||||||
|
@ -29,36 +29,48 @@ export function setConfigDiffInState(config) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function exportAllConfig() {
|
export const SET_CONFIG_PARTIAL_DIFF_IN_STATE = 'SET_CONFIG_PARTIAL_DIFF_IN_STATE';
|
||||||
return async function(dispatch) {
|
export function setConfigPartialDiffInState(config) {
|
||||||
dispatch(setLoadingState(true));
|
return {
|
||||||
try {
|
type: SET_CONFIG_PARTIAL_DIFF_IN_STATE,
|
||||||
const { message } = await request('/config-sync/export', { method: 'GET' });
|
config,
|
||||||
dispatch(setConfigDiffInState(Map({})));
|
};
|
||||||
|
|
||||||
strapi.notification.success(message);
|
|
||||||
dispatch(setLoadingState(false));
|
|
||||||
} catch(err) {
|
|
||||||
strapi.notification.error('notification.error');
|
|
||||||
dispatch(setLoadingState(false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function importAllConfig() {
|
export function exportAllConfig(partialDiff, toggleNotification) {
|
||||||
return async function(dispatch) {
|
return async function(dispatch) {
|
||||||
dispatch(setLoadingState(true));
|
dispatch(setLoadingState(true));
|
||||||
try {
|
try {
|
||||||
const { message } = await request('/config-sync/import', { method: 'GET' });
|
const { message } = await request('/config-sync/export', {
|
||||||
dispatch(setConfigDiffInState(Map({})));
|
method: 'POST',
|
||||||
|
body: partialDiff,
|
||||||
strapi.notification.success(message);
|
});
|
||||||
|
toggleNotification({ type: 'success', message });
|
||||||
|
dispatch(getAllConfigDiff(toggleNotification));
|
||||||
dispatch(setLoadingState(false));
|
dispatch(setLoadingState(false));
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
strapi.notification.error('notification.error');
|
toggleNotification({ type: 'warning', message: { id: 'notification.error' } });
|
||||||
dispatch(setLoadingState(false));
|
dispatch(setLoadingState(false));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importAllConfig(partialDiff, toggleNotification) {
|
||||||
|
return async function(dispatch) {
|
||||||
|
dispatch(setLoadingState(true));
|
||||||
|
try {
|
||||||
|
const { message } = await request('/config-sync/import', {
|
||||||
|
method: 'POST',
|
||||||
|
body: partialDiff,
|
||||||
|
});
|
||||||
|
toggleNotification({ type: 'success', message });
|
||||||
|
dispatch(getAllConfigDiff(toggleNotification));
|
||||||
|
dispatch(setLoadingState(false));
|
||||||
|
} catch (err) {
|
||||||
|
toggleNotification({ type: 'warning', message: { id: 'notification.error' } });
|
||||||
|
dispatch(setLoadingState(false));
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SET_LOADING_STATE = 'SET_LOADING_STATE';
|
export const SET_LOADING_STATE = 'SET_LOADING_STATE';
|
||||||
|
|
|
@ -4,11 +4,12 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { fromJS, Map } from 'immutable';
|
import { fromJS, Map, List } from 'immutable';
|
||||||
import { SET_CONFIG_DIFF_IN_STATE, SET_LOADING_STATE } from '../../actions/Config';
|
import { SET_CONFIG_DIFF_IN_STATE, SET_CONFIG_PARTIAL_DIFF_IN_STATE, SET_LOADING_STATE } from '../../actions/Config';
|
||||||
|
|
||||||
const initialState = fromJS({
|
const initialState = fromJS({
|
||||||
configDiff: Map({}),
|
configDiff: Map({}),
|
||||||
|
partialDiff: List([]),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -16,10 +17,13 @@ export default function configReducer(state = initialState, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case SET_CONFIG_DIFF_IN_STATE:
|
case SET_CONFIG_DIFF_IN_STATE:
|
||||||
return state
|
return state
|
||||||
.update('configDiff', () => fromJS(action.config))
|
.update('configDiff', () => fromJS(action.config));
|
||||||
|
case SET_CONFIG_PARTIAL_DIFF_IN_STATE:
|
||||||
|
return state
|
||||||
|
.update('partialDiff', () => fromJS(action.config));
|
||||||
case SET_LOADING_STATE:
|
case SET_LOADING_STATE:
|
||||||
return state
|
return state
|
||||||
.update('isLoading', () => fromJS(action.value))
|
.update('isLoading', () => fromJS(action.value));
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
{
|
{
|
||||||
"popUpWarning.warning.import": "If you continue all your local config files<br></br>will be imported into the database.",
|
"popUpWarning.warning.import_1": "If you continue all your local config files",
|
||||||
"popUpWarning.warning.export": "If you continue all your database config<br></br>will be written into config files.",
|
"popUpWarning.warning.import_2": "will be imported into the database.",
|
||||||
|
"popUpWarning.warning.export_1": "If you continue all your database config",
|
||||||
|
"popUpWarning.warning.export_2": "will be written into config files.",
|
||||||
"popUpWarning.button.import": "Yes, import",
|
"popUpWarning.button.import": "Yes, import",
|
||||||
"popUpWarning.button.export": "Yes, export",
|
"popUpWarning.button.export": "Yes, export",
|
||||||
|
"popUpWarning.button.cancel": "Cancel",
|
||||||
|
|
||||||
"Header.Title": "Config Sync",
|
"Header.Title": "Config Sync",
|
||||||
"Header.Description": "Manage your database config across environments.",
|
"Header.Description": "Manage your database config across environments.",
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
require('../server/cli');
|
|
@ -0,0 +1,4 @@
|
||||||
|
comment:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- develop
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"destination": "extensions/config-sync/files/",
|
|
||||||
"minify": false,
|
|
||||||
"importOnBootstrap": false,
|
|
||||||
"include": [
|
|
||||||
"core-store",
|
|
||||||
"role-permissions"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"core-store.plugin_users-permissions_grant"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An asynchronous bootstrap function that runs before
|
|
||||||
* your application gets started.
|
|
||||||
*
|
|
||||||
* This gives you an opportunity to set up your data model,
|
|
||||||
* run jobs, or perform some special logic.
|
|
||||||
*
|
|
||||||
* See more details here: https://strapi.io/documentation/v3.x/concepts/configurations.html#bootstrap
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = async () => {
|
|
||||||
if (strapi.plugins['config-sync'].config.importOnBootstrap) {
|
|
||||||
if (fs.existsSync(strapi.plugins['config-sync'].config.destination)) {
|
|
||||||
await strapi.plugins['config-sync'].services.main.importAllConfig();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,28 +0,0 @@
|
||||||
{
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/export",
|
|
||||||
"handler": "config.exportAll",
|
|
||||||
"config": {
|
|
||||||
"policies": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/import",
|
|
||||||
"handler": "config.importAll",
|
|
||||||
"config": {
|
|
||||||
"policies": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/diff",
|
|
||||||
"handler": "config.getDiff",
|
|
||||||
"config": {
|
|
||||||
"policies": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const difference = require('../utils/getObjectDiff');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main controllers for config import/export.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
/**
|
|
||||||
* Export all config, from db to filesystem.
|
|
||||||
*
|
|
||||||
* @param {object} ctx - Request context object.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
exportAll: async (ctx) => {
|
|
||||||
await strapi.plugins['config-sync'].services.main.exportAllConfig();
|
|
||||||
|
|
||||||
ctx.send({
|
|
||||||
message: `Config was successfully exported to ${strapi.plugins['config-sync'].config.destination}.`
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import all config, from filesystem to db.
|
|
||||||
*
|
|
||||||
* @param {object} ctx - Request context object.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
importAll: async (ctx) => {
|
|
||||||
// Check for existance of the config file destination dir.
|
|
||||||
if (!fs.existsSync(strapi.plugins['config-sync'].config.destination)) {
|
|
||||||
ctx.send({
|
|
||||||
message: 'No config files were found.'
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await strapi.plugins['config-sync'].services.main.importAllConfig();
|
|
||||||
|
|
||||||
ctx.send({
|
|
||||||
message: 'Config was successfully imported.'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get config diff between filesystem & db.
|
|
||||||
*
|
|
||||||
* @param {object} ctx - Request context object.
|
|
||||||
* @returns Object with key value pairs of config.
|
|
||||||
*/
|
|
||||||
getDiff: async (ctx) => {
|
|
||||||
// Check for existance of the config file destination dir.
|
|
||||||
if (!fs.existsSync(strapi.plugins['config-sync'].config.destination)) {
|
|
||||||
ctx.send({
|
|
||||||
message: 'No config files were found.'
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formattedDiff = {
|
|
||||||
fileConfig: {},
|
|
||||||
databaseConfig: {},
|
|
||||||
diff: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fileConfig = await strapi.plugins['config-sync'].services.main.getAllConfigFromFiles();
|
|
||||||
const databaseConfig = await strapi.plugins['config-sync'].services.main.getAllConfigFromDatabase();
|
|
||||||
|
|
||||||
const diff = difference(databaseConfig, fileConfig);
|
|
||||||
|
|
||||||
formattedDiff.diff = diff;
|
|
||||||
|
|
||||||
Object.keys(diff).map((changedConfigName) => {
|
|
||||||
formattedDiff.fileConfig[changedConfigName] = fileConfig[changedConfigName];
|
|
||||||
formattedDiff.databaseConfig[changedConfigName] = databaseConfig[changedConfigName];
|
|
||||||
})
|
|
||||||
|
|
||||||
return formattedDiff;
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
module.exports = {
|
||||||
|
name: 'Unit test',
|
||||||
|
testMatch: ['**/__tests__/?(*.)+(spec|test).js'],
|
||||||
|
transform: {},
|
||||||
|
coverageDirectory: "./coverage/",
|
||||||
|
collectCoverage: true,
|
||||||
|
};
|
78
package.json
78
package.json
|
@ -1,21 +1,30 @@
|
||||||
{
|
{
|
||||||
"name": "strapi-plugin-config-sync",
|
"name": "strapi-plugin-config-sync",
|
||||||
"version": "0.1.6",
|
"version": "1.0.0-alpha.1",
|
||||||
"description": "Manage your Strapi database configuration as partial json files which can be imported/exported across environments. ",
|
"description": "CLI & GUI for syncing config data across environments.",
|
||||||
"strapi": {
|
"strapi": {
|
||||||
|
"displayName": "Config Sync",
|
||||||
"name": "config-sync",
|
"name": "config-sync",
|
||||||
"icon": "sync",
|
"icon": "sync",
|
||||||
"description": "Manage your Strapi database configuration as partial json files which can be imported/exported across environments. "
|
"description": "CLI & GUI for syncing config data across environments.",
|
||||||
|
"required": false,
|
||||||
|
"kind": "plugin"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"config-sync": "./bin/config-sync"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"eslint": "eslint --max-warnings=0 './**/*.{js,jsx}'",
|
||||||
|
"eslint:fix": "eslint --fix './**/*.{js,jsx}'",
|
||||||
|
"test:unit": "jest --verbose",
|
||||||
|
"postinstall": "rm -rf node_modules/@strapi/helper-plugin"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"immutable": "^4.0.0-rc.12",
|
"chalk": "^4.1.2",
|
||||||
"react": "^16.13.1",
|
"cli-table": "^0.3.6",
|
||||||
|
"commander": "^8.3.0",
|
||||||
|
"inquirer": "^8.2.0",
|
||||||
"react-diff-viewer": "^3.1.1",
|
"react-diff-viewer": "^3.1.1",
|
||||||
"react-dom": "^16.9.0",
|
|
||||||
"react-redux": "^7.2.2",
|
|
||||||
"redux": "^4.0.5",
|
|
||||||
"redux-immutable": "^4.0.0",
|
|
||||||
"redux-logger": "^3.0.6",
|
|
||||||
"redux-thunk": "^2.3.0"
|
"redux-thunk": "^2.3.0"
|
||||||
},
|
},
|
||||||
"author": {
|
"author": {
|
||||||
|
@ -30,19 +39,52 @@
|
||||||
"url": "https://github.com/boazpoolman"
|
"url": "https://github.com/boazpoolman"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"files": [
|
||||||
|
"admin",
|
||||||
|
"server",
|
||||||
|
"bin",
|
||||||
|
"strapi-admin.js",
|
||||||
|
"strapi-server.js"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"@fortawesome/react-fontawesome": "^0.1.16",
|
||||||
|
"@strapi/design-system": "0.0.1-alpha.70",
|
||||||
|
"@strapi/helper-plugin": "4.0.0",
|
||||||
|
"@strapi/icons": "0.0.1-alpha.70",
|
||||||
|
"@strapi/strapi": "^4.0.0",
|
||||||
|
"@strapi/utils": "4.0.0",
|
||||||
|
"babel-eslint": "9.0.0",
|
||||||
|
"codecov": "^3.8.3",
|
||||||
|
"eslint": "^5.16.0",
|
||||||
|
"eslint-config-airbnb": "^18.2.1",
|
||||||
|
"eslint-config-react-app": "^3.0.7",
|
||||||
|
"eslint-import-resolver-webpack": "^0.11.0",
|
||||||
|
"eslint-loader": "2.1.1",
|
||||||
|
"eslint-plugin-babel": "^5.3.0",
|
||||||
|
"eslint-plugin-flowtype": "2.50.1",
|
||||||
|
"eslint-plugin-import": "^2.22.1",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||||
|
"eslint-plugin-react": "^7.21.5",
|
||||||
|
"eslint-plugin-react-hooks": "^2.3.0",
|
||||||
|
"immutable": "^4.0.0-rc.14",
|
||||||
|
"jest": "^26.0.1",
|
||||||
|
"jest-cli": "^26.0.1",
|
||||||
|
"jest-styled-components": "^7.0.2",
|
||||||
|
"lodash": "^4.17.11",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-intl": "^5.20.12",
|
||||||
|
"react-redux": "^7.2.2",
|
||||||
|
"redux": "^4.0.5",
|
||||||
|
"redux-immutable": "^4.0.0",
|
||||||
|
"styled-components": "^5.2.3"
|
||||||
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/boazpoolman/strapi-plugin-config-sync/issues"
|
"url": "https://github.com/boazpoolman/strapi-plugin-config-sync/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/boazpoolman/strapi-plugin-config-sync#readme",
|
"homepage": "https://github.com/boazpoolman/strapi-plugin-config-sync#readme",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.16.0 <=14.x.x",
|
"node": ">=10.0.0",
|
||||||
"npm": ">=6.0.0"
|
"npm": ">=6.0.0"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"devDependencies": {
|
|
||||||
"husky": "^6.0.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"prepare": "husky install"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An asynchronous bootstrap function that runs before
|
||||||
|
* your application gets started.
|
||||||
|
*
|
||||||
|
* This gives you an opportunity to set up your data model,
|
||||||
|
* run jobs, or perform some special logic.
|
||||||
|
*
|
||||||
|
* See more details here: https://strapi.io/documentation/v3.x/concepts/configurations.html#bootstrap
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = async () => {
|
||||||
|
// Import on bootstrap.
|
||||||
|
if (strapi.config.get('plugin.config-sync.importOnBootstrap')) {
|
||||||
|
if (fs.existsSync(strapi.config.get('plugin.config-sync.destination'))) {
|
||||||
|
await strapi.plugin('config-sync').service('main').importAllConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register permission actions.
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
section: 'plugins',
|
||||||
|
displayName: 'Access the plugin settings',
|
||||||
|
uid: 'settings.read',
|
||||||
|
pluginName: 'config-sync',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
section: 'plugins',
|
||||||
|
displayName: 'Menu link to plugin settings',
|
||||||
|
uid: 'menu-link',
|
||||||
|
pluginName: 'config-sync',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
await strapi.admin.services.permission.actionProvider.registerMany(actions);
|
||||||
|
};
|
|
@ -0,0 +1,223 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const { Command } = require('commander');
|
||||||
|
const Table = require('cli-table');
|
||||||
|
const chalk = require('chalk');
|
||||||
|
const inquirer = require('inquirer');
|
||||||
|
const { isEmpty } = require('lodash');
|
||||||
|
const strapi = require('@strapi/strapi');
|
||||||
|
|
||||||
|
const packageJSON = require('../package.json');
|
||||||
|
|
||||||
|
const program = new Command();
|
||||||
|
|
||||||
|
const initTable = (head) => {
|
||||||
|
return new Table({
|
||||||
|
head: [chalk.green('Name'), chalk.green(head || 'State')],
|
||||||
|
colWidths: [65, 20],
|
||||||
|
chars: { top: '═',
|
||||||
|
'top-mid': '╤',
|
||||||
|
'top-left': '╔',
|
||||||
|
'top-right': '╗',
|
||||||
|
bottom: '═',
|
||||||
|
'bottom-mid': '╧',
|
||||||
|
'bottom-left': '╚',
|
||||||
|
'bottom-right': '╝',
|
||||||
|
left: '║',
|
||||||
|
'left-mid': '╟',
|
||||||
|
mid: '─',
|
||||||
|
'mid-mid': '┼',
|
||||||
|
right: '║',
|
||||||
|
'right-mid': '╢',
|
||||||
|
middle: '│',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getConfigState = (diff, configName, syncType) => {
|
||||||
|
if (
|
||||||
|
diff.fileConfig[configName]
|
||||||
|
&& diff.databaseConfig[configName]
|
||||||
|
) {
|
||||||
|
return chalk.yellow(syncType ? 'Update' : 'Different');
|
||||||
|
} else if (
|
||||||
|
diff.fileConfig[configName]
|
||||||
|
&& !diff.databaseConfig[configName]
|
||||||
|
) {
|
||||||
|
if (syncType === 'import') {
|
||||||
|
return chalk.green('Create');
|
||||||
|
} else if (syncType === 'export') {
|
||||||
|
return chalk.red('Delete');
|
||||||
|
} else {
|
||||||
|
return chalk.red('Only in sync dir');
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
!diff.fileConfig[configName]
|
||||||
|
&& diff.databaseConfig[configName]
|
||||||
|
) {
|
||||||
|
if (syncType === 'import') {
|
||||||
|
return chalk.red('Delete');
|
||||||
|
} else if (syncType === 'export') {
|
||||||
|
return chalk.green('Create');
|
||||||
|
} else {
|
||||||
|
return chalk.green('Only in DB');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAction = async (syncType, skipConfirm, configType, partials) => {
|
||||||
|
const app = await strapi().load();
|
||||||
|
const diff = await app.plugin('config-sync').service('main').getFormattedDiff();
|
||||||
|
|
||||||
|
// No changes.
|
||||||
|
if (isEmpty(diff.diff)) {
|
||||||
|
console.log(`${chalk.bgCyan.bold('[notice]')} There are no changes to ${syncType}.`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init table.
|
||||||
|
const table = initTable('Action');
|
||||||
|
const configNames = partials && partials.split(',');
|
||||||
|
const partialDiff = {};
|
||||||
|
|
||||||
|
// Fill partialDiff with arguments.
|
||||||
|
if (configNames) {
|
||||||
|
configNames.map((name) => {
|
||||||
|
if (diff.diff[name]) partialDiff[name] = diff.diff[name];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (configType) {
|
||||||
|
Object.keys(diff.diff).map((name) => {
|
||||||
|
if (configType === name.split('.')[0]) {
|
||||||
|
partialDiff[name] = diff.diff[name];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// No changes for partial diff.
|
||||||
|
if ((partials || configType) && isEmpty(partialDiff)) {
|
||||||
|
console.log(`${chalk.bgCyan.bold('[notice]')} There are no changes for the specified config.`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalDiff = (partials || configType) && partialDiff ? partialDiff : diff.diff;
|
||||||
|
|
||||||
|
// Add diff to table.
|
||||||
|
Object.keys(finalDiff).map((configName) => {
|
||||||
|
table.push([configName, getConfigState(diff, configName, syncType)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Print table.
|
||||||
|
console.log(table.toString(), '\n');
|
||||||
|
|
||||||
|
// Prompt to confirm.
|
||||||
|
let answer = {};
|
||||||
|
if (!skipConfirm) {
|
||||||
|
answer = await inquirer.prompt([{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'confirm',
|
||||||
|
message: `Are you sure you want to ${syncType} the config changes?`,
|
||||||
|
}]);
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preform the action.
|
||||||
|
if (skipConfirm || answer.confirm) {
|
||||||
|
if (syncType === 'import') {
|
||||||
|
const onSuccess = (name) => console.log(`${chalk.bgGreen.bold('[success]')} Imported ${name}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(Object.keys(finalDiff).map(async (name) => {
|
||||||
|
await app.plugin('config-sync').service('main').importSingleConfig(name, onSuccess);
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`${chalk.bgRed.bold('[error]')} Something went wrong during the import. ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (syncType === 'export') {
|
||||||
|
const onSuccess = (name) => console.log(`${chalk.bgGreen.bold('[success]')} Exported ${name}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(Object.keys(finalDiff).map(async (name) => {
|
||||||
|
await app.plugin('config-sync').service('main').exportSingleConfig(name, onSuccess);
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`${chalk.bgRed.bold('[error]')} Something went wrong during the export. ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial program setup
|
||||||
|
program.storeOptionsAsProperties(false).allowUnknownOption(true);
|
||||||
|
|
||||||
|
program.helpOption('-h, --help', 'Display help for command');
|
||||||
|
program.addHelpCommand('help [command]', 'Display help for command');
|
||||||
|
|
||||||
|
// `$ config-sync version` (--version synonym)
|
||||||
|
program.version(packageJSON.version, '-v, --version', 'Output the version number');
|
||||||
|
program
|
||||||
|
.command('version')
|
||||||
|
.description('Output your version of the config-sync plugin')
|
||||||
|
.action(() => {
|
||||||
|
process.stdout.write(`${packageJSON.version}\n`);
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// `$ config-sync import`
|
||||||
|
program
|
||||||
|
.command('import')
|
||||||
|
.alias('i')
|
||||||
|
.option('-t, --type <type>', 'The type of config')
|
||||||
|
.option('-p, --partial <partials>', 'A comma separated string of configs')
|
||||||
|
.option('-y', 'Skip the confirm prompt')
|
||||||
|
.description('Import the config')
|
||||||
|
.action(async ({ y, type, partial }) => {
|
||||||
|
return handleAction('import', y, type, partial);
|
||||||
|
});
|
||||||
|
|
||||||
|
// `$ config-sync export`
|
||||||
|
program
|
||||||
|
.command('export')
|
||||||
|
.alias('e')
|
||||||
|
.option('-t, --type <type>', 'The type of config')
|
||||||
|
.option('-p, --partial <partials>', 'A comma separated string of configs')
|
||||||
|
.option('-y', 'Skip the confirm prompt')
|
||||||
|
.description('Export the config')
|
||||||
|
.action(async ({ y, type, partial }) => {
|
||||||
|
return handleAction('export', y, type, partial);
|
||||||
|
});
|
||||||
|
|
||||||
|
// `$ config-sync diff`
|
||||||
|
program
|
||||||
|
.command('diff')
|
||||||
|
.alias('d')
|
||||||
|
// .option('-t, --type <type>', 'The type of config') // TODO: partial diff
|
||||||
|
.description('The config diff')
|
||||||
|
.action(async ({ type }) => {
|
||||||
|
const app = await strapi().load();
|
||||||
|
const diff = await app.plugin('config-sync').service('main').getFormattedDiff();
|
||||||
|
|
||||||
|
// No changes.
|
||||||
|
if (isEmpty(diff.diff)) {
|
||||||
|
console.log(`${chalk.bgCyan.bold('[notice]')} No differences between DB and sync directory.`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init table.
|
||||||
|
const table = initTable();
|
||||||
|
|
||||||
|
// Add diff to table.
|
||||||
|
Object.keys(diff.diff).map((configName) => {
|
||||||
|
table.push([configName, getConfigState(diff, configName)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Print table.
|
||||||
|
console.log(table.toString());
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
program.parseAsync(process.argv);
|
|
@ -0,0 +1,19 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
default: {
|
||||||
|
destination: "src/extensions/config-sync/files/",
|
||||||
|
minify: false,
|
||||||
|
importOnBootstrap: false,
|
||||||
|
include: [
|
||||||
|
"core-store",
|
||||||
|
"user-role",
|
||||||
|
"admin-role",
|
||||||
|
"i18n-locale",
|
||||||
|
],
|
||||||
|
exclude: [
|
||||||
|
"core-store.plugin_users-permissions_grant",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
validator() {},
|
||||||
|
};
|
|
@ -0,0 +1,197 @@
|
||||||
|
const { logMessage, sanitizeConfig, dynamicSort, noLimit } = require('../utils');
|
||||||
|
const difference = require('../utils/getArrayDiff');
|
||||||
|
|
||||||
|
const ConfigType = class ConfigType {
|
||||||
|
constructor(queryString, configPrefix, uid, jsonFields, relations) {
|
||||||
|
if (!queryString) {
|
||||||
|
strapi.log.error(logMessage('Query string is missing for ConfigType'));
|
||||||
|
}
|
||||||
|
this.queryString = queryString;
|
||||||
|
this.configPrefix = configPrefix;
|
||||||
|
this.uid = uid;
|
||||||
|
this.jsonFields = jsonFields || [];
|
||||||
|
this.relations = relations || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a single role-permissions config file into the db.
|
||||||
|
*
|
||||||
|
* @param {string} configName - The name of the config file.
|
||||||
|
* @param {string} configContent - The JSON content of the config file.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
importSingle = async (configName, configContent) => {
|
||||||
|
// Check if the config should be excluded.
|
||||||
|
const shouldExclude = strapi.config.get('plugin.config-sync.exclude').includes(`${this.configPrefix}.${configName}`);
|
||||||
|
if (shouldExclude) return;
|
||||||
|
|
||||||
|
const queryAPI = strapi.query(this.queryString);
|
||||||
|
|
||||||
|
let existingConfig = await queryAPI
|
||||||
|
.findOne({
|
||||||
|
where: { [this.uid]: configName },
|
||||||
|
populate: this.relations.map(({ relationName }) => relationName),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingConfig && configContent === null) {
|
||||||
|
const entity = await queryAPI.findOne({
|
||||||
|
where: { [this.uid]: configName },
|
||||||
|
populate: this.relations.map(({ relationName }) => relationName),
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(this.relations.map(async ({ queryString, parentName }) => {
|
||||||
|
const relations = await noLimit(strapi.query(queryString), {
|
||||||
|
where: {
|
||||||
|
[parentName]: entity.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(relations.map(async (relation) => {
|
||||||
|
await strapi.query(queryString).delete({
|
||||||
|
where: { id: relation.id },
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
|
||||||
|
await queryAPI.delete({
|
||||||
|
where: { id: entity.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existingConfig) {
|
||||||
|
// Format JSON fields.
|
||||||
|
const query = { ...configContent };
|
||||||
|
this.jsonFields.map((field) => query[field] = JSON.stringify(configContent[field]));
|
||||||
|
|
||||||
|
// Create entity.
|
||||||
|
this.relations.map(({ relationName }) => delete query[relationName]);
|
||||||
|
const newEntity = await queryAPI.create({ data: query });
|
||||||
|
|
||||||
|
// Create relation entities.
|
||||||
|
await Promise.all(this.relations.map(async ({ queryString, relationName, parentName }) => {
|
||||||
|
const relationQueryApi = strapi.query(queryString);
|
||||||
|
|
||||||
|
await Promise.all(configContent[relationName].map(async (relationEntity) => {
|
||||||
|
const relationQuery = { ...relationEntity, [parentName]: newEntity };
|
||||||
|
await relationQueryApi.create({ data: relationQuery });
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// Format JSON fields.
|
||||||
|
configContent = sanitizeConfig(configContent);
|
||||||
|
const query = { ...configContent };
|
||||||
|
this.jsonFields.map((field) => query[field] = JSON.stringify(configContent[field]));
|
||||||
|
|
||||||
|
// Update entity.
|
||||||
|
this.relations.map(({ relationName }) => delete query[relationName]);
|
||||||
|
const entity = await queryAPI.update({ where: { [this.uid]: configName }, data: query });
|
||||||
|
|
||||||
|
// Delete/create relations.
|
||||||
|
await Promise.all(this.relations.map(async ({ queryString, relationName, parentName, relationSortField }) => {
|
||||||
|
const relationQueryApi = strapi.query(queryString);
|
||||||
|
existingConfig = sanitizeConfig(existingConfig, relationName, relationSortField);
|
||||||
|
configContent = sanitizeConfig(configContent, relationName, relationSortField);
|
||||||
|
|
||||||
|
const configToAdd = difference(configContent[relationName], existingConfig[relationName], relationSortField);
|
||||||
|
const configToDelete = difference(existingConfig[relationName], configContent[relationName], relationSortField);
|
||||||
|
|
||||||
|
await Promise.all(configToDelete.map(async (config) => {
|
||||||
|
await relationQueryApi.delete({
|
||||||
|
where: {
|
||||||
|
[relationSortField]: config[relationSortField],
|
||||||
|
[parentName]: entity.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
await Promise.all(configToAdd.map(async (config) => {
|
||||||
|
await relationQueryApi.create({
|
||||||
|
data: { ...config, [parentName]: entity.id },
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a single core-store config to a file.
|
||||||
|
*
|
||||||
|
* @param {string} configName - The name of the config file.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
exportSingle = async (configName) => {
|
||||||
|
const formattedDiff = await strapi.plugin('config-sync').service('main').getFormattedDiff(this.configPrefix);
|
||||||
|
|
||||||
|
// Check if the config should be excluded.
|
||||||
|
const shouldExclude = strapi.config.get('plugin.config-sync.exclude').includes(`${configName}`);
|
||||||
|
if (shouldExclude) return;
|
||||||
|
|
||||||
|
const currentConfig = formattedDiff.databaseConfig[configName];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!currentConfig
|
||||||
|
&& formattedDiff.fileConfig[configName]
|
||||||
|
) {
|
||||||
|
await strapi.plugin('config-sync').service('main').deleteConfigFile(configName);
|
||||||
|
} else {
|
||||||
|
await strapi.plugin('config-sync').service('main').writeConfigFile(this.configPrefix, currentConfig[this.uid], currentConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all role-permissions config from the db.
|
||||||
|
*
|
||||||
|
* @returns {object} Object with key value pairs of configs.
|
||||||
|
*/
|
||||||
|
getAllFromDatabase = async () => {
|
||||||
|
const AllConfig = await noLimit(strapi.query(this.queryString), {});
|
||||||
|
const configs = {};
|
||||||
|
|
||||||
|
await Promise.all(Object.values(AllConfig).map(async (config) => {
|
||||||
|
// Check if the config should be excluded.
|
||||||
|
const shouldExclude = strapi.config.get('plugin.config-sync.exclude').includes(`${this.configPrefix}.${config[this.uid]}`);
|
||||||
|
if (shouldExclude) return;
|
||||||
|
|
||||||
|
const formattedConfig = { ...sanitizeConfig(config) };
|
||||||
|
await Promise.all(this.relations.map(async ({ queryString, relationName, relationSortField, parentName }) => {
|
||||||
|
const relations = await noLimit(strapi.query(queryString), {
|
||||||
|
where: { [parentName]: { [this.uid]: config[this.uid] } },
|
||||||
|
});
|
||||||
|
|
||||||
|
relations.map((relation) => sanitizeConfig(relation));
|
||||||
|
relations.sort(dynamicSort(relationSortField));
|
||||||
|
formattedConfig[relationName] = relations;
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.jsonFields.map((field) => formattedConfig[field] = JSON.parse(config[field]));
|
||||||
|
configs[`${this.configPrefix}.${config[this.uid]}`] = formattedConfig;
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
return configs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import all core-store config files into the db.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
importAll = async () => {
|
||||||
|
// The main.importAllConfig service will loop the core-store.importSingle service.
|
||||||
|
await strapi.plugin('config-sync').service('main').importAllConfig(this.configPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export all core-store config to files.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
exportAll = async () => {
|
||||||
|
// The main.importAllConfig service will loop the core-store.importSingle service.
|
||||||
|
await strapi.plugin('config-sync').service('main').exportAllConfig(this.configPrefix);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = ConfigType;
|
|
@ -0,0 +1,32 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const ConfigType = require("./type");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
'i18n-locale': new ConfigType('plugin::i18n.locale', 'i18n-locale', 'code'),
|
||||||
|
'core-store': new ConfigType('strapi::core-store', 'core-store', 'key', ['value']),
|
||||||
|
'user-role': new ConfigType(
|
||||||
|
'plugin::users-permissions.role',
|
||||||
|
'user-role',
|
||||||
|
'type',
|
||||||
|
[],
|
||||||
|
[{
|
||||||
|
queryString: 'plugin::users-permissions.permission',
|
||||||
|
relationName: 'permissions',
|
||||||
|
parentName: 'role',
|
||||||
|
relationSortField: 'action',
|
||||||
|
}]
|
||||||
|
),
|
||||||
|
'admin-role': new ConfigType(
|
||||||
|
'admin::role',
|
||||||
|
'admin-role',
|
||||||
|
'code',
|
||||||
|
[],
|
||||||
|
[{
|
||||||
|
queryString: 'admin::permission',
|
||||||
|
relationName: 'permissions',
|
||||||
|
parentName: 'role',
|
||||||
|
relationSortField: 'action',
|
||||||
|
}]
|
||||||
|
),
|
||||||
|
};
|
|
@ -0,0 +1,86 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const { isEmpty } = require('lodash');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main controllers for config import/export.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* Export all config, from db to filesystem.
|
||||||
|
*
|
||||||
|
* @param {object} ctx - Request context object.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
exportAll: async (ctx) => {
|
||||||
|
if (isEmpty(ctx.request.body)) {
|
||||||
|
await strapi.plugin('config-sync').service('main').exportAllConfig();
|
||||||
|
} else {
|
||||||
|
await Promise.all(ctx.request.body.map(async (configName) => {
|
||||||
|
await strapi.plugin('config-sync').service('main').exportSingleConfig(configName);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ctx.send({
|
||||||
|
message: `Config was successfully exported to ${strapi.config.get('plugin.config-sync.destination')}.`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import all config, from filesystem to db.
|
||||||
|
*
|
||||||
|
* @param {object} ctx - Request context object.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
importAll: async (ctx) => {
|
||||||
|
// Check for existance of the config file destination dir.
|
||||||
|
if (!fs.existsSync(strapi.config.get('plugin.config-sync.destination'))) {
|
||||||
|
ctx.send({
|
||||||
|
message: 'No config files were found.',
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.request.body) {
|
||||||
|
ctx.send({
|
||||||
|
message: 'No config was specified for the export endpoint.',
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(ctx.request.body.map(async (configName) => {
|
||||||
|
await strapi.plugin('config-sync').service('main').importSingleConfig(configName);
|
||||||
|
}));
|
||||||
|
|
||||||
|
ctx.send({
|
||||||
|
message: 'Config was successfully imported.',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get config diff between filesystem & db.
|
||||||
|
*
|
||||||
|
* @param {object} ctx - Request context object.
|
||||||
|
* @returns {object} formattedDiff - The formatted diff object.
|
||||||
|
* @returns {object} formattedDiff.fileConfig - The config as found in the filesystem.
|
||||||
|
* @returns {object} formattedDiff.databaseConfig - The config as found in the database.
|
||||||
|
* @returns {object} formattedDiff.diff - The diff between the file config and databse config.
|
||||||
|
*/
|
||||||
|
getDiff: async (ctx) => {
|
||||||
|
// Check for existance of the config file destination dir.
|
||||||
|
if (!fs.existsSync(strapi.config.get('plugin.config-sync.destination'))) {
|
||||||
|
ctx.send({
|
||||||
|
message: 'No config files were found.',
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return strapi.plugin('config-sync').service('main').getFormattedDiff();
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const config = require('./config');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
config: config,
|
||||||
|
};
|
|
@ -0,0 +1,31 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
type: 'admin',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
path: "/export",
|
||||||
|
handler: "config.exportAll",
|
||||||
|
config: {
|
||||||
|
policies: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
path: "/import",
|
||||||
|
handler: "config.importAll",
|
||||||
|
config: {
|
||||||
|
policies: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
path: "/diff",
|
||||||
|
handler: "config.getDiff",
|
||||||
|
config: {
|
||||||
|
policies: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const adminRoutes = require('./admin');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
admin: adminRoutes,
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const main = require('./main');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
main,
|
||||||
|
};
|
|
@ -0,0 +1,282 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const util = require('util');
|
||||||
|
const types = require('../config/types');
|
||||||
|
const difference = require('../utils/getObjectDiff');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main services for config import/export.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = () => ({
|
||||||
|
/**
|
||||||
|
* Write a single config file.
|
||||||
|
*
|
||||||
|
* @param {string} configType - The type of the config.
|
||||||
|
* @param {string} configName - The name of the config file.
|
||||||
|
* @param {string} fileContents - The JSON content of the config file.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
writeConfigFile: async (configType, configName, fileContents) => {
|
||||||
|
// Check if the config should be excluded.
|
||||||
|
const shouldExclude = strapi.config.get('plugin.config-sync.exclude').includes(`${configType}.${configName}`);
|
||||||
|
if (shouldExclude) return;
|
||||||
|
|
||||||
|
// Replace ':' with '#' in filenames for Windows support.
|
||||||
|
configName = configName.replace(/:/g, "#");
|
||||||
|
|
||||||
|
// Check if the JSON content should be minified.
|
||||||
|
const json = !strapi.config.get('plugin.config-sync').minify
|
||||||
|
? JSON.stringify(fileContents, null, 2)
|
||||||
|
: JSON.stringify(fileContents);
|
||||||
|
|
||||||
|
if (!fs.existsSync(strapi.config.get('plugin.config-sync.destination'))) {
|
||||||
|
fs.mkdirSync(strapi.config.get('plugin.config-sync.destination'), { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeFile = util.promisify(fs.writeFile);
|
||||||
|
await writeFile(`${strapi.config.get('plugin.config-sync.destination')}${configType}.${configName}.json`, json)
|
||||||
|
.then(() => {
|
||||||
|
// @TODO:
|
||||||
|
// Add logging for successfull config export.
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// @TODO:
|
||||||
|
// Add logging for failed config export.
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete config file.
|
||||||
|
*
|
||||||
|
* @param {string} configName - The name of the config file.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
deleteConfigFile: async (configName) => {
|
||||||
|
// Check if the config should be excluded.
|
||||||
|
const shouldExclude = strapi.config.get('plugin.config-sync.exclude').includes(`${configName}`);
|
||||||
|
if (shouldExclude) return;
|
||||||
|
|
||||||
|
// Replace ':' with '#' in filenames for Windows support.
|
||||||
|
configName = configName.replace(/:/g, "#");
|
||||||
|
|
||||||
|
fs.unlinkSync(`${strapi.config.get('plugin.config-sync.destination')}${configName}.json`);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read from a config file.
|
||||||
|
*
|
||||||
|
* @param {string} configType - The type of config.
|
||||||
|
* @param {string} configName - The name of the config file.
|
||||||
|
* @returns {object} The JSON content of the config file.
|
||||||
|
*/
|
||||||
|
readConfigFile: async (configType, configName) => {
|
||||||
|
// Replace ':' with '#' in filenames for Windows support.
|
||||||
|
configName = configName.replace(/:/g, "#");
|
||||||
|
|
||||||
|
const readFile = util.promisify(fs.readFile);
|
||||||
|
return readFile(`${strapi.config.get('plugin.config-sync.destination')}${configType}.${configName}.json`)
|
||||||
|
.then((data) => {
|
||||||
|
return JSON.parse(data);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the config JSON from the filesystem.
|
||||||
|
*
|
||||||
|
* @param {string} configType - Type of config to gather. Leave empty to get all config.
|
||||||
|
* @returns {object} Object with key value pairs of configs.
|
||||||
|
*/
|
||||||
|
getAllConfigFromFiles: async (configType = null) => {
|
||||||
|
if (!fs.existsSync(strapi.config.get('plugin.config-sync.destination'))) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const configFiles = fs.readdirSync(strapi.config.get('plugin.config-sync.destination'));
|
||||||
|
|
||||||
|
const getConfigs = async () => {
|
||||||
|
const fileConfigs = {};
|
||||||
|
|
||||||
|
await Promise.all(configFiles.map(async (file) => {
|
||||||
|
const type = file.split('.')[0]; // Grab the first part of the filename.
|
||||||
|
const name = file.split(/\.(.+)/)[1].split('.').slice(0, -1).join('.'); // Grab the rest of the filename minus the file extension.
|
||||||
|
|
||||||
|
// Replace ':' with '#' in filenames for Windows support.
|
||||||
|
const formattedName = name.replace(/#/g, ":");
|
||||||
|
|
||||||
|
if (
|
||||||
|
configType && configType !== type
|
||||||
|
|| !strapi.config.get('plugin.config-sync.include').includes(type)
|
||||||
|
|| strapi.config.get('plugin.config-sync.exclude').includes(`${type}.${name}`)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileContents = await strapi.plugin('config-sync').service('main').readConfigFile(type, name);
|
||||||
|
fileConfigs[`${type}.${formattedName}`] = fileContents;
|
||||||
|
}));
|
||||||
|
|
||||||
|
return fileConfigs;
|
||||||
|
};
|
||||||
|
|
||||||
|
return getConfigs();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the config JSON from the database.
|
||||||
|
*
|
||||||
|
* @param {string} configType - Type of config to gather. Leave empty to get all config.
|
||||||
|
* @returns {object} Object with key value pairs of configs.
|
||||||
|
*/
|
||||||
|
getAllConfigFromDatabase: async (configType = null) => {
|
||||||
|
const getConfigs = async () => {
|
||||||
|
let databaseConfigs = {};
|
||||||
|
|
||||||
|
await Promise.all(strapi.config.get('plugin.config-sync.include').map(async (type) => {
|
||||||
|
if (configType && configType !== type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await types[type].getAllFromDatabase();
|
||||||
|
databaseConfigs = Object.assign(config, databaseConfigs);
|
||||||
|
}));
|
||||||
|
|
||||||
|
return databaseConfigs;
|
||||||
|
};
|
||||||
|
|
||||||
|
return getConfigs();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import all config files into the db.
|
||||||
|
*
|
||||||
|
* @param {string} configType - Type of config to impor. Leave empty to import all config.
|
||||||
|
* @param {object} onSuccess - Success callback to run on each single successfull import.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
importAllConfig: async (configType = null, onSuccess) => {
|
||||||
|
const fileConfig = await strapi.plugin('config-sync').service('main').getAllConfigFromFiles();
|
||||||
|
const databaseConfig = await strapi.plugin('config-sync').service('main').getAllConfigFromDatabase();
|
||||||
|
|
||||||
|
const diff = difference(databaseConfig, fileConfig);
|
||||||
|
|
||||||
|
await Promise.all(Object.keys(diff).map(async (file) => {
|
||||||
|
const type = file.split('.')[0]; // Grab the first part of the filename.
|
||||||
|
const name = file.split(/\.(.+)/)[1]; // Grab the rest of the filename minus the file extension.
|
||||||
|
|
||||||
|
if (configType && configType !== type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await strapi.plugin('config-sync').service('main').importSingleConfig(`${type}.${name}`, onSuccess);
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export all config files.
|
||||||
|
*
|
||||||
|
* @param {string} configType - Type of config to export. Leave empty to export all config.
|
||||||
|
* @param {object} onSuccess - Success callback to run on each single successfull import.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
exportAllConfig: async (configType = null, onSuccess) => {
|
||||||
|
const fileConfig = await strapi.plugin('config-sync').service('main').getAllConfigFromFiles();
|
||||||
|
const databaseConfig = await strapi.plugin('config-sync').service('main').getAllConfigFromDatabase();
|
||||||
|
|
||||||
|
const diff = difference(databaseConfig, fileConfig);
|
||||||
|
|
||||||
|
await Promise.all(Object.keys(diff).map(async (file) => {
|
||||||
|
const type = file.split('.')[0]; // Grab the first part of the filename.
|
||||||
|
const name = file.split(/\.(.+)/)[1]; // Grab the rest of the filename minus the file extension.
|
||||||
|
|
||||||
|
if (configType && configType !== type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await strapi.plugin('config-sync').service('main').exportSingleConfig(`${type}.${name}`, onSuccess);
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a single config file into the db.
|
||||||
|
*
|
||||||
|
* @param {string} configName - The name of the config file.
|
||||||
|
* @param {object} onSuccess - Success callback to run on each single successfull import.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
importSingleConfig: async (configName, onSuccess) => {
|
||||||
|
// Check if the config should be excluded.
|
||||||
|
const shouldExclude = strapi.config.get('plugin.config-sync.exclude').includes(configName);
|
||||||
|
if (shouldExclude) return;
|
||||||
|
|
||||||
|
const [type, name] = configName.split('.'); // Split the configName.
|
||||||
|
const fileContents = await strapi.plugin('config-sync').service('main').readConfigFile(type, name);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await types[type].importSingle(name, fileContents);
|
||||||
|
if (onSuccess) {
|
||||||
|
onSuccess(`${type}.${name}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a single config file.
|
||||||
|
*
|
||||||
|
* @param {string} configName - The name of the config file.
|
||||||
|
* @param {object} onSuccess - Success callback to run on each single successfull import.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
exportSingleConfig: async (configName, onSuccess) => {
|
||||||
|
// Check if the config should be excluded.
|
||||||
|
const shouldExclude = strapi.config.get('plugin.config-sync.exclude').includes(configName);
|
||||||
|
if (shouldExclude) return;
|
||||||
|
|
||||||
|
const [type, name] = configName.split('.'); // Split the configName.
|
||||||
|
|
||||||
|
try {
|
||||||
|
await types[type].exportSingle(configName);
|
||||||
|
if (onSuccess) {
|
||||||
|
onSuccess(`${type}.${name}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the formatted diff.
|
||||||
|
*
|
||||||
|
* @param {string} configType - Type of config to get the diff of. Leave empty to get the diff of all config.
|
||||||
|
*
|
||||||
|
* @returns {object} - the formatted diff.
|
||||||
|
*/
|
||||||
|
getFormattedDiff: async (configType = null) => {
|
||||||
|
const formattedDiff = {
|
||||||
|
fileConfig: {},
|
||||||
|
databaseConfig: {},
|
||||||
|
diff: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileConfig = await strapi.plugin('config-sync').service('main').getAllConfigFromFiles(configType);
|
||||||
|
const databaseConfig = await strapi.plugin('config-sync').service('main').getAllConfigFromDatabase(configType);
|
||||||
|
|
||||||
|
const diff = difference(databaseConfig, fileConfig);
|
||||||
|
|
||||||
|
formattedDiff.diff = diff;
|
||||||
|
|
||||||
|
Object.keys(diff).map((changedConfigName) => {
|
||||||
|
formattedDiff.fileConfig[changedConfigName] = fileConfig[changedConfigName];
|
||||||
|
formattedDiff.databaseConfig[changedConfigName] = databaseConfig[changedConfigName];
|
||||||
|
});
|
||||||
|
|
||||||
|
return formattedDiff;
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,7 @@
|
||||||
|
const difference = (arrayOne, arrayTwo, compareKey) => {
|
||||||
|
return arrayOne.filter(({ [compareKey]: id1 }) => {
|
||||||
|
return !arrayTwo.some(({ [compareKey]: id2 }) => id2 === id1);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = difference;
|
|
@ -0,0 +1,30 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { transform, isEqual, isArray, isObject } = require('lodash');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find difference between two objects
|
||||||
|
* @param {object} origObj - Source object to compare newObj against
|
||||||
|
* @param {object} newObj - New object with potential changes
|
||||||
|
* @return {object} differences
|
||||||
|
*/
|
||||||
|
const difference = (origObj, newObj) => {
|
||||||
|
let arrayIndexCounter = 0;
|
||||||
|
|
||||||
|
const newObjChange = transform(newObj, (result, value, key) => {
|
||||||
|
if (!isEqual(value, origObj[key])) {
|
||||||
|
const resultKey = isArray(origObj) ? arrayIndexCounter++ : key;
|
||||||
|
result[resultKey] = (isObject(value) && isObject(origObj[key])) ? difference(value, origObj[key]) : value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const origObjChange = transform(origObj, (result, value, key) => {
|
||||||
|
if (!isEqual(value, newObj[key])) {
|
||||||
|
const resultKey = isArray(newObj) ? arrayIndexCounter++ : key;
|
||||||
|
result[resultKey] = (isObject(value) && isObject(newObj[key])) ? difference(value, newObj[key]) : value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.assign(newObjChange, origObjChange);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = difference;
|
|
@ -0,0 +1,93 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const getCoreStore = () => {
|
||||||
|
return strapi.store({ type: 'plugin', name: 'config-sync' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getService = (name) => {
|
||||||
|
return strapi.plugin('config-sync').service(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const logMessage = (msg = '') => `[strapi-plugin-config-sync]: ${msg}`;
|
||||||
|
|
||||||
|
const sortByKeys = (unordered) => {
|
||||||
|
return Object.keys(unordered).sort().reduce((obj, key) => {
|
||||||
|
obj[key] = unordered[key];
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dynamicSort = (property) => {
|
||||||
|
let sortOrder = 1;
|
||||||
|
|
||||||
|
if (property[0] === "-") {
|
||||||
|
sortOrder = -1;
|
||||||
|
property = property.substr(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (a, b) => {
|
||||||
|
if (sortOrder === -1) {
|
||||||
|
return b[property].localeCompare(a[property]);
|
||||||
|
} else {
|
||||||
|
return a[property].localeCompare(b[property]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizeConfig = (config, relation, relationSortField) => {
|
||||||
|
delete config._id;
|
||||||
|
delete config.id;
|
||||||
|
delete config.updatedAt;
|
||||||
|
delete config.createdAt;
|
||||||
|
|
||||||
|
if (relation) {
|
||||||
|
const formattedRelations = [];
|
||||||
|
|
||||||
|
config[relation].map((relationEntity) => {
|
||||||
|
delete relationEntity._id;
|
||||||
|
delete relationEntity.id;
|
||||||
|
delete relationEntity.updatedAt;
|
||||||
|
delete relationEntity.createdAt;
|
||||||
|
relationEntity = sortByKeys(relationEntity);
|
||||||
|
|
||||||
|
formattedRelations.push(relationEntity);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (relationSortField) {
|
||||||
|
formattedRelations.sort(dynamicSort(relationSortField));
|
||||||
|
}
|
||||||
|
|
||||||
|
config[relation] = formattedRelations;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
const noLimit = async (query, parameters, limit = 100) => {
|
||||||
|
let entries = [];
|
||||||
|
const amountOfEntries = await query.count(parameters);
|
||||||
|
|
||||||
|
for (let i = 0; i < (amountOfEntries / limit); i++) {
|
||||||
|
/* eslint-disable-next-line */
|
||||||
|
const chunk = await query.findMany({
|
||||||
|
...parameters,
|
||||||
|
limit: limit,
|
||||||
|
offset: (i * limit),
|
||||||
|
});
|
||||||
|
entries = [...chunk, ...entries];
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getService,
|
||||||
|
getCoreStore,
|
||||||
|
logMessage,
|
||||||
|
sanitizeConfig,
|
||||||
|
sortByKeys,
|
||||||
|
dynamicSort,
|
||||||
|
noLimit,
|
||||||
|
};
|
|
@ -1,127 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const coreStoreQueryString = 'core_store';
|
|
||||||
const configPrefix = 'core-store'; // Should be the same as the filename.
|
|
||||||
const difference = require('../utils/getObjectDiff');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import/Export for core-store configs.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
/**
|
|
||||||
* Export all core-store config to files.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
exportAll: async () => {
|
|
||||||
const formattedDiff = {
|
|
||||||
fileConfig: {},
|
|
||||||
databaseConfig: {},
|
|
||||||
diff: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fileConfig = await strapi.plugins['config-sync'].services.main.getAllConfigFromFiles(configPrefix);
|
|
||||||
const databaseConfig = await strapi.plugins['config-sync'].services.main.getAllConfigFromDatabase(configPrefix);
|
|
||||||
const diff = difference(databaseConfig, fileConfig);
|
|
||||||
|
|
||||||
formattedDiff.diff = diff;
|
|
||||||
|
|
||||||
Object.keys(diff).map((changedConfigName) => {
|
|
||||||
formattedDiff.fileConfig[changedConfigName] = fileConfig[changedConfigName];
|
|
||||||
formattedDiff.databaseConfig[changedConfigName] = databaseConfig[changedConfigName];
|
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.all(Object.entries(diff).map(async ([configName, config]) => {
|
|
||||||
// Check if the config should be excluded.
|
|
||||||
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(`${configName}`);
|
|
||||||
if (shouldExclude) return;
|
|
||||||
|
|
||||||
const currentConfig = formattedDiff.databaseConfig[configName];
|
|
||||||
|
|
||||||
if (
|
|
||||||
!currentConfig &&
|
|
||||||
formattedDiff.fileConfig[configName]
|
|
||||||
) {
|
|
||||||
await strapi.plugins['config-sync'].services.main.deleteConfigFile(configName);
|
|
||||||
} else {
|
|
||||||
await strapi.plugins['config-sync'].services.main.writeConfigFile(configPrefix, currentConfig.key, currentConfig);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import a single core-store config file into the db.
|
|
||||||
*
|
|
||||||
* @param {string} configName - The name of the config file.
|
|
||||||
* @param {string} configContent - The JSON content of the config file.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
importSingle: async (configName, configContent) => {
|
|
||||||
// Check if the config should be excluded.
|
|
||||||
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(`${configPrefix}.${configName}`);
|
|
||||||
if (shouldExclude) return;
|
|
||||||
|
|
||||||
const coreStoreAPI = strapi.query(coreStoreQueryString);
|
|
||||||
|
|
||||||
const configExists = await coreStoreAPI
|
|
||||||
.findOne({ key: configName });
|
|
||||||
|
|
||||||
if (configExists && configContent === null) {
|
|
||||||
await coreStoreAPI.delete({ key: configName });
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { value, ...fileContent } = configContent;
|
|
||||||
|
|
||||||
if (!configExists) {
|
|
||||||
await coreStoreAPI.create({ value: JSON.stringify(value), ...fileContent });
|
|
||||||
} else {
|
|
||||||
await coreStoreAPI.update({ key: configName }, { value: JSON.stringify(value), ...fileContent });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all core-store config from the db.
|
|
||||||
*
|
|
||||||
* @returns {object} Object with key value pairs of configs.
|
|
||||||
*/
|
|
||||||
getAllFromDatabase: async () => {
|
|
||||||
const coreStore = await strapi.query(coreStoreQueryString).find({ _limit: -1 });
|
|
||||||
let configs = {};
|
|
||||||
|
|
||||||
Object.values(coreStore).map( ({ id, value, key, ...config }) => {
|
|
||||||
// Check if the config should be excluded.
|
|
||||||
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(`${configPrefix}.${key}`);
|
|
||||||
if (shouldExclude) return;
|
|
||||||
|
|
||||||
// Do not export the _id field, as it is immutable
|
|
||||||
delete config._id;
|
|
||||||
|
|
||||||
configs[`${configPrefix}.${key}`] = { key, value: JSON.parse(value), ...config };
|
|
||||||
});
|
|
||||||
|
|
||||||
return configs;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import all core-store config files into the db.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
importAll: async () => {
|
|
||||||
// The main.importAllConfig service will loop the core-store.importSingle service.
|
|
||||||
await strapi.plugins['config-sync'].services.main.importAllConfig(configPrefix);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export a single core-store config to a file.
|
|
||||||
*
|
|
||||||
* @param {string} configName - The name of the config file.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
exportSingle: async (configName) => {
|
|
||||||
// @TODO: write export for a single core-store config.
|
|
||||||
},
|
|
||||||
};
|
|
206
services/main.js
206
services/main.js
|
@ -1,206 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const util = require('util');
|
|
||||||
const difference = require('../utils/getObjectDiff');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main services for config import/export.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
/**
|
|
||||||
* Write a single config file.
|
|
||||||
*
|
|
||||||
* @param {string} configType - The type of the config.
|
|
||||||
* @param {string} configName - The name of the config file.
|
|
||||||
* @param {string} fileContents - The JSON content of the config file.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
writeConfigFile: async (configType, configName, fileContents) => {
|
|
||||||
// Check if the config should be excluded.
|
|
||||||
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(`${configType}.${configName}`);
|
|
||||||
if (shouldExclude) return;
|
|
||||||
|
|
||||||
// Check if the JSON content should be minified.
|
|
||||||
const json =
|
|
||||||
!strapi.plugins['config-sync'].config.minify ?
|
|
||||||
JSON.stringify(fileContents, null, 2)
|
|
||||||
: JSON.stringify(fileContents);
|
|
||||||
|
|
||||||
if (!fs.existsSync(strapi.plugins['config-sync'].config.destination)) {
|
|
||||||
fs.mkdirSync(strapi.plugins['config-sync'].config.destination, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const writeFile = util.promisify(fs.writeFile);
|
|
||||||
await writeFile(`${strapi.plugins['config-sync'].config.destination}${configType}.${configName}.json`, json)
|
|
||||||
.then(() => {
|
|
||||||
// @TODO:
|
|
||||||
// Add logging for successfull config export.
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// @TODO:
|
|
||||||
// Add logging for failed config export.
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete config file.
|
|
||||||
*
|
|
||||||
* @param {string} configName - The name of the config file.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
deleteConfigFile: async (configName) => {
|
|
||||||
// Check if the config should be excluded.
|
|
||||||
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(`${configName}`);
|
|
||||||
if (shouldExclude) return;
|
|
||||||
|
|
||||||
fs.unlinkSync(`${strapi.plugins['config-sync'].config.destination}${configName}.json`);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read from a config file.
|
|
||||||
*
|
|
||||||
* @param {string} configType - The type of config.
|
|
||||||
* @param {string} configName - The name of the config file.
|
|
||||||
* @returns {object} The JSON content of the config file.
|
|
||||||
*/
|
|
||||||
readConfigFile: async (configType, configName) => {
|
|
||||||
const readFile = util.promisify(fs.readFile);
|
|
||||||
return await readFile(`${strapi.plugins['config-sync'].config.destination}${configType}.${configName}.json`)
|
|
||||||
.then((data) => {
|
|
||||||
return JSON.parse(data);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all the config JSON from the filesystem.
|
|
||||||
*
|
|
||||||
* @returns {object} Object with key value pairs of configs.
|
|
||||||
*/
|
|
||||||
getAllConfigFromFiles: async (configType = null) => {
|
|
||||||
if (!fs.existsSync(strapi.plugins['config-sync'].config.destination)) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const configFiles = fs.readdirSync(strapi.plugins['config-sync'].config.destination);
|
|
||||||
|
|
||||||
const getConfigs = async () => {
|
|
||||||
let fileConfigs = {};
|
|
||||||
|
|
||||||
await Promise.all(configFiles.map(async (file) => {
|
|
||||||
const type = file.split('.')[0]; // Grab the first part of the filename.
|
|
||||||
const name = file.split(/\.(.+)/)[1].split('.').slice(0, -1).join('.'); // Grab the rest of the filename minus the file extension.
|
|
||||||
|
|
||||||
if (
|
|
||||||
configType && configType !== type ||
|
|
||||||
!strapi.plugins['config-sync'].config.include.includes(type) ||
|
|
||||||
strapi.plugins['config-sync'].config.exclude.includes(`${type}.${name}`)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileContents = await strapi.plugins['config-sync'].services.main.readConfigFile(type, name);
|
|
||||||
fileConfigs[`${type}.${name}`] = fileContents;
|
|
||||||
}));
|
|
||||||
|
|
||||||
return fileConfigs;
|
|
||||||
};
|
|
||||||
|
|
||||||
return await getConfigs();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all the config JSON from the database.
|
|
||||||
*
|
|
||||||
* @returns {object} Object with key value pairs of configs.
|
|
||||||
*/
|
|
||||||
getAllConfigFromDatabase: async (configType = null) => {
|
|
||||||
const getConfigs = async () => {
|
|
||||||
let databaseConfigs = {};
|
|
||||||
|
|
||||||
await Promise.all(strapi.plugins['config-sync'].config.include.map(async (type) => {
|
|
||||||
if (configType && configType !== type) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = await strapi.plugins['config-sync'].services[type].getAllFromDatabase();
|
|
||||||
databaseConfigs = Object.assign(config, databaseConfigs);
|
|
||||||
}));
|
|
||||||
|
|
||||||
return databaseConfigs;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await getConfigs();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import all config files into the db.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
importAllConfig: async (configType = null) => {
|
|
||||||
const fileConfig = await strapi.plugins['config-sync'].services.main.getAllConfigFromFiles();
|
|
||||||
const databaseConfig = await strapi.plugins['config-sync'].services.main.getAllConfigFromDatabase();
|
|
||||||
|
|
||||||
const diff = difference(databaseConfig, fileConfig);
|
|
||||||
|
|
||||||
await Promise.all(Object.keys(diff).map(async (file) => {
|
|
||||||
const type = file.split('.')[0]; // Grab the first part of the filename.
|
|
||||||
const name = file.split(/\.(.+)/)[1]; // Grab the rest of the filename minus the file extension.
|
|
||||||
|
|
||||||
if (configType && configType !== type) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await strapi.plugins['config-sync'].services.main.importSingleConfig(type, name);
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export all config files.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
exportAllConfig: async (configType = null) => {
|
|
||||||
await Promise.all(strapi.plugins['config-sync'].config.include.map(async (type) => {
|
|
||||||
if (configType && configType !== type) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await strapi.plugins['config-sync'].services[type].exportAll();
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import a single config file into the db.
|
|
||||||
*
|
|
||||||
* @param {string} configType - The type of config.
|
|
||||||
* @param {string} configName - The name of the config file.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
importSingleConfig: async (configType, configName) => {
|
|
||||||
// Check if the config should be excluded.
|
|
||||||
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(`${configType}.${configName}`);
|
|
||||||
if (shouldExclude) return;
|
|
||||||
|
|
||||||
const fileContents = await strapi.plugins['config-sync'].services.main.readConfigFile(configType, configName);
|
|
||||||
|
|
||||||
await strapi.plugins['config-sync'].services[configType].importSingle(configName, fileContents);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export a single config file.
|
|
||||||
*
|
|
||||||
* @param {string} configType - The type of config.
|
|
||||||
* @param {string} configName - The name of the config file.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
exportSingleConfig: async (configType, configName) => {
|
|
||||||
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,152 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const { sanitizeEntity } = require('strapi-utils');
|
|
||||||
const difference = require('../utils/getObjectDiff');
|
|
||||||
|
|
||||||
const configPrefix = 'role-permissions'; // Should be the same as the filename.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import/Export for role-permissions configs.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
/**
|
|
||||||
* Export all role-permissions config to files.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
exportAll: async () => {
|
|
||||||
const formattedDiff = {
|
|
||||||
fileConfig: {},
|
|
||||||
databaseConfig: {},
|
|
||||||
diff: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fileConfig = await strapi.plugins['config-sync'].services.main.getAllConfigFromFiles(configPrefix);
|
|
||||||
const databaseConfig = await strapi.plugins['config-sync'].services.main.getAllConfigFromDatabase(configPrefix);
|
|
||||||
const diff = difference(databaseConfig, fileConfig);
|
|
||||||
|
|
||||||
formattedDiff.diff = diff;
|
|
||||||
|
|
||||||
Object.keys(diff).map((changedConfigName) => {
|
|
||||||
formattedDiff.fileConfig[changedConfigName] = fileConfig[changedConfigName];
|
|
||||||
formattedDiff.databaseConfig[changedConfigName] = databaseConfig[changedConfigName];
|
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.all(Object.entries(diff).map(async ([configName, config]) => {
|
|
||||||
// Check if the config should be excluded.
|
|
||||||
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(`${configName}`);
|
|
||||||
if (shouldExclude) return;
|
|
||||||
|
|
||||||
const currentConfig = formattedDiff.databaseConfig[configName];
|
|
||||||
|
|
||||||
if (
|
|
||||||
!currentConfig &&
|
|
||||||
formattedDiff.fileConfig[configName]
|
|
||||||
) {
|
|
||||||
await strapi.plugins['config-sync'].services.main.deleteConfigFile(configName);
|
|
||||||
} else {
|
|
||||||
await strapi.plugins['config-sync'].services.main.writeConfigFile(configPrefix, currentConfig.type, currentConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import a single role-permissions config file into the db.
|
|
||||||
*
|
|
||||||
* @param {string} configName - The name of the config file.
|
|
||||||
* @param {string} configContent - The JSON content of the config file.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
importSingle: async (configName, configContent) => {
|
|
||||||
// Check if the config should be excluded.
|
|
||||||
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(`${configPrefix}.${configName}`);
|
|
||||||
if (shouldExclude) return;
|
|
||||||
|
|
||||||
const service =
|
|
||||||
strapi.plugins['users-permissions'].services.userspermissions;
|
|
||||||
|
|
||||||
const role = await strapi
|
|
||||||
.query('role', 'users-permissions')
|
|
||||||
.findOne({ type: configName });
|
|
||||||
|
|
||||||
if (role && configContent === null) {
|
|
||||||
const publicRole = await strapi.query('role', 'users-permissions').findOne({ type: 'public' });
|
|
||||||
const publicRoleID = publicRole.id;
|
|
||||||
|
|
||||||
await service.deleteRole(role.id, publicRoleID);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const users = role ? role.users : [];
|
|
||||||
configContent.users = users;
|
|
||||||
|
|
||||||
if (!role) {
|
|
||||||
await service.createRole(configContent);
|
|
||||||
} else {
|
|
||||||
await service.updateRole(role.id, configContent);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all role-permissions config from the db.
|
|
||||||
*
|
|
||||||
* @returns {object} Object with key value pairs of configs.
|
|
||||||
*/
|
|
||||||
getAllFromDatabase: async () => {
|
|
||||||
const service =
|
|
||||||
strapi.plugins['users-permissions'].services.userspermissions;
|
|
||||||
|
|
||||||
const [roles, plugins] = await Promise.all([
|
|
||||||
service.getRoles(),
|
|
||||||
service.getPlugins(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const rolesWithPermissions = await Promise.all(
|
|
||||||
roles.map(async role => service.getRole(role.id, plugins))
|
|
||||||
);
|
|
||||||
|
|
||||||
const sanitizedRolesArray = rolesWithPermissions.map(role =>
|
|
||||||
sanitizeEntity(role, {
|
|
||||||
model: strapi.plugins['users-permissions'].models.role,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
let configs = {};
|
|
||||||
|
|
||||||
Object.values(sanitizedRolesArray).map(({ id, ...config }) => {
|
|
||||||
// Check if the config should be excluded.
|
|
||||||
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(`${configPrefix}.${config.type}`);
|
|
||||||
if (shouldExclude) return;
|
|
||||||
|
|
||||||
// Do not export the _id field, as it is immutable
|
|
||||||
delete config._id;
|
|
||||||
|
|
||||||
configs[`${configPrefix}.${config.type}`] = config;
|
|
||||||
});
|
|
||||||
|
|
||||||
return configs;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import all role-permissions config files into the db.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
importAll: async () => {
|
|
||||||
// The main.importAllConfig service will loop the role-permissions.importSingle service.
|
|
||||||
await strapi.plugins['config-sync'].services.main.importAllConfig(configPrefix);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export a single role-permissions config to a file.
|
|
||||||
*
|
|
||||||
* @param {string} configName - The name of the config file.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
exportSingle: async (configName) => {
|
|
||||||
// @TODO: write export for a single role-permissions config.
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,121 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import/Export for webhook configs.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const webhookQueryString = 'strapi_webhooks';
|
|
||||||
const configPrefix = 'webhooks'; // Should be the same as the filename.
|
|
||||||
const difference = require('../utils/getObjectDiff');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
/**
|
|
||||||
* Export all webhooks to config files.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
exportAll: async () => {
|
|
||||||
const formattedDiff = {
|
|
||||||
fileConfig: {},
|
|
||||||
databaseConfig: {},
|
|
||||||
diff: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fileConfig = await strapi.plugins['config-sync'].services.main.getAllConfigFromFiles(configPrefix);
|
|
||||||
const databaseConfig = await strapi.plugins['config-sync'].services.main.getAllConfigFromDatabase(configPrefix);
|
|
||||||
const diff = difference(databaseConfig, fileConfig);
|
|
||||||
|
|
||||||
formattedDiff.diff = diff;
|
|
||||||
|
|
||||||
Object.keys(diff).map((changedConfigName) => {
|
|
||||||
formattedDiff.fileConfig[changedConfigName] = fileConfig[changedConfigName];
|
|
||||||
formattedDiff.databaseConfig[changedConfigName] = databaseConfig[changedConfigName];
|
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.all(Object.entries(diff).map(async ([configName, config]) => {
|
|
||||||
// Check if the config should be excluded.
|
|
||||||
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(`${configName}`);
|
|
||||||
if (shouldExclude) return;
|
|
||||||
|
|
||||||
const currentConfig = formattedDiff.databaseConfig[configName];
|
|
||||||
|
|
||||||
if (
|
|
||||||
!currentConfig &&
|
|
||||||
formattedDiff.fileConfig[configName]
|
|
||||||
) {
|
|
||||||
await strapi.plugins['config-sync'].services.main.deleteConfigFile(configName);
|
|
||||||
} else {
|
|
||||||
await strapi.plugins['config-sync'].services.main.writeConfigFile(configPrefix, currentConfig.id, currentConfig);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import a single webhook config file into the db.
|
|
||||||
*
|
|
||||||
* @param {string} configName - The name of the config file.
|
|
||||||
* @param {string} configContent - The JSON content of the config file.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
importSingle: async (configName, configContent) => {
|
|
||||||
// Check if the config should be excluded.
|
|
||||||
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(`${configPrefix}.${configName}`);
|
|
||||||
if (shouldExclude) return;
|
|
||||||
|
|
||||||
const webhookAPI = strapi.query(webhookQueryString);
|
|
||||||
|
|
||||||
const configExists = await webhookAPI.findOne({ id: configName });
|
|
||||||
|
|
||||||
if (!configExists) {
|
|
||||||
await webhookAPI.create(configContent);
|
|
||||||
} else {
|
|
||||||
if (configContent === null) {
|
|
||||||
await webhookAPI.delete({ id: configName });
|
|
||||||
}
|
|
||||||
await webhookAPI.update({ id: configName }, configContent);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all webhook config from the db.
|
|
||||||
*
|
|
||||||
* @returns {object} Object with key value pairs of configs.
|
|
||||||
*/
|
|
||||||
getAllFromDatabase: async () => {
|
|
||||||
const webhooks = await strapi.query(webhookQueryString).find({ _limit: -1 });
|
|
||||||
let configs = {};
|
|
||||||
|
|
||||||
Object.values(webhooks).map( (config) => {
|
|
||||||
// Check if the config should be excluded.
|
|
||||||
const shouldExclude = strapi.plugins['config-sync'].config.exclude.includes(`${configPrefix}.${config.id}`);
|
|
||||||
if (shouldExclude) return;
|
|
||||||
|
|
||||||
// Do not export the _id field, as it is immutable
|
|
||||||
delete config._id;
|
|
||||||
|
|
||||||
configs[`${configPrefix}.${config.id}`] = config;
|
|
||||||
});
|
|
||||||
|
|
||||||
return configs;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import all webhook config files into the db.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
importAll: async () => {
|
|
||||||
// The main.importAllConfig service will loop the webhooks.importSingle service.
|
|
||||||
await strapi.plugins['config-sync'].services.main.importAllConfig(configPrefix);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export a single webhook into a config file.
|
|
||||||
*
|
|
||||||
* @param {string} configName - The name of the config file.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
exportSingle: async (configName) => {
|
|
||||||
// @TODO: write export for a single webhook config.
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = require('./admin/src').default;
|
|
@ -0,0 +1,17 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const bootstrap = require('./server/bootstrap');
|
||||||
|
const services = require('./server/services');
|
||||||
|
const routes = require('./server/routes');
|
||||||
|
const config = require('./server/config');
|
||||||
|
const controllers = require('./server/controllers');
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
return {
|
||||||
|
bootstrap,
|
||||||
|
routes,
|
||||||
|
config,
|
||||||
|
controllers,
|
||||||
|
services,
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,33 +0,0 @@
|
||||||
'use strict';
|
|
||||||
const { transform, isEqual, isArray, isObject } = require('lodash');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find difference between two objects
|
|
||||||
* @param {object} origObj - Source object to compare newObj against
|
|
||||||
* @param {object} newObj - New object with potential changes
|
|
||||||
* @return {object} differences
|
|
||||||
*/
|
|
||||||
const difference = (origObj, newObj) => {
|
|
||||||
function changes(newObj, origObj) {
|
|
||||||
let arrayIndexCounter = 0
|
|
||||||
|
|
||||||
const newObjChange = transform(newObj, function (result, value, key) {
|
|
||||||
if (!isEqual(value, origObj[key])) {
|
|
||||||
let resultKey = isArray(origObj) ? arrayIndexCounter++ : key
|
|
||||||
result[resultKey] = (isObject(value) && isObject(origObj[key])) ? changes(value, origObj[key]) : value
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const origObjChange = transform(origObj, function (result, value, key) {
|
|
||||||
if (!isEqual(value, newObj[key])) {
|
|
||||||
let resultKey = isArray(newObj) ? arrayIndexCounter++ : key
|
|
||||||
result[resultKey] = (isObject(value) && isObject(newObj[key])) ? changes(value, newObj[key]) : value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return Object.assign(newObjChange, origObjChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
return changes(newObj, origObj)
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = difference;
|
|
Loading…
Reference in New Issue