mirror of
https://github.com/aleksejs1/obsidian-contact-sync-plugin.git
synced 2026-07-22 05:48:16 +00:00
Improve code quality (#23)
* improve code quality * add complexity check * add dependency-cruiser * add ts-prune * add eslint-plugin-boundaries * add quality script * fix github actions
This commit is contained in:
parent
bf39688a94
commit
d3fa61e98c
39 changed files with 1319 additions and 350 deletions
24
.dependency-cruiser.cjs
Normal file
24
.dependency-cruiser.cjs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/** @type {import('dependency-cruiser').IConfiguration} */
|
||||
module.exports = {
|
||||
forbidden: [
|
||||
{
|
||||
name: 'no-circular',
|
||||
severity: 'error',
|
||||
comment:
|
||||
'This dependency is part of a circular relationship. You might want to revise ' +
|
||||
'your solution (e.g. use dependency inversion, standard interfaces, pull out ' +
|
||||
'common parts to a separate module)',
|
||||
from: {},
|
||||
to: { circular: true }
|
||||
}
|
||||
],
|
||||
options: {
|
||||
doNotFollow: {
|
||||
path: 'node_modules'
|
||||
},
|
||||
tsPreCompilationDeps: true,
|
||||
tsConfig: {
|
||||
fileName: 'tsconfig.json'
|
||||
}
|
||||
}
|
||||
};
|
||||
3
.github/workflows/docs.yml
vendored
3
.github/workflows/docs.yml
vendored
|
|
@ -4,9 +4,6 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
|
|||
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
|
|
@ -33,3 +33,6 @@ jobs:
|
|||
- name: Run type check
|
||||
run: npm run typecheck
|
||||
|
||||
- name: Run dependency check
|
||||
run: npm run lint:deps
|
||||
|
||||
|
|
|
|||
198
eslint.config.js
198
eslint.config.js
|
|
@ -1,9 +1,205 @@
|
|||
import eslint from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import boundaries from 'eslint-plugin-boundaries';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['dist/', 'node_modules/', 'coverage/', 'main.js'],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
prettier,
|
||||
{
|
||||
plugins: {
|
||||
boundaries,
|
||||
},
|
||||
settings: {
|
||||
'boundaries/elements': [
|
||||
{
|
||||
type: 'entry',
|
||||
pattern: 'src/main.ts',
|
||||
},
|
||||
{
|
||||
type: 'core',
|
||||
pattern: 'src/core/**/*',
|
||||
},
|
||||
{
|
||||
type: 'plugin',
|
||||
pattern: 'src/plugin/**/*',
|
||||
},
|
||||
{
|
||||
type: 'services',
|
||||
pattern: 'src/services/**/*',
|
||||
},
|
||||
{
|
||||
type: 'auth',
|
||||
pattern: 'src/auth/**/*',
|
||||
},
|
||||
{
|
||||
type: 'types',
|
||||
pattern: 'src/types/**/*',
|
||||
},
|
||||
{
|
||||
type: 'utils',
|
||||
pattern: 'src/utils/**/*',
|
||||
},
|
||||
{
|
||||
type: 'i18n',
|
||||
pattern: 'src/i18n/**/*',
|
||||
},
|
||||
{
|
||||
type: 'config',
|
||||
pattern: 'src/config/**/*',
|
||||
},
|
||||
{
|
||||
type: 'tests',
|
||||
pattern: 'src/__tests__/**/*',
|
||||
},
|
||||
{
|
||||
type: 'mocks',
|
||||
pattern: 'src/__mocks__/**/*',
|
||||
},
|
||||
],
|
||||
},
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'boundaries/entry-point': [
|
||||
'error',
|
||||
{
|
||||
default: 'allow',
|
||||
},
|
||||
],
|
||||
'boundaries/no-private': 'error',
|
||||
'boundaries/element-types': [
|
||||
'error',
|
||||
{
|
||||
default: 'disallow',
|
||||
rules: [
|
||||
{
|
||||
from: 'entry',
|
||||
allow: [
|
||||
'core',
|
||||
'plugin',
|
||||
'services',
|
||||
'auth',
|
||||
'types',
|
||||
'utils',
|
||||
'i18n',
|
||||
'config',
|
||||
],
|
||||
},
|
||||
{
|
||||
from: 'core',
|
||||
allow: [
|
||||
'types',
|
||||
'utils',
|
||||
'config',
|
||||
'i18n',
|
||||
'core',
|
||||
],
|
||||
},
|
||||
{
|
||||
from: 'plugin',
|
||||
allow: [
|
||||
'core',
|
||||
'services',
|
||||
'auth',
|
||||
'types',
|
||||
'utils',
|
||||
'i18n',
|
||||
'config',
|
||||
'plugin',
|
||||
],
|
||||
},
|
||||
{
|
||||
from: 'services',
|
||||
allow: ['types', 'utils', 'config', 'services'],
|
||||
},
|
||||
{
|
||||
from: 'auth',
|
||||
allow: ['types', 'utils', 'config', 'auth'],
|
||||
},
|
||||
{
|
||||
from: 'types',
|
||||
allow: ['types'],
|
||||
},
|
||||
{
|
||||
from: 'utils',
|
||||
allow: ['utils'],
|
||||
},
|
||||
{
|
||||
from: 'i18n',
|
||||
allow: ['i18n', 'types'],
|
||||
},
|
||||
{
|
||||
from: 'config',
|
||||
allow: ['config'],
|
||||
},
|
||||
{
|
||||
from: 'tests',
|
||||
allow: [
|
||||
'core',
|
||||
'plugin',
|
||||
'services',
|
||||
'auth',
|
||||
'types',
|
||||
'utils',
|
||||
'i18n',
|
||||
'config',
|
||||
'mocks',
|
||||
'tests',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
curly: ['error', 'all'],
|
||||
eqeqeq: ['error', 'always'],
|
||||
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
'@typescript-eslint/no-unsafe-return': 'warn',
|
||||
'@typescript-eslint/restrict-template-expressions': [
|
||||
'error',
|
||||
{
|
||||
allowNumber: true,
|
||||
allowBoolean: true,
|
||||
},
|
||||
],
|
||||
complexity: ['error', 10],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/__{tests,mocks}__/**/*.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-extraneous-class': 'off',
|
||||
'@typescript-eslint/require-await': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/restrict-template-expressions': 'off',
|
||||
'@typescript-eslint/no-useless-constructor': 'off',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
|||
614
package-lock.json
generated
614
package-lock.json
generated
|
|
@ -19,8 +19,10 @@
|
|||
"@types/jest": "^29.5.14",
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.0",
|
||||
"@typescript-eslint/parser": "^8.31.0",
|
||||
"dependency-cruiser": "^17.3.4",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-boundaries": "^5.3.1",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"lint-staged": "^15.5.1",
|
||||
|
|
@ -28,6 +30,7 @@
|
|||
"prettier": "^3.5.3",
|
||||
"rollup": "^4.40.0",
|
||||
"ts-jest": "^29.3.2",
|
||||
"ts-prune": "^0.10.3",
|
||||
"typedoc": "^0.28.3",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.31.0"
|
||||
|
|
@ -549,6 +552,22 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@boundaries/elements": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@boundaries/elements/-/elements-1.1.2.tgz",
|
||||
"integrity": "sha512-DnGHL+v36YVMoWhWZqyJYVZ9dapNm7h4N3/P0lDPirJj0CHVPkjChMCCotj74cg6LW7iPJZFGrdEfh0X0g2bmQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"eslint-import-resolver-node": "0.3.9",
|
||||
"eslint-module-utils": "2.12.1",
|
||||
"handlebars": "4.7.8",
|
||||
"is-core-module": "2.16.1",
|
||||
"micromatch": "4.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
|
||||
|
|
@ -1316,6 +1335,41 @@
|
|||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
"run-parallel": "^1.1.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.stat": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.walk": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
"fastq": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-node-resolve": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz",
|
||||
|
|
@ -1775,6 +1829,40 @@
|
|||
"@sinonjs/commons": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ts-morph/common": {
|
||||
"version": "0.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.12.3.tgz",
|
||||
"integrity": "sha512-4tUmeLyXJnJWvTFOKtcNJ1yh0a3SsTLi2MUoyj8iUNznFRN1ZquaNe7Oukqrnki2FzZkm0J9adCNLDZxUzvj+w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fast-glob": "^3.2.7",
|
||||
"minimatch": "^3.0.4",
|
||||
"mkdirp": "^1.0.4",
|
||||
"path-browserify": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ts-morph/common/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ts-morph/common/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||
|
|
@ -1935,6 +2023,12 @@
|
|||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/parse-json": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
|
||||
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||
|
|
@ -2238,6 +2332,24 @@
|
|||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-jsx-walk": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz",
|
||||
"integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/acorn-loose": {
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.2.tgz",
|
||||
"integrity": "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"acorn": "^8.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||
|
|
@ -2779,6 +2891,12 @@
|
|||
"node": ">= 0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/code-block-writer": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.3.tgz",
|
||||
"integrity": "sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/collect-v8-coverage": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz",
|
||||
|
|
@ -2837,6 +2955,31 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cosmiconfig": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
||||
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/parse-json": "^4.0.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
"parse-json": "^5.0.0",
|
||||
"path-type": "^4.0.0",
|
||||
"yaml": "^1.10.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/cosmiconfig/node_modules/yaml": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/create-jest": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
|
||||
|
|
@ -2938,6 +3081,53 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dependency-cruiser": {
|
||||
"version": "17.3.4",
|
||||
"resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-17.3.4.tgz",
|
||||
"integrity": "sha512-co6WQmyTl7KLrL6OR3cCFl0Ol2SBoY5e5kQBfE6ZR8+W+GCBqoq7A67br51O/Tp2ixL2ZVmLrPcyddtuXKBnYA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"acorn-jsx": "5.3.2",
|
||||
"acorn-jsx-walk": "2.0.0",
|
||||
"acorn-loose": "8.5.2",
|
||||
"acorn-walk": "8.3.4",
|
||||
"commander": "14.0.2",
|
||||
"enhanced-resolve": "5.18.4",
|
||||
"ignore": "7.0.5",
|
||||
"interpret": "3.1.1",
|
||||
"is-installed-globally": "1.0.0",
|
||||
"json5": "2.2.3",
|
||||
"memoize": "10.2.0",
|
||||
"picomatch": "4.0.3",
|
||||
"prompts": "2.4.2",
|
||||
"rechoir": "0.8.0",
|
||||
"safe-regex": "2.1.1",
|
||||
"semver": "7.7.3",
|
||||
"tsconfig-paths-webpack-plugin": "4.2.0",
|
||||
"watskeburt": "5.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"depcruise": "bin/dependency-cruise.mjs",
|
||||
"depcruise-baseline": "bin/depcruise-baseline.mjs",
|
||||
"depcruise-fmt": "bin/depcruise-fmt.mjs",
|
||||
"depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs",
|
||||
"dependency-cruise": "bin/dependency-cruise.mjs",
|
||||
"dependency-cruiser": "bin/dependency-cruise.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.12||^22||>=24"
|
||||
}
|
||||
},
|
||||
"node_modules/dependency-cruiser/node_modules/commander": {
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
|
||||
"integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-newline": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
|
||||
|
|
@ -2994,6 +3184,19 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.4",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz",
|
||||
"integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"tapable": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
|
|
@ -3129,6 +3332,71 @@
|
|||
"eslint": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-import-resolver-node": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
|
||||
"integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^3.2.7",
|
||||
"is-core-module": "^2.13.0",
|
||||
"resolve": "^1.22.4"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-import-resolver-node/node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-module-utils": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz",
|
||||
"integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^3.2.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-module-utils/node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-boundaries": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-boundaries/-/eslint-plugin-boundaries-5.3.1.tgz",
|
||||
"integrity": "sha512-91StsOYtDyrna1fyRJ+1Ps5CnrfyFLbdCouPZ3E/o2cllLxJke3OoScdqjpBSl7pNEYbojhpNlurQAr30sf9Bg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@boundaries/elements": "1.1.2",
|
||||
"chalk": "4.1.2",
|
||||
"eslint-import-resolver-node": "0.3.9",
|
||||
"eslint-module-utils": "2.12.1",
|
||||
"micromatch": "4.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
|
||||
|
|
@ -3368,6 +3636,34 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
"glob-parent": "^5.1.2",
|
||||
"merge2": "^1.3.0",
|
||||
"micromatch": "^4.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob/node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
|
|
@ -3382,6 +3678,15 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fb-watchman": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
|
||||
|
|
@ -3621,6 +3926,21 @@
|
|||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/global-directory": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz",
|
||||
"integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ini": "4.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
||||
|
|
@ -3795,6 +4115,24 @@
|
|||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz",
|
||||
"integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/interpret": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz",
|
||||
"integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||
|
|
@ -3864,6 +4202,22 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-installed-globally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz",
|
||||
"integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"global-directory": "^4.0.1",
|
||||
"is-path-inside": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-module": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
|
||||
|
|
@ -3881,6 +4235,18 @@
|
|||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-path-inside": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz",
|
||||
"integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
|
|
@ -4935,6 +5301,12 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
|
|
@ -5134,6 +5506,21 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/memoize": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/memoize/-/memoize-10.2.0.tgz",
|
||||
"integrity": "sha512-DeC6b7QBrZsRs3Y02A6A7lQyzFbsQbqgjI6UW0GigGWV+u1s25TycMr0XHZE4cJce7rY/vyw2ctMQqfDkIhUEA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mimic-function": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/memoize?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
|
|
@ -5141,6 +5528,15 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
|
|
@ -5217,6 +5613,18 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
|
|
@ -5418,6 +5826,12 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/path-browserify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
|
|
@ -5455,6 +5869,15 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
|
@ -5672,6 +6095,26 @@
|
|||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
|
|
@ -5679,6 +6122,27 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rechoir": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
|
||||
"integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"resolve": "^1.20.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regexp-tree": {
|
||||
"version": "0.1.27",
|
||||
"resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
|
||||
"integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"regexp-tree": "bin/regexp-tree"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
|
|
@ -5799,6 +6263,16 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"iojs": ">=1.0.0",
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rfdc": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||
|
|
@ -5848,6 +6322,38 @@
|
|||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-regex": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz",
|
||||
"integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"regexp-tree": "~0.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
|
|
@ -6140,6 +6646,19 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
|
||||
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||
|
|
@ -6216,6 +6735,15 @@
|
|||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/true-myth": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/true-myth/-/true-myth-4.1.1.tgz",
|
||||
"integrity": "sha512-rqy30BSpxPznbbTcAcci90oZ1YR4DqvKcNXNerG5gQBU2v4jk0cygheiul5J6ExIMrgDVuanv/MkGfqZbKrNNg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "10.* || >= 12.*"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
|
|
@ -6295,6 +6823,16 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-morph": {
|
||||
"version": "13.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-13.0.3.tgz",
|
||||
"integrity": "sha512-pSOfUMx8Ld/WUreoSzvMFQG5i9uEiWIsBYjpU9+TTASOeUa89j5HykomeqVULm1oqWtBdleI3KEFRLrlA3zGIw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ts-morph/common": "~0.12.3",
|
||||
"code-block-writer": "^11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
|
|
@ -6338,6 +6876,70 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ts-prune": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-prune/-/ts-prune-0.10.3.tgz",
|
||||
"integrity": "sha512-iS47YTbdIcvN8Nh/1BFyziyUqmjXz7GVzWu02RaZXqb+e/3Qe1B7IQ4860krOeCGUeJmterAlaM2FRH0Ue0hjw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"commander": "^6.2.1",
|
||||
"cosmiconfig": "^7.0.1",
|
||||
"json5": "^2.1.3",
|
||||
"lodash": "^4.17.21",
|
||||
"true-myth": "^4.1.0",
|
||||
"ts-morph": "^13.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"ts-prune": "lib/index.js"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-prune/node_modules/commander": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
|
||||
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfig-paths": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
|
||||
"integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"json5": "^2.2.2",
|
||||
"minimist": "^1.2.6",
|
||||
"strip-bom": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfig-paths-webpack-plugin": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz",
|
||||
"integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.0",
|
||||
"enhanced-resolve": "^5.7.0",
|
||||
"tapable": "^2.2.1",
|
||||
"tsconfig-paths": "^4.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfig-paths/node_modules/strip-bom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
|
|
@ -6548,6 +7150,18 @@
|
|||
"makeerror": "1.0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/watskeburt": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-5.0.0.tgz",
|
||||
"integrity": "sha512-fEMhfIzu9WOuAJdDcTT+aPjn0JHI2+UeJ+zWSEs/tgMvc+MFDZVmhlZ8C1uJWXax1ETYc4trUnHFHyx2DrG0jQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"watskeburt": "dist/run-cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.12||^22.13||>=24.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@
|
|||
"clean": "rm -rf dist",
|
||||
"build": "npm run clean && npx rollup -c",
|
||||
"doc": "npx typedoc",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint:deps": "depcruise src",
|
||||
"lint:prune": "ts-prune",
|
||||
"quality": "npm run typecheck && npm run lint:deps && npm run lint:prune && npm run lint && npm run test"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
|
@ -24,8 +27,10 @@
|
|||
"@types/jest": "^29.5.14",
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.0",
|
||||
"@typescript-eslint/parser": "^8.31.0",
|
||||
"dependency-cruiser": "^17.3.4",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-boundaries": "^5.3.1",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"lint-staged": "^15.5.1",
|
||||
|
|
@ -33,6 +38,7 @@
|
|||
"prettier": "^3.5.3",
|
||||
"rollup": "^4.40.0",
|
||||
"ts-jest": "^29.3.2",
|
||||
"ts-prune": "^0.10.3",
|
||||
"typedoc": "^0.28.3",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.31.0"
|
||||
|
|
|
|||
|
|
@ -8,13 +8,12 @@ export class Vault {
|
|||
}
|
||||
}
|
||||
export class TFile {
|
||||
path: string = '';
|
||||
path = '';
|
||||
}
|
||||
export class TFolder {
|
||||
children: unknown[] = [];
|
||||
}
|
||||
export class Notice {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
constructor(_message: string) {
|
||||
// console.debug('Notice:', message);
|
||||
}
|
||||
|
|
@ -27,7 +26,7 @@ export class FileManager {
|
|||
}
|
||||
}
|
||||
export class MockMetadataCache {
|
||||
private fileCacheMap: Map<string, unknown> = new Map();
|
||||
private fileCacheMap = new Map<string, unknown>();
|
||||
setFileCache(file: TFile, cache: unknown) {
|
||||
this.fileCacheMap.set(file.path, cache);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ContactAuditService } from '../core/ContactAuditService';
|
||||
import { ContactAuditService } from '../services/ContactAuditService';
|
||||
import { App, TFolder, TFile } from 'obsidian'; // Mock these
|
||||
import { GoogleContactsService } from '../core/GoogleContactsService';
|
||||
import { GoogleContactsService } from '../services/GoogleContactsService';
|
||||
import { ContactSyncSettings } from '../types/Settings';
|
||||
import { getAllMarkdownFilesInFolder } from '../utils/getAllMarkdownFilesInFolder';
|
||||
|
||||
|
|
@ -48,6 +48,7 @@ describe('ContactAuditService', () => {
|
|||
contactsFolder: 'Contacts',
|
||||
syncLabel: 'My Contacts',
|
||||
accessToken: 'valid-token',
|
||||
propertyNamePrefix: '',
|
||||
} as unknown as ContactSyncSettings;
|
||||
|
||||
// Default valid mocks
|
||||
|
|
@ -57,10 +58,13 @@ describe('ContactAuditService', () => {
|
|||
(googleService.fetchGoogleContacts as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
// Mock TFolder return
|
||||
if (!mockVault.getAbstractFileByPath)
|
||||
if (!mockVault.getAbstractFileByPath) {
|
||||
throw new Error('mockVault.getAbstractFileByPath is undefined');
|
||||
}
|
||||
mockVault.getAbstractFileByPath.mockImplementation((path: string) => {
|
||||
if (path === 'Contacts') return new TFolder(); // Using mocked TFolder
|
||||
if (path === 'Contacts') {
|
||||
return new TFolder();
|
||||
} // Using mocked TFolder
|
||||
return null;
|
||||
});
|
||||
|
||||
|
|
@ -72,8 +76,9 @@ describe('ContactAuditService', () => {
|
|||
});
|
||||
|
||||
it('should handle missing contacts folder', async () => {
|
||||
if (!mockVault.getAbstractFileByPath)
|
||||
if (!mockVault.getAbstractFileByPath) {
|
||||
throw new Error('mockVault.getAbstractFileByPath is undefined');
|
||||
}
|
||||
mockVault.getAbstractFileByPath.mockReturnValue(null);
|
||||
await service.auditContacts('token');
|
||||
// valid, implies it returned early without throwing
|
||||
|
|
@ -133,11 +138,16 @@ describe('ContactAuditService', () => {
|
|||
]);
|
||||
|
||||
// Mock metadata
|
||||
if (!mockMetadataCache.getFileCache)
|
||||
if (!mockMetadataCache.getFileCache) {
|
||||
throw new Error('mockMetadataCache.getFileCache is undefined');
|
||||
}
|
||||
mockMetadataCache.getFileCache.mockImplementation((file: TFile) => {
|
||||
if (file === fileA) return { frontmatter: { id: 'contactA' } };
|
||||
if (file === fileB) return { frontmatter: { id: 'contactB' } };
|
||||
if (file === fileA) {
|
||||
return { frontmatter: { id: 'contactA' } };
|
||||
}
|
||||
if (file === fileB) {
|
||||
return { frontmatter: { id: 'contactB' } };
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
|
|
@ -170,8 +180,9 @@ describe('ContactAuditService', () => {
|
|||
|
||||
(getAllMarkdownFilesInFolder as jest.Mock).mockReturnValue([fileA]);
|
||||
|
||||
if (!mockMetadataCache.getFileCache)
|
||||
if (!mockMetadataCache.getFileCache) {
|
||||
throw new Error('mockMetadataCache.getFileCache is undefined');
|
||||
}
|
||||
mockMetadataCache.getFileCache.mockReturnValue({
|
||||
frontmatter: { id: 'contactA' },
|
||||
});
|
||||
|
|
@ -208,11 +219,16 @@ describe('ContactAuditService', () => {
|
|||
|
||||
(getAllMarkdownFilesInFolder as jest.Mock).mockReturnValue([fileA, fileB]);
|
||||
|
||||
if (!mockMetadataCache.getFileCache)
|
||||
if (!mockMetadataCache.getFileCache) {
|
||||
throw new Error('mockMetadataCache.getFileCache is undefined');
|
||||
}
|
||||
mockMetadataCache.getFileCache.mockImplementation((file: TFile) => {
|
||||
if (file === fileA) return { frontmatter: { id: 'contactA' } };
|
||||
if (file === fileB) return { frontmatter: { id: 'contactB' } };
|
||||
if (file === fileA) {
|
||||
return { frontmatter: { id: 'contactA' } };
|
||||
}
|
||||
if (file === fileB) {
|
||||
return { frontmatter: { id: 'contactB' } };
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { MockMetadataCache } from 'src/__mocks__/obsidian';
|
||||
import { ContactNoteWriter } from '../../core/ContactNoteWriter';
|
||||
import { ContactNoteWriter } from '../../services/ContactNoteWriter';
|
||||
import { Vault, TFile, FileStats, MetadataCache, FileManager } from 'obsidian';
|
||||
import { GoogleContact } from 'src/types/Contact';
|
||||
import { ContactNoteConfig } from 'src/types/ContactNoteConfig';
|
||||
|
|
@ -55,8 +55,25 @@ jest.mock('src/utils/getAllMarkdownFilesInFolder', () => ({
|
|||
getAllMarkdownFilesInFolder: jest.fn(),
|
||||
}));
|
||||
|
||||
class TestContactNoteWriter extends ContactNoteWriter {
|
||||
public scanFiles(
|
||||
files: TFile[],
|
||||
propertyPrefix: string
|
||||
): Record<string, TFile> {
|
||||
return super.scanFiles(files, propertyPrefix);
|
||||
}
|
||||
|
||||
public hasSyncLabel(
|
||||
contact: GoogleContact,
|
||||
syncLabel: string,
|
||||
labelMap: Record<string, string>
|
||||
): boolean {
|
||||
return super.hasSyncLabel(contact, syncLabel, labelMap);
|
||||
}
|
||||
}
|
||||
|
||||
describe('ContactNoteWriter', () => {
|
||||
let contactNoteWriter: ContactNoteWriter;
|
||||
let contactNoteWriter: TestContactNoteWriter;
|
||||
let vault: Vault;
|
||||
let metadataCache: MetadataCache;
|
||||
let fileManager: FileManager;
|
||||
|
|
@ -72,7 +89,7 @@ describe('ContactNoteWriter', () => {
|
|||
getFileByPath: jest.fn(),
|
||||
processFrontMatter: jest.fn(),
|
||||
} as unknown as FileManager;
|
||||
contactNoteWriter = new ContactNoteWriter(
|
||||
contactNoteWriter = new TestContactNoteWriter(
|
||||
vault,
|
||||
metadataCache,
|
||||
fileManager
|
||||
|
|
@ -96,7 +113,7 @@ describe('ContactNoteWriter', () => {
|
|||
(vault.getFolderByPath as jest.Mock).mockReturnValue(mockFolder);
|
||||
(getAllMarkdownFilesInFolder as jest.Mock).mockReturnValue([]);
|
||||
(vault.create as jest.Mock).mockResolvedValue(undefined);
|
||||
(vault.getFileByPath as jest.Mock).mockResolvedValue(null);
|
||||
(vault.getFileByPath as jest.Mock).mockReturnValue(null);
|
||||
|
||||
const config: ContactNoteConfig = {
|
||||
prefix: 'prefix-',
|
||||
|
|
@ -145,7 +162,7 @@ describe('ContactNoteWriter', () => {
|
|||
(vault.getFolderByPath as jest.Mock).mockReturnValue(mockFolder);
|
||||
(getAllMarkdownFilesInFolder as jest.Mock).mockReturnValue([]);
|
||||
(vault.create as jest.Mock).mockResolvedValue(undefined);
|
||||
(vault.getFileByPath as jest.Mock).mockResolvedValue(null);
|
||||
(vault.getFileByPath as jest.Mock).mockReturnValue(null);
|
||||
|
||||
const config: ContactNoteConfig = {
|
||||
prefix: 'prefix-',
|
||||
|
|
@ -193,7 +210,7 @@ describe('ContactNoteWriter', () => {
|
|||
(vault.getFolderByPath as jest.Mock).mockReturnValue(mockFolder);
|
||||
(getAllMarkdownFilesInFolder as jest.Mock).mockReturnValue([]);
|
||||
(vault.create as jest.Mock).mockResolvedValue(undefined);
|
||||
(vault.getFileByPath as jest.Mock).mockResolvedValue(null);
|
||||
(vault.getFileByPath as jest.Mock).mockReturnValue(null);
|
||||
|
||||
const config: ContactNoteConfig = {
|
||||
prefix: 'prefix-',
|
||||
|
|
@ -235,7 +252,7 @@ describe('ContactNoteWriter', () => {
|
|||
(vault.getFolderByPath as jest.Mock).mockReturnValue(mockFolder);
|
||||
(getAllMarkdownFilesInFolder as jest.Mock).mockReturnValue([]);
|
||||
(vault.create as jest.Mock).mockResolvedValue(undefined);
|
||||
(vault.getFileByPath as jest.Mock).mockResolvedValue(null);
|
||||
(vault.getFileByPath as jest.Mock).mockReturnValue(null);
|
||||
|
||||
const config: ContactNoteConfig = {
|
||||
prefix: 'prefix-',
|
||||
|
|
@ -275,7 +292,7 @@ describe('ContactNoteWriter', () => {
|
|||
(vault.getFolderByPath as jest.Mock).mockReturnValue(mockFolder);
|
||||
(getAllMarkdownFilesInFolder as jest.Mock).mockReturnValue([]);
|
||||
(vault.create as jest.Mock).mockResolvedValue(undefined);
|
||||
(vault.getFileByPath as jest.Mock).mockResolvedValue(null);
|
||||
(vault.getFileByPath as jest.Mock).mockReturnValue(null);
|
||||
|
||||
const config: ContactNoteConfig = {
|
||||
prefix: 'prefix-',
|
||||
|
|
@ -323,10 +340,7 @@ describe('ContactNoteWriter', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const result = await contactNoteWriter['scanFiles'](
|
||||
mockFiles,
|
||||
'propertyPrefix-'
|
||||
);
|
||||
const result = contactNoteWriter.scanFiles(mockFiles, 'propertyPrefix-');
|
||||
|
||||
expect(result).toEqual({
|
||||
'123': mockFiles[0], // Expect the file with id 123
|
||||
|
|
@ -348,7 +362,7 @@ describe('ContactNoteWriter', () => {
|
|||
};
|
||||
const mockLabelMap = { family: 'group1' };
|
||||
|
||||
const result = contactNoteWriter['hasSyncLabel'](
|
||||
const result = contactNoteWriter.hasSyncLabel(
|
||||
mockContact,
|
||||
'family',
|
||||
mockLabelMap
|
||||
|
|
@ -370,7 +384,7 @@ describe('ContactNoteWriter', () => {
|
|||
};
|
||||
const mockLabelMap = { family: 'group1' };
|
||||
|
||||
const result = contactNoteWriter['hasSyncLabel'](
|
||||
const result = contactNoteWriter.hasSyncLabel(
|
||||
mockContact,
|
||||
'family',
|
||||
mockLabelMap
|
||||
|
|
@ -386,7 +400,7 @@ describe('ContactNoteWriter', () => {
|
|||
};
|
||||
const mockLabelMap = { family: 'group1' };
|
||||
|
||||
const result = contactNoteWriter['hasSyncLabel'](
|
||||
const result = contactNoteWriter.hasSyncLabel(
|
||||
mockContact,
|
||||
'family',
|
||||
mockLabelMap
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { MockMetadataCache } from 'src/__mocks__/obsidian';
|
||||
import { ContactNoteWriter } from '../../core/ContactNoteWriter';
|
||||
import { ContactNoteWriter } from '../../services/ContactNoteWriter';
|
||||
import { FileManager, MetadataCache, Vault } from 'obsidian';
|
||||
import { GoogleContact } from 'src/types/Contact';
|
||||
import { ContactNoteConfig } from 'src/types/ContactNoteConfig';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GoogleContactsService } from '../../core/GoogleContactsService';
|
||||
import { GoogleContactsService } from '../../services/GoogleContactsService';
|
||||
import { requestUrl } from 'obsidian';
|
||||
import {
|
||||
URL_PEOPLE_CONNECTIONS,
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ describe('getAllMarkdownFilesInFolder', () => {
|
|||
}
|
||||
}
|
||||
|
||||
const isTFolder = (obj: MockTFolder): obj is MockTFolder =>
|
||||
obj && obj.__isMockTFolder;
|
||||
const isTFile = (obj: MockTFile): obj is MockTFile =>
|
||||
obj && obj.__isMockTFile;
|
||||
const isTFolder = (obj: unknown): obj is MockTFolder =>
|
||||
typeof obj === 'object' && obj !== null && '__isMockTFolder' in obj;
|
||||
const isTFile = (obj: unknown): obj is MockTFile =>
|
||||
typeof obj === 'object' && obj !== null && '__isMockTFile' in obj;
|
||||
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(TFolder, Symbol.hasInstance, {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
import type { ContactSyncSettings } from '../types/Settings';
|
||||
import { requestUrl } from 'obsidian';
|
||||
import { URL_OAUTH_TOKEN, URI_OATUH_REDIRECT } from '../config';
|
||||
import { RequestUrlResponse } from 'obsidian';
|
||||
|
||||
interface TokenResponse {
|
||||
access_token: string;
|
||||
refresh_token?: string;
|
||||
expires_in: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages OAuth2 authentication for accessing the Google contacts API.
|
||||
|
|
@ -41,7 +48,7 @@ export class AuthManager {
|
|||
redirect_uri: URI_OATUH_REDIRECT,
|
||||
grant_type: 'authorization_code',
|
||||
};
|
||||
const response = await requestUrl({
|
||||
const res: RequestUrlResponse = await requestUrl({
|
||||
url: URL_OAUTH_TOKEN,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -50,10 +57,10 @@ export class AuthManager {
|
|||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const data = await response.json;
|
||||
this.accessToken = await data.access_token;
|
||||
this.refreshToken = (await data.refresh_token) || this.refreshToken;
|
||||
this.tokenExpiresAt = Date.now() + (await data.expires_in) * 1000;
|
||||
const data = res.json as TokenResponse;
|
||||
this.accessToken = data.access_token;
|
||||
this.refreshToken = data.refresh_token ?? this.refreshToken;
|
||||
this.tokenExpiresAt = Date.now() + data.expires_in * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -81,7 +88,7 @@ export class AuthManager {
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await requestUrl({
|
||||
const res: RequestUrlResponse = await requestUrl({
|
||||
url: URL_OAUTH_TOKEN,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
|
|
@ -93,7 +100,7 @@ export class AuthManager {
|
|||
}).toString(),
|
||||
});
|
||||
|
||||
const data = await response.json;
|
||||
const data = res.json as TokenResponse;
|
||||
if (!data.access_token) {
|
||||
throw new Error('Failed to refresh token');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ export const URL_OAUTH_AUTH = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|||
export const URL_OAUTH_TOKEN = 'https://oauth2.googleapis.com/token';
|
||||
|
||||
/** Google People API endpoints */
|
||||
export const URL_PEOPLE_BASE = 'https://people.googleapis.com/v1';
|
||||
const URL_PEOPLE_BASE = 'https://people.googleapis.com/v1';
|
||||
export const URL_PEOPLE_CONNECTIONS = `${URL_PEOPLE_BASE}/people/me/connections`;
|
||||
export const PERSONAL_FIELDS = `names,emailAddresses,phoneNumbers,birthdays,memberships,metadata,addresses,biographies,organizations`;
|
||||
export const URL_CONTACT_GROUPS = `${URL_PEOPLE_BASE}/contactGroups?pageSize=1000`;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { LabelAdapter } from './adapters/LabelAdapter';
|
|||
/**
|
||||
* Formatter class responsible for coordinating field extraction and key generation.
|
||||
*/
|
||||
export class Formatter {
|
||||
class Formatter {
|
||||
constructor(
|
||||
private adapters: Record<string, FieldAdapter>,
|
||||
private strategy: KeyNamingStrategy
|
||||
|
|
@ -48,7 +48,7 @@ export class Formatter {
|
|||
results.forEach((result, arrayIndex) => {
|
||||
// Use result.index if present (for grouping subfields),
|
||||
// otherwise use array index
|
||||
const index = result.index !== undefined ? result.index : arrayIndex;
|
||||
const index = result.index ?? arrayIndex;
|
||||
const key = this.strategy.generateKey(
|
||||
fieldId,
|
||||
index,
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ export class AddressAdapter implements FieldAdapter {
|
|||
contact: GoogleContact,
|
||||
context?: Record<string, unknown>
|
||||
): ExtractionResult[] {
|
||||
const addresses = contact.addresses || [];
|
||||
if (addresses.length === 0) return [];
|
||||
const addresses = contact.addresses ?? [];
|
||||
if (addresses.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if we're using VCF strategy
|
||||
const isVcfStrategy =
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { GoogleContact } from '../../types/Contact';
|
|||
|
||||
export class BioAdapter implements FieldAdapter {
|
||||
extract(contact: GoogleContact): ExtractionResult[] {
|
||||
return (contact.biographies || [])
|
||||
return (contact.biographies ?? [])
|
||||
.filter((item) => item.value)
|
||||
.map((item) => ({ value: item.value }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import { GoogleContact } from '../../types/Contact';
|
|||
|
||||
export class BirthdayAdapter implements FieldAdapter {
|
||||
extract(contact: GoogleContact): ExtractionResult[] {
|
||||
if (!contact.birthdays || contact.birthdays.length === 0) return [];
|
||||
if (!contact.birthdays || contact.birthdays.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const results: ExtractionResult[] = [];
|
||||
contact.birthdays.forEach((bday) => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { GoogleContact } from '../../types/Contact';
|
|||
|
||||
export class DepartmentAdapter implements FieldAdapter {
|
||||
extract(contact: GoogleContact): ExtractionResult[] {
|
||||
return (contact.organizations || [])
|
||||
return (contact.organizations ?? [])
|
||||
.filter((item) => item.department)
|
||||
.map((item) => ({ value: item.department }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { GoogleContact } from '../../types/Contact';
|
|||
|
||||
export class EmailAdapter implements FieldAdapter {
|
||||
extract(contact: GoogleContact): ExtractionResult[] {
|
||||
return (contact.emailAddresses || [])
|
||||
return (contact.emailAddresses ?? [])
|
||||
.filter((item) => item.value)
|
||||
.map((item) => ({ value: item.value }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import { GoogleContact } from '../../types/Contact';
|
|||
|
||||
export class GoogleIdAdapter implements FieldAdapter {
|
||||
extract(contact: GoogleContact): ExtractionResult[] {
|
||||
if (!contact.resourceName) return [];
|
||||
if (!contact.resourceName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// resourceName is like "people/c1234567890"
|
||||
// We want the ID part "c1234567890"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { GoogleContact } from '../../types/Contact';
|
|||
|
||||
export class JobTitleAdapter implements FieldAdapter {
|
||||
extract(contact: GoogleContact): ExtractionResult[] {
|
||||
return (contact.organizations || [])
|
||||
return (contact.organizations ?? [])
|
||||
.filter((item) => item.title)
|
||||
.map((item) => ({ value: item.title }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
import { FieldAdapter, ExtractionResult } from '../interfaces';
|
||||
import { GoogleContact } from '../../types/Contact';
|
||||
|
||||
export interface LabelAdapterContext {
|
||||
labelMap: Record<string, string>;
|
||||
}
|
||||
|
||||
export class LabelAdapter implements FieldAdapter {
|
||||
extract(
|
||||
contact: GoogleContact,
|
||||
|
|
|
|||
|
|
@ -6,129 +6,91 @@ export class NameAdapter implements FieldAdapter {
|
|||
contact: GoogleContact,
|
||||
context?: Record<string, unknown>
|
||||
): ExtractionResult[] {
|
||||
const results: ExtractionResult[] = [];
|
||||
const isVcfStrategy = context?.namingStrategy === 'VcfNamingStrategy';
|
||||
|
||||
// For Default strategy: only extract displayName
|
||||
if (!isVcfStrategy) {
|
||||
const displayName = contact.names?.[0]?.displayName;
|
||||
|
||||
if (displayName) {
|
||||
contact.names?.forEach((item) => {
|
||||
if (item.displayName) {
|
||||
results.push({ value: item.displayName });
|
||||
}
|
||||
});
|
||||
} else if (contact.organizations?.[0]?.name) {
|
||||
// Fall back to organization name if no displayName
|
||||
results.push({ value: contact.organizations[0].name });
|
||||
}
|
||||
|
||||
return results;
|
||||
if (isVcfStrategy) {
|
||||
return this.extractVcf(contact);
|
||||
}
|
||||
|
||||
// For VCF strategy: extract all structured name fields
|
||||
const names = contact.names || [];
|
||||
return this.extractDefault(contact);
|
||||
}
|
||||
|
||||
private extractDefault(contact: GoogleContact): ExtractionResult[] {
|
||||
const results: ExtractionResult[] = [];
|
||||
const names = contact.names ?? [];
|
||||
|
||||
if (names.length > 0) {
|
||||
names.forEach((item) => {
|
||||
if (item.displayName) {
|
||||
results.push({ value: item.displayName });
|
||||
}
|
||||
});
|
||||
} else if (contact.organizations?.[0]?.name) {
|
||||
results.push({ value: contact.organizations[0].name });
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private extractVcf(contact: GoogleContact): ExtractionResult[] {
|
||||
const results: ExtractionResult[] = [];
|
||||
const names = contact.names ?? [];
|
||||
|
||||
if (names.length === 0) {
|
||||
// Fall back to organization name if no name data
|
||||
if (contact.organizations?.[0]?.name) {
|
||||
results.push({ value: contact.organizations[0].name });
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// Process each name entry (usually just one, but could be multiple)
|
||||
names.forEach((name, nameIndex) => {
|
||||
// Main name fields - all subfields of this name get the same index
|
||||
// Note: displayName is handled by FormattedNameAdapter for FN field
|
||||
|
||||
if (name.givenName) {
|
||||
results.push({ value: name.givenName, suffix: 'GN', index: nameIndex });
|
||||
}
|
||||
|
||||
if (name.middleName) {
|
||||
results.push({
|
||||
value: name.middleName,
|
||||
suffix: 'MN',
|
||||
index: nameIndex,
|
||||
});
|
||||
}
|
||||
|
||||
if (name.familyName) {
|
||||
results.push({
|
||||
value: name.familyName,
|
||||
suffix: 'FN',
|
||||
index: nameIndex,
|
||||
});
|
||||
}
|
||||
|
||||
if (name.honorificPrefix) {
|
||||
results.push({
|
||||
value: name.honorificPrefix,
|
||||
suffix: 'PREFIX',
|
||||
index: nameIndex,
|
||||
});
|
||||
}
|
||||
|
||||
if (name.honorificSuffix) {
|
||||
results.push({
|
||||
value: name.honorificSuffix,
|
||||
suffix: 'SUFFIX',
|
||||
index: nameIndex,
|
||||
});
|
||||
}
|
||||
|
||||
// Phonetic name fields
|
||||
if (name.phoneticFullName) {
|
||||
results.push({
|
||||
value: name.phoneticFullName,
|
||||
suffix: 'PHONETIC_FULL',
|
||||
index: nameIndex,
|
||||
});
|
||||
}
|
||||
|
||||
if (name.phoneticGivenName) {
|
||||
results.push({
|
||||
value: name.phoneticGivenName,
|
||||
suffix: 'PHONETIC_GN',
|
||||
index: nameIndex,
|
||||
});
|
||||
}
|
||||
|
||||
if (name.phoneticMiddleName) {
|
||||
results.push({
|
||||
value: name.phoneticMiddleName,
|
||||
suffix: 'PHONETIC_MN',
|
||||
index: nameIndex,
|
||||
});
|
||||
}
|
||||
|
||||
if (name.phoneticFamilyName) {
|
||||
results.push({
|
||||
value: name.phoneticFamilyName,
|
||||
suffix: 'PHONETIC_FN',
|
||||
index: nameIndex,
|
||||
});
|
||||
}
|
||||
|
||||
if (name.phoneticHonorificPrefix) {
|
||||
results.push({
|
||||
value: name.phoneticHonorificPrefix,
|
||||
suffix: 'PHONETIC_PREFIX',
|
||||
index: nameIndex,
|
||||
});
|
||||
}
|
||||
|
||||
if (name.phoneticHonorificSuffix) {
|
||||
results.push({
|
||||
value: name.phoneticHonorificSuffix,
|
||||
suffix: 'PHONETIC_SUFFIX',
|
||||
index: nameIndex,
|
||||
});
|
||||
}
|
||||
this.addNameFields(results, name, nameIndex);
|
||||
this.addPhoneticFields(results, name, nameIndex);
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private addNameFields(
|
||||
results: ExtractionResult[],
|
||||
name: NonNullable<GoogleContact['names']>[number],
|
||||
index: number
|
||||
): void {
|
||||
const fields: Record<string, string> = {
|
||||
givenName: 'GN',
|
||||
middleName: 'MN',
|
||||
familyName: 'FN',
|
||||
honorificPrefix: 'PREFIX',
|
||||
honorificSuffix: 'SUFFIX',
|
||||
};
|
||||
|
||||
for (const [prop, suffix] of Object.entries(fields)) {
|
||||
const value = (name as Record<string, unknown>)[prop];
|
||||
if (typeof value === 'string') {
|
||||
results.push({ value, suffix, index });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addPhoneticFields(
|
||||
results: ExtractionResult[],
|
||||
name: NonNullable<GoogleContact['names']>[number],
|
||||
index: number
|
||||
): void {
|
||||
const fields: Record<string, string> = {
|
||||
phoneticFullName: 'PHONETIC_FULL',
|
||||
phoneticGivenName: 'PHONETIC_GN',
|
||||
phoneticMiddleName: 'PHONETIC_MN',
|
||||
phoneticFamilyName: 'PHONETIC_FN',
|
||||
phoneticHonorificPrefix: 'PHONETIC_PREFIX',
|
||||
phoneticHonorificSuffix: 'PHONETIC_SUFFIX',
|
||||
};
|
||||
|
||||
for (const [prop, suffix] of Object.entries(fields)) {
|
||||
const value = (name as Record<string, unknown>)[prop];
|
||||
if (typeof value === 'string') {
|
||||
results.push({ value, suffix, index });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
import { FieldAdapter, ExtractionResult } from '../interfaces';
|
||||
import { GoogleContact } from '../../types/Contact';
|
||||
|
||||
export interface OrganizationAdapterContext {
|
||||
organizationAsLink: boolean;
|
||||
}
|
||||
|
||||
export class OrganizationAdapter implements FieldAdapter {
|
||||
extract(
|
||||
contact: GoogleContact,
|
||||
|
|
@ -14,7 +10,7 @@ export class OrganizationAdapter implements FieldAdapter {
|
|||
const isVcfStrategy =
|
||||
context?.namingStrategy === 'VcfNamingStrategy' ||
|
||||
context?.namingStrategy === 'VcfNamingStrategy';
|
||||
return (contact.organizations || [])
|
||||
return (contact.organizations ?? [])
|
||||
.map((org) => org.name)
|
||||
.filter((name) => !!name)
|
||||
.map((name) => ({
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { GoogleContact } from '../../types/Contact';
|
|||
|
||||
export class PhoneAdapter implements FieldAdapter {
|
||||
extract(contact: GoogleContact): ExtractionResult[] {
|
||||
return (contact.phoneNumbers || [])
|
||||
return (contact.phoneNumbers ?? [])
|
||||
.filter((item) => item.value)
|
||||
.map((item) => ({ value: item.value }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export class DefaultNamingStrategy implements KeyNamingStrategy {
|
|||
baseKey: string,
|
||||
index: number,
|
||||
prefix: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
_suffix?: string
|
||||
): string {
|
||||
const keySuffix = index === 0 ? '' : `_${index + 1}`;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export class VcfNamingStrategy implements KeyNamingStrategy {
|
|||
_prefix: string,
|
||||
suffix?: string
|
||||
): string {
|
||||
const vcfKey = this.keyMap[baseKey] || baseKey.toUpperCase();
|
||||
const vcfKey = this.keyMap[baseKey] ?? baseKey.toUpperCase();
|
||||
// VCF format doesn't support prefixes, but needs indexed notation
|
||||
// for multiple values: EMAIL[2], EMAIL[3], etc.
|
||||
// This format is compatible with obsidian-vcf-contacts plugin
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { ru, en, lv } from './translations';
|
|||
export class Translator {
|
||||
private language: string;
|
||||
|
||||
constructor(language: string = 'en') {
|
||||
constructor(language = 'en') {
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ export class Translator {
|
|||
}
|
||||
|
||||
// Singleton-like instance for general use
|
||||
export const translator = new Translator(getLanguage()); // Default to the current Obsidian language
|
||||
const translator = new Translator(getLanguage()); // Default to the current Obsidian language
|
||||
|
||||
/**
|
||||
* Global translation function.
|
||||
|
|
|
|||
91
src/main.ts
91
src/main.ts
|
|
@ -3,10 +3,10 @@ import { ContactSyncSettingTab } from './plugin/settings';
|
|||
import { DEFAULT_SETTINGS } from './config';
|
||||
import { Plugin, Notice } from 'obsidian';
|
||||
import { AuthManager } from './auth/AuthManager';
|
||||
import { GoogleContactsService } from './core/GoogleContactsService';
|
||||
import { ContactNoteWriter } from './core/ContactNoteWriter';
|
||||
import { GoogleContactsService } from './services/GoogleContactsService';
|
||||
import { ContactNoteWriter } from './services/ContactNoteWriter';
|
||||
import { ContactNoteConfig } from './types/ContactNoteConfig';
|
||||
import { ContactAuditService } from './core/ContactAuditService';
|
||||
import { ContactAuditService } from './services/ContactAuditService';
|
||||
import { t } from './i18n/translator';
|
||||
import type { GoogleContact } from './types/Contact';
|
||||
|
||||
|
|
@ -75,21 +75,25 @@ export default class GoogleContactsSyncPlugin extends Plugin {
|
|||
);
|
||||
|
||||
if (this.settings.syncOnStartup || this.shouldSyncNow()) {
|
||||
this.syncContacts();
|
||||
void this.syncContacts();
|
||||
}
|
||||
|
||||
this.setupAutoSync();
|
||||
}
|
||||
|
||||
async onunload() {
|
||||
if (this.syncIntervalId) clearInterval(this.syncIntervalId);
|
||||
onunload() {
|
||||
if (this.syncIntervalId) {
|
||||
clearInterval(this.syncIntervalId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up automatic periodic syncing using setInterval based on plugin settings.
|
||||
*/
|
||||
setupAutoSync() {
|
||||
if (this.syncIntervalId) clearInterval(this.syncIntervalId);
|
||||
if (this.syncIntervalId) {
|
||||
clearInterval(this.syncIntervalId);
|
||||
}
|
||||
|
||||
const interval = this.settings.syncIntervalMinutes;
|
||||
|
||||
|
|
@ -98,7 +102,7 @@ export default class GoogleContactsSyncPlugin extends Plugin {
|
|||
window.setInterval(
|
||||
() => {
|
||||
if (this.shouldSyncNow()) {
|
||||
this.syncContacts();
|
||||
void this.syncContacts();
|
||||
}
|
||||
},
|
||||
interval * 60 * 1000
|
||||
|
|
@ -114,8 +118,12 @@ export default class GoogleContactsSyncPlugin extends Plugin {
|
|||
shouldSyncNow(): boolean {
|
||||
const { lastSyncTime, syncIntervalMinutes } = this.settings;
|
||||
|
||||
if (syncIntervalMinutes === 0) return false;
|
||||
if (!lastSyncTime) return true;
|
||||
if (syncIntervalMinutes === 0) {
|
||||
return false;
|
||||
}
|
||||
if (!lastSyncTime) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const last = new Date(lastSyncTime).getTime();
|
||||
const now = Date.now();
|
||||
|
|
@ -128,18 +136,44 @@ export default class GoogleContactsSyncPlugin extends Plugin {
|
|||
* Performs the contact synchronization process: fetching, processing, and saving contact notes.
|
||||
*/
|
||||
async syncContacts() {
|
||||
this.updateLastSyncTime();
|
||||
void this.updateLastSyncTime();
|
||||
|
||||
const token = await this.getToken();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
const labelMap = await this.getLabelMap(token);
|
||||
if (!labelMap) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contacts = await this.getContacts(token);
|
||||
if (!contacts) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = this.getNoteConfig();
|
||||
await this.noteWriter?.writeNotesForContacts(config, labelMap, contacts);
|
||||
|
||||
new Notice(t('Google contacts synced!'));
|
||||
}
|
||||
|
||||
private async getToken(): Promise<string | null> {
|
||||
const token = await this.auth?.ensureValidToken();
|
||||
if (!token) {
|
||||
new Notice(t('Failed to obtain access token. Please re-authenticate.'));
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
this.updateAuthSettings();
|
||||
void this.updateAuthSettings();
|
||||
return token;
|
||||
}
|
||||
|
||||
let labelMap: Record<string, string> = {};
|
||||
private async getLabelMap(
|
||||
token: string
|
||||
): Promise<Record<string, string> | null> {
|
||||
try {
|
||||
labelMap = (await this.googleService?.fetchGoogleGroups(token)) || {};
|
||||
return (await this.googleService?.fetchGoogleGroups(token)) ?? {};
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to fetch Google groups',
|
||||
|
|
@ -148,12 +182,13 @@ export default class GoogleContactsSyncPlugin extends Plugin {
|
|||
new Notice(
|
||||
t('Failed to fetch Google groups. Check console for details.')
|
||||
);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
let contacts: GoogleContact[] = [];
|
||||
private async getContacts(token: string): Promise<GoogleContact[] | null> {
|
||||
try {
|
||||
contacts = (await this.googleService?.fetchGoogleContacts(token)) || [];
|
||||
return (await this.googleService?.fetchGoogleContacts(token)) ?? [];
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to fetch Google contacts',
|
||||
|
|
@ -162,13 +197,15 @@ export default class GoogleContactsSyncPlugin extends Plugin {
|
|||
new Notice(
|
||||
t('Failed to fetch Google contacts. Check console for details.')
|
||||
);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const config: ContactNoteConfig = {
|
||||
private getNoteConfig(): ContactNoteConfig {
|
||||
return {
|
||||
folderPath: this.settings.contactsFolder,
|
||||
prefix: this.settings.fileNamePrefix || '',
|
||||
propertyPrefix: this.settings.propertyNamePrefix || '',
|
||||
prefix: this.settings.fileNamePrefix,
|
||||
propertyPrefix: this.settings.propertyNamePrefix,
|
||||
syncLabel: this.settings.syncLabel,
|
||||
noteBody: this.settings.noteTemplate || '# Notes\n',
|
||||
organizationAsLink: this.settings.organizationAsLink,
|
||||
|
|
@ -176,10 +213,6 @@ export default class GoogleContactsSyncPlugin extends Plugin {
|
|||
renameFiles: this.settings.renameFiles,
|
||||
namingStrategy: this.settings.namingStrategy,
|
||||
};
|
||||
|
||||
await this.noteWriter?.writeNotesForContacts(config, labelMap, contacts);
|
||||
|
||||
new Notice(t('Google contacts synced!'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -204,7 +237,11 @@ export default class GoogleContactsSyncPlugin extends Plugin {
|
|||
* Loads plugin settings from disk, applying defaults as needed.
|
||||
*/
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
this.settings = Object.assign(
|
||||
{},
|
||||
DEFAULT_SETTINGS,
|
||||
(await this.loadData()) as Partial<ContactSyncSettings>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { Notice, Setting, App, PluginSettingTab } from 'obsidian';
|
||||
import { LINK_TO_MANUAL } from '../config';
|
||||
import { getAuthUrl } from '../auth/getAuthUrl';
|
||||
import GoogleContactsSyncPlugin from '../main';
|
||||
import { IPlugin } from '../types/IPlugin';
|
||||
import { NamingStrategy } from 'src/types/Settings';
|
||||
import { t } from '../i18n/translator';
|
||||
import { FolderSuggest } from 'src/core/FolderSuggest';
|
||||
import { FolderSuggest } from './FolderSuggest';
|
||||
|
||||
/**
|
||||
* Settings tab for the Google contacts sync plugin.
|
||||
|
|
@ -12,14 +12,14 @@ import { FolderSuggest } from 'src/core/FolderSuggest';
|
|||
*/
|
||||
export class ContactSyncSettingTab extends PluginSettingTab {
|
||||
/** Reference to the main plugin instance */
|
||||
plugin: GoogleContactsSyncPlugin;
|
||||
plugin: IPlugin;
|
||||
|
||||
/**
|
||||
* Constructs the settings tab.
|
||||
* @param app The current Obsidian app instance.
|
||||
* @param plugin The instance of the GoogleContactsSyncPlugin.
|
||||
*/
|
||||
constructor(app: App, plugin: GoogleContactsSyncPlugin) {
|
||||
constructor(app: App, plugin: IPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,29 +15,56 @@ export class ContactAuditService {
|
|||
async auditContacts(token: string): Promise<void> {
|
||||
new Notice(t('Starting contact audit...'));
|
||||
|
||||
let labelMap: Record<string, string> = {};
|
||||
const labelMap = await this.getLabelMap(token);
|
||||
if (!labelMap) {
|
||||
return;
|
||||
}
|
||||
|
||||
const googleContacts = await this.getGoogleContacts(token);
|
||||
if (!googleContacts) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validGoogleIds = this.getValidGoogleIds(googleContacts, labelMap);
|
||||
const orphans = this.identifyOrphans(validGoogleIds);
|
||||
|
||||
if (orphans) {
|
||||
await this.generateReport(orphans);
|
||||
}
|
||||
}
|
||||
|
||||
private async getLabelMap(
|
||||
token: string
|
||||
): Promise<Record<string, string> | null> {
|
||||
try {
|
||||
labelMap = (await this.googleService.fetchGoogleGroups(token)) || {};
|
||||
return await this.googleService.fetchGoogleGroups(token);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch Google groups', error);
|
||||
new Notice(
|
||||
t('Failed to fetch Google groups. Check console for details.')
|
||||
);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
let googleContacts: GoogleContact[] = [];
|
||||
private async getGoogleContacts(
|
||||
token: string
|
||||
): Promise<GoogleContact[] | null> {
|
||||
try {
|
||||
googleContacts =
|
||||
(await this.googleService.fetchGoogleContacts(token)) || [];
|
||||
return await this.googleService.fetchGoogleContacts(token);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch Google contacts', error);
|
||||
new Notice(
|
||||
t('Failed to fetch Google contacts. Check console for details.')
|
||||
);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private getValidGoogleIds(
|
||||
googleContacts: GoogleContact[],
|
||||
labelMap: Record<string, string>
|
||||
): Set<string> {
|
||||
const validGoogleIds = new Set<string>();
|
||||
googleContacts.forEach((contact) => {
|
||||
if (this.hasSyncLabel(contact, this.settings.syncLabel, labelMap)) {
|
||||
|
|
@ -47,32 +74,44 @@ export class ContactAuditService {
|
|||
}
|
||||
}
|
||||
});
|
||||
return validGoogleIds;
|
||||
}
|
||||
|
||||
private identifyOrphans(
|
||||
validGoogleIds: Set<string>
|
||||
): { file: TFile; id: string }[] | null {
|
||||
const folderPath = this.settings.contactsFolder;
|
||||
const folder = this.app.vault.getAbstractFileByPath(folderPath);
|
||||
if (!folder || !(folder instanceof TFolder)) {
|
||||
new Notice(`${t('Contacts folder not found')}: ${folderPath}`);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const files = getAllMarkdownFilesInFolder(folder);
|
||||
const orphans: { file: TFile; id: string }[] = [];
|
||||
|
||||
const propertyPrefix = this.settings.propertyNamePrefix || '';
|
||||
const idField = `${propertyPrefix}id`;
|
||||
const idField = `${this.settings.propertyNamePrefix}id`;
|
||||
|
||||
for (const file of files) {
|
||||
const cache = this.app.metadataCache.getFileCache(file);
|
||||
const id = cache?.frontmatter?.[idField];
|
||||
|
||||
if (id) {
|
||||
if (!validGoogleIds.has(String(id))) {
|
||||
orphans.push({ file, id: String(id) });
|
||||
}
|
||||
const id = this.getContactIdFromFile(file, idField);
|
||||
if (id && !validGoogleIds.has(id)) {
|
||||
orphans.push({ file, id });
|
||||
}
|
||||
}
|
||||
|
||||
await this.generateReport(orphans);
|
||||
return orphans;
|
||||
}
|
||||
|
||||
private getContactIdFromFile(file: TFile, idField: string): string | null {
|
||||
const cache = this.app.metadataCache.getFileCache(file);
|
||||
const frontmatter = cache?.frontmatter as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
const id = frontmatter?.[idField];
|
||||
|
||||
if (typeof id === 'string' || typeof id === 'number') {
|
||||
return String(id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async generateReport(
|
||||
|
|
@ -113,7 +152,7 @@ export class ContactAuditService {
|
|||
new Notice(`${t('Audit complete. Report saved to')} ${reportPath}`);
|
||||
const reportFile = this.app.vault.getAbstractFileByPath(reportPath);
|
||||
if (reportFile instanceof TFile) {
|
||||
this.app.workspace.getLeaf(true).openFile(reportFile);
|
||||
void this.app.workspace.getLeaf(true).openFile(reportFile);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -122,18 +161,24 @@ export class ContactAuditService {
|
|||
syncLabel: string,
|
||||
labelMap: Record<string, string>
|
||||
): boolean {
|
||||
if (!syncLabel) return true;
|
||||
if (!syncLabel) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const targetGroupId = labelMap[syncLabel];
|
||||
if (!targetGroupId) return false;
|
||||
if (!targetGroupId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (contact.memberships || []).some(
|
||||
return (contact.memberships ?? []).some(
|
||||
(m) => m.contactGroupMembership?.contactGroupId === targetGroupId
|
||||
);
|
||||
}
|
||||
|
||||
private getContactId(contact: GoogleContact): string | null {
|
||||
if (!contact.resourceName) return null;
|
||||
return contact.resourceName.split('/').pop() || null;
|
||||
if (!contact.resourceName) {
|
||||
return null;
|
||||
}
|
||||
return contact.resourceName.split('/').pop() ?? null;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,8 @@ import {
|
|||
} from 'obsidian';
|
||||
import { GoogleContact } from 'src/types/Contact';
|
||||
import { getAllMarkdownFilesInFolder } from 'src/utils/getAllMarkdownFilesInFolder';
|
||||
import { createDefaultFormatter } from './Formatter';
|
||||
import { VaultService } from 'src/services/VaultService';
|
||||
import { createDefaultFormatter } from '../core/Formatter';
|
||||
import { VaultService } from './VaultService';
|
||||
import { ContactNoteConfig } from 'src/types/ContactNoteConfig';
|
||||
import { NamingStrategy } from 'src/types/Settings';
|
||||
|
||||
|
|
@ -70,56 +70,118 @@ export class ContactNoteWriter {
|
|||
await this.vaultService.createFolderIfNotExists(config.folderPath);
|
||||
|
||||
const folder = this.vaultService.getFolderByPath(config.folderPath);
|
||||
if (!folder) return;
|
||||
if (!folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = getAllMarkdownFilesInFolder(folder);
|
||||
const filesIdMapping = await this.scanFiles(files, config.propertyPrefix);
|
||||
const invertedLabelMap: Record<string, string> = Object.fromEntries(
|
||||
Object.entries(labelMap).map((a) => a.reverse())
|
||||
);
|
||||
const filesIdMapping = this.scanFiles(files, config.propertyPrefix);
|
||||
const invertedLabelMap = this.getInvertedLabelMap(labelMap);
|
||||
|
||||
for (const contact of contacts) {
|
||||
if (!this.hasSyncLabel(contact, config.syncLabel, labelMap)) continue;
|
||||
|
||||
const id = this.getContactId(contact);
|
||||
if (!id) continue;
|
||||
|
||||
const filename = this.getFilename(
|
||||
await this.processContact(
|
||||
contact,
|
||||
id,
|
||||
config.folderPath,
|
||||
config.prefix
|
||||
);
|
||||
if (!filename) continue;
|
||||
|
||||
if (config.renameFiles && filesIdMapping[id]) {
|
||||
const existingFile = filesIdMapping[id];
|
||||
if (existingFile.path !== filename) {
|
||||
await this.vaultService.renameFile(existingFile, filename);
|
||||
delete filesIdMapping[id];
|
||||
filesIdMapping[id] = (await this.vaultService.getFileByPath(
|
||||
filename
|
||||
)) as TFile;
|
||||
}
|
||||
}
|
||||
|
||||
await this.fileManager.processFrontMatter(
|
||||
filesIdMapping[id] ||
|
||||
(await this.vaultService.getFileByPath(filename)) ||
|
||||
(await this.vaultService.createFile(filename, config.noteBody)),
|
||||
this.processFrontMatter(
|
||||
this.generateFrontmatterLines(
|
||||
config.propertyPrefix,
|
||||
contact,
|
||||
invertedLabelMap,
|
||||
config.namingStrategy,
|
||||
config.organizationAsLink,
|
||||
config.trackSyncTime
|
||||
)
|
||||
)
|
||||
config,
|
||||
labelMap,
|
||||
invertedLabelMap,
|
||||
filesIdMapping
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private getInvertedLabelMap(
|
||||
labelMap: Record<string, string>
|
||||
): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(labelMap).map(([key, value]) => [value, key])
|
||||
) as Record<string, string>;
|
||||
}
|
||||
|
||||
private async processContact(
|
||||
contact: GoogleContact,
|
||||
config: ContactNoteConfig,
|
||||
labelMap: Record<string, string>,
|
||||
invertedLabelMap: Record<string, string>,
|
||||
filesIdMapping: Record<string, TFile>
|
||||
): Promise<void> {
|
||||
if (!this.hasSyncLabel(contact, config.syncLabel, labelMap)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = this.getContactId(contact);
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let filename = this.getFilename(
|
||||
contact,
|
||||
id,
|
||||
config.folderPath,
|
||||
config.prefix
|
||||
);
|
||||
if (!filename) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.renameFiles && filesIdMapping[id]) {
|
||||
filename = await this.ensureRenamed(id, filename, filesIdMapping);
|
||||
}
|
||||
|
||||
const file = await this.getOrCreateFile(
|
||||
id,
|
||||
filename,
|
||||
filesIdMapping,
|
||||
config.noteBody
|
||||
);
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
await this.fileManager.processFrontMatter(
|
||||
file,
|
||||
this.processFrontMatter(
|
||||
this.generateFrontmatterLines(
|
||||
config.propertyPrefix,
|
||||
contact,
|
||||
invertedLabelMap,
|
||||
config.namingStrategy,
|
||||
config.organizationAsLink,
|
||||
config.trackSyncTime
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async ensureRenamed(
|
||||
id: string,
|
||||
filename: string,
|
||||
filesIdMapping: Record<string, TFile>
|
||||
): Promise<string> {
|
||||
const existingFile = filesIdMapping[id];
|
||||
if (existingFile && existingFile.path !== filename) {
|
||||
await this.vaultService.renameFile(existingFile, filename);
|
||||
const updatedFile = this.vaultService.getFileByPath(filename);
|
||||
if (updatedFile) {
|
||||
filesIdMapping[id] = updatedFile;
|
||||
return filename;
|
||||
}
|
||||
}
|
||||
return existingFile?.path ?? filename;
|
||||
}
|
||||
|
||||
private async getOrCreateFile(
|
||||
id: string,
|
||||
filename: string,
|
||||
filesIdMapping: Record<string, TFile>,
|
||||
noteBody: string
|
||||
): Promise<TFile | null> {
|
||||
const existingFile =
|
||||
filesIdMapping[id] ?? this.vaultService.getFileByPath(filename);
|
||||
if (existingFile) {
|
||||
return existingFile;
|
||||
}
|
||||
return await this.vaultService.createFile(filename, noteBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a filename for the contact note based on the contact's display name and ID.
|
||||
* Falls back to organization name if no display name is available.
|
||||
|
|
@ -138,8 +200,10 @@ export class ContactNoteWriter {
|
|||
): string | null {
|
||||
// Try displayName first, then organization name, then fall back to ID
|
||||
const name =
|
||||
contact.names?.[0]?.displayName || contact.organizations?.[0]?.name || id;
|
||||
if (!name) return null;
|
||||
contact.names?.[0]?.displayName ?? contact.organizations?.[0]?.name ?? id;
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
const safeName = name.replace(/[\\/:*?"<>|]/g, '_');
|
||||
const filename = normalizePath(`${folderPath}/${prefix}${safeName}.md`);
|
||||
return filename;
|
||||
|
|
@ -168,8 +232,10 @@ export class ContactNoteWriter {
|
|||
* @returns The extracted contact ID, or null if not found.
|
||||
*/
|
||||
private getContactId(contact: GoogleContact): string | null {
|
||||
if (!contact.resourceName) return null;
|
||||
return contact.resourceName.split('/').pop() || null;
|
||||
if (!contact.resourceName) {
|
||||
return null;
|
||||
}
|
||||
return contact.resourceName.split('/').pop() ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -186,8 +252,8 @@ export class ContactNoteWriter {
|
|||
contact: GoogleContact,
|
||||
invertedLabelMap: Record<string, string>,
|
||||
namingStrategy: NamingStrategy,
|
||||
organizationAsLink: boolean = false,
|
||||
trackSyncTime: boolean = false
|
||||
organizationAsLink = false,
|
||||
trackSyncTime = false
|
||||
): Record<string, string | string[]> {
|
||||
const formatter = createDefaultFormatter(namingStrategy);
|
||||
const formattedFields = formatter.generateFrontmatter(
|
||||
|
|
@ -205,9 +271,7 @@ export class ContactNoteWriter {
|
|||
};
|
||||
|
||||
if (trackSyncTime && namingStrategy !== NamingStrategy.VCF) {
|
||||
frontmatterLines[`${propertyPrefix}synced`] = String(
|
||||
new Date().toISOString()
|
||||
);
|
||||
frontmatterLines[`${propertyPrefix}synced`] = new Date().toISOString();
|
||||
}
|
||||
|
||||
return frontmatterLines;
|
||||
|
|
@ -226,10 +290,12 @@ export class ContactNoteWriter {
|
|||
syncLabel: string,
|
||||
labelMap: Record<string, string>
|
||||
): boolean {
|
||||
if (!syncLabel) return true;
|
||||
if (!syncLabel) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const targetGroupId = labelMap[syncLabel];
|
||||
return (contact.memberships || []).some(
|
||||
return (contact.memberships ?? []).some(
|
||||
(m) => m.contactGroupMembership?.contactGroupId === targetGroupId
|
||||
);
|
||||
}
|
||||
|
|
@ -241,17 +307,20 @@ export class ContactNoteWriter {
|
|||
* @param propertyPrefix - The prefix to use for identifying frontmatter properties.
|
||||
* @returns A mapping of property values to TFile objects.
|
||||
*/
|
||||
protected async scanFiles(
|
||||
protected scanFiles(
|
||||
files: TFile[],
|
||||
propertyPrefix: string
|
||||
): Promise<Record<string, TFile>> {
|
||||
): Record<string, TFile> {
|
||||
const idToFileMapping: Record<string, TFile> = {};
|
||||
|
||||
for (const file of files) {
|
||||
const frontmatter = this.metadataCache.getFileCache(file)?.frontmatter;
|
||||
const frontmatter = this.metadataCache.getFileCache(file)?.frontmatter as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
const idFieldName = `${propertyPrefix}id`;
|
||||
if (frontmatter && frontmatter[idFieldName]) {
|
||||
idToFileMapping[frontmatter[idFieldName]] = file;
|
||||
const idValue = frontmatter?.[idFieldName];
|
||||
if (typeof idValue === 'string' || typeof idValue === 'number') {
|
||||
idToFileMapping[String(idValue)] = file;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5,6 +5,16 @@ import {
|
|||
PERSONAL_FIELDS,
|
||||
} from '../config';
|
||||
import type { GoogleContact, GoogleContactGroup } from '../types/Contact';
|
||||
import { RequestUrlResponse } from 'obsidian';
|
||||
|
||||
interface PeopleConnectionsResponse {
|
||||
connections?: GoogleContact[] | undefined;
|
||||
nextPageToken?: string | undefined;
|
||||
}
|
||||
|
||||
interface ContactGroupsResponse {
|
||||
contactGroups?: GoogleContactGroup[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Core service responsible for interacting with Google contacts and Contact Groups APIs.
|
||||
|
|
@ -24,10 +34,7 @@ export class GoogleContactsService {
|
|||
async fetchGoogleContacts(token: string): Promise<GoogleContact[]> {
|
||||
let allContacts: GoogleContact[] = [];
|
||||
let nextPageToken: string | undefined = undefined;
|
||||
let data: {
|
||||
connections: GoogleContact[];
|
||||
nextPageToken?: string | undefined;
|
||||
} = {
|
||||
let data: PeopleConnectionsResponse = {
|
||||
connections: [],
|
||||
nextPageToken: undefined,
|
||||
};
|
||||
|
|
@ -36,14 +43,14 @@ export class GoogleContactsService {
|
|||
const url = `${URL_PEOPLE_CONNECTIONS}?personFields=${PERSONAL_FIELDS}&pageSize=1000${nextPageToken ? `&pageToken=${nextPageToken}` : ''}`;
|
||||
|
||||
try {
|
||||
const res = await requestUrl({
|
||||
const res: RequestUrlResponse = await requestUrl({
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
data = await res.json;
|
||||
data = (await res.json) as PeopleConnectionsResponse;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to fetch Google contacts',
|
||||
|
|
@ -54,7 +61,7 @@ export class GoogleContactsService {
|
|||
nextPageToken: undefined,
|
||||
};
|
||||
}
|
||||
allContacts = allContacts.concat(data.connections || []);
|
||||
allContacts = allContacts.concat(data.connections ?? []);
|
||||
nextPageToken = data.nextPageToken;
|
||||
} while (nextPageToken);
|
||||
|
||||
|
|
@ -75,7 +82,7 @@ export class GoogleContactsService {
|
|||
},
|
||||
});
|
||||
|
||||
const data = await groupResponse.json;
|
||||
const data = (await groupResponse.json) as ContactGroupsResponse;
|
||||
if (!Array.isArray(data.contactGroups)) {
|
||||
return {};
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import { TFile, CachedMetadata, MetadataCache } from 'obsidian';
|
||||
|
||||
/**
|
||||
* Service class for managing metadata in Obsidian files.
|
||||
* Encapsulates metadata operations to facilitate testing and decouple from direct API usage.
|
||||
*/
|
||||
export class MetadataService {
|
||||
private metadataCache: MetadataCache;
|
||||
|
||||
/**
|
||||
* Creates an instance of MetadataService.
|
||||
*
|
||||
* @param metadataCache - The Obsidian MetadataCache instance to operate on.
|
||||
*/
|
||||
constructor(metadataCache: MetadataCache) {
|
||||
this.metadataCache = metadataCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the frontmatter metadata from a file.
|
||||
*
|
||||
* @param file - The TFile to retrieve frontmatter from.
|
||||
* @returns The frontmatter as a record of key-value pairs, or undefined if not found.
|
||||
*/
|
||||
getFrontmatter(file: TFile): Record<string, string | string[]> | undefined {
|
||||
const cache = this.metadataCache.getFileCache(file);
|
||||
return cache?.frontmatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the YAML frontmatter from a file.
|
||||
*
|
||||
* @param file - The TFile to retrieve YAML frontmatter from.
|
||||
* @returns The YAML frontmatter as a string, or null if not found.
|
||||
*/
|
||||
getFileCache(file: TFile): CachedMetadata | null {
|
||||
return this.metadataCache.getFileCache(file);
|
||||
}
|
||||
}
|
||||
|
|
@ -34,7 +34,11 @@ export class VaultService {
|
|||
*/
|
||||
async createFolderIfNotExists(folderPath: string): Promise<void> {
|
||||
const normalizedPath = normalizePath(folderPath);
|
||||
await this.vault.createFolder(normalizedPath).catch(() => {});
|
||||
try {
|
||||
await this.vault.createFolder(normalizedPath);
|
||||
} catch {
|
||||
// Folder already exists or other error we can ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -43,7 +47,7 @@ export class VaultService {
|
|||
* @param filePath - The path of the file to retrieve.
|
||||
* @returns The TFile if found, or null.
|
||||
*/
|
||||
async getFileByPath(filePath: string): Promise<TFile | null> {
|
||||
getFileByPath(filePath: string): TFile | null {
|
||||
return this.vault.getFileByPath(filePath);
|
||||
}
|
||||
|
||||
|
|
@ -71,6 +75,6 @@ export class VaultService {
|
|||
|
||||
async renameFile(file: TFile, newPath: string): Promise<void> {
|
||||
const normalizedPath = normalizePath(newPath);
|
||||
return await this.fileManager.renameFile(file, normalizedPath);
|
||||
await this.fileManager.renameFile(file, normalizedPath);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export interface GoogleContact {
|
|||
/**
|
||||
* Represents a contact's birthday, including optional structured date and metadata.
|
||||
*/
|
||||
export interface Birthday {
|
||||
interface Birthday {
|
||||
/** Structured date object (year, month, day) */
|
||||
date?: {
|
||||
year?: number;
|
||||
|
|
@ -79,7 +79,7 @@ export interface Birthday {
|
|||
/**
|
||||
* Represents a membership in a Google contact Group.
|
||||
*/
|
||||
export interface ContactGroupMembership {
|
||||
interface ContactGroupMembership {
|
||||
/** Identifier of the contact group (e.g., "abc123") */
|
||||
contactGroupId: string;
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ export interface ContactGroupMembership {
|
|||
/**
|
||||
* Represents a contact's membership in various domains or groups.
|
||||
*/
|
||||
export interface Membership {
|
||||
interface Membership {
|
||||
/** Membership in a specific contact group */
|
||||
contactGroupMembership?: ContactGroupMembership;
|
||||
|
||||
|
|
|
|||
10
src/types/IPlugin.ts
Normal file
10
src/types/IPlugin.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Plugin } from 'obsidian';
|
||||
import { ContactSyncSettings } from './Settings';
|
||||
import { AuthManager } from '../auth/AuthManager';
|
||||
|
||||
export interface IPlugin extends Plugin {
|
||||
settings: ContactSyncSettings;
|
||||
auth: AuthManager | null;
|
||||
saveSettings(): Promise<void>;
|
||||
setupAutoSync(): void;
|
||||
}
|
||||
Loading…
Reference in a new issue