From 6aad759a4cb2bdbfb5960be036a7b6716bf9d425 Mon Sep 17 00:00:00 2001 From: Vitaliy Makeev Date: Sat, 17 Jul 2021 19:14:57 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B0=D0=B4=D0=B0=D0=BF=D1=82=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F=20=D0=BC=D0=BE=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - адаптация хелперов для использования совместо с моделью МойСклад - расширенная типизация BREAKING CHANGE --- package.json | 15 +-- src/getHelpers.ts | 155 ------------------------------ src/helpers/getHelpers.ts | 138 +++++++++++++++++++++++++++ src/helpers/getRefMetaType.ts | 75 +++++++++++++++ src/index.ts | 3 +- src/types.ts | 78 +++++++++++++-- tests/index.test.ts | 174 ++++++++++++++++++++++++++-------- tests/tools.ts | 5 + tests/types/ref.test.ts | 27 ++++++ tests/types/types.test.ts | 36 +++++++ 10 files changed, 500 insertions(+), 206 deletions(-) delete mode 100644 src/getHelpers.ts create mode 100644 src/helpers/getHelpers.ts create mode 100644 src/helpers/getRefMetaType.ts create mode 100644 tests/tools.ts create mode 100644 tests/types/ref.test.ts create mode 100644 tests/types/types.test.ts diff --git a/package.json b/package.json index 18c1881..4ac5f3b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "moysklad-helpers", - "version": "2.0.4", + "version": "3.0.0-beta.1", "description": "Вспомогательные функции для работы с библиотекой moysklad", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -10,7 +10,9 @@ "scripts": { "format": "prettier --write \"src/**/*.ts\"", "build": "npm run format && rm -rf dist/* && tsc --build tsconfig.deploy.json", - "test": "node dist/tests/index.test.js | tap-spec" + "test": "npm run build && node dist/tests/index.test.js | tap-spec", + "git:tag": "git tag \"v$(cat package.json | json version)\"", + "npm:publish": "npm run test && npm publish && npm run git:tag" }, "repository": { "type": "git", @@ -29,10 +31,11 @@ }, "homepage": "https://github.com/wmakeev/moysklad-helpers#readme", "devDependencies": { - "@types/tape": "^4.13.0", - "moysklad": "0.9.2", - "prettier": "^2.2.1", + "@types/tape": "^4.13.1", + "moysklad": "^0.10.0", + "moysklad-api-model": "^0.2.0", + "prettier": "^2.3.2", "tape": "^5.2.2", - "typescript": "^4.2.3" + "typescript": "^4.3.5" } } diff --git a/src/getHelpers.ts b/src/getHelpers.ts deleted file mode 100644 index 79fe028..0000000 --- a/src/getHelpers.ts +++ /dev/null @@ -1,155 +0,0 @@ -import type { Instance } from 'moysklad' - -import { EntityRef, EntityRefOrPath, Path } from './types' - -function isEntityRef(val: any): val is EntityRef { - return val?.meta?.href != null -} - -function getShortHrefMetaType(shortHref: string) { - switch (true) { - case shortHref.indexOf('entity/customentity') === 0: - return 'customentity' - - case shortHref.indexOf('metadata/attributes') !== -1: - return 'attributemetadata' - - case shortHref.indexOf('metadata/states') !== -1: - return 'state' - - case shortHref.indexOf( - 'context/companysettings/metadata/customEntities' - ) === 0: - return 'customentitymetadata' - - case shortHref.indexOf('context/companysettings/pricetype') === 0: - return 'pricetype' - - default: - const parts = shortHref.split('/') - if (parts[0] === 'entity' && parts[1]) { - if (parts[3] === 'positions') { - return `${parts[1]}position` - } else { - return parts[1] - } - } - throw new Error('Неизвестный тип сокращенного href - ' + shortHref) - } -} - -// TODO Как правильно ограничить M чтобы не было примеси (напр. `EntityRef & object`), когда M value не указан -type RefType = ( - path: T, - value?: M -) => T extends EntityRefOrPath - ? EntityRef & M - : T extends EntityRefOrPath | undefined - ? (EntityRef & M) | undefined - : never - -export function getHelpers(ms: Instance) { - /** - * Возвращает href для некого пути - * - * ```ts - * href('') - * ``` - */ - const href = (path: Path | EntityRef): string => { - if (isEntityRef(path)) { - return ms.buildUrl(path.meta.href) - } else { - return ms.buildUrl(path) - } - } - - // TODO Подумать, возможно оптимизировать, чтобы не вызывать href несколько раз - const getStrPath = (path: Path | EntityRef) => - ms.parseUrl(href(path)).path.join('/') - - const meta = (path: Path | EntityRef) => ({ - type: getShortHrefMetaType(getStrPath(path)), - href: href(path) - }) - - const attr = (path: Path, value: T) => { - if (getShortHrefMetaType(getStrPath(path)) !== 'attributemetadata') { - throw new Error('attr: Href не соответствует типу атрибута') - } - - return { - meta: { - type: 'attributemetadata', - href: href(path) - }, - value - } - } - - const ref: RefType = (path, value) => { - return path != null - ? Object.assign({}, value as any, { meta: meta(path!) }) // ? { ...value, meta: meta(path!) } - : undefined - } - - const positionRef = ( - docPath: Path | EntityRef, - posId: string, - value?: T - ) => { - return ref(href(docPath) + `/positions/${posId}`, value) - } - - const refEqual = ( - entityRef1: Path | EntityRef | null | undefined, - entityRef2: Path | EntityRef | null | undefined - ) => { - return entityRef1 == null || entityRef2 == null - ? false - : href(entityRef1) === href(entityRef2) - } - - const copyEntRefs = ( - srcEntity: T, - fieldNames: K[] - ) => { - return fieldNames.reduce((res, fieldName) => { - const curFieldVal = srcEntity[fieldName] - - if (isEntityRef(curFieldVal)) { - res[fieldName] = { - meta: curFieldVal.meta - } as EntityRef - } - - return res - }, {} as { [P in K]: EntityRef }) - } - - const copyFields = ( - srcEntity: T, - fieldNames: Array - ) => { - return fieldNames.reduce((res, fieldName) => { - const curFieldVal = srcEntity[fieldName] - - if (curFieldVal != null) { - res[fieldName] = curFieldVal - } - - return res - }, {} as { [P in K]: T[P] }) - } - - return { - href, - attr, - meta, - ref, - positionRef, - refEqual, - copyEntRefs, - copyFields - } -} diff --git a/src/helpers/getHelpers.ts b/src/helpers/getHelpers.ts new file mode 100644 index 0000000..961a800 --- /dev/null +++ b/src/helpers/getHelpers.ts @@ -0,0 +1,138 @@ +import type { Instance } from 'moysklad' +import { + DocumentPositionType, + DocumentWithPositionsMetaType, + MetaType +} from 'moysklad-api-model' +import { EntityRef, isEntityRef, HrefMetaType, Meta } from '../types' +import { getRefMetaType } from './getRefMetaType' + +export function getHelpers(ms: Instance) { + /** + * Возвращает href для некого пути + */ + function href(ref: T): string + + function href(entityRef: EntityRef): string + + function href(path: any) { + if (isEntityRef(path)) { + return ms.buildUrl(path.meta.href) + } else { + return ms.buildUrl(path) + } + } + + function meta(path: T): Meta> + + function meta(entityRef?: EntityRef): Meta + + function meta(path: any) { + return { + type: getRefMetaType(path), + href: href(path) + } + } + + const attr = (path: string, value: T) => { + if (getRefMetaType(path) !== 'attributemetadata') { + throw new Error('attr: Href не соответствует типу атрибута') + } + + return { + meta: { + type: 'attributemetadata', + href: href(path) + }, + value + } + } + + function ref(path: T): EntityRef> + function ref( + path: T | undefined + ): EntityRef> | undefined + + function ref(entityRef?: EntityRef): EntityRef + function ref( + entityRef?: EntityRef | undefined + ): EntityRef | undefined + + function ref(path: any) { + return path != null ? { meta: meta(path) } : undefined + } + + function positionRef( + documentRef: T, + positionId: string + ): HrefMetaType extends DocumentWithPositionsMetaType + ? EntityRef]> + : never + + function positionRef( + documentRef: EntityRef, + positionId: string + ): EntityRef + + function positionRef(...args: any[]) { + const docHref = href(args[0]) + return ref(`${docHref}/positions/${args[1]}`) + } + + const refEqual = ( + entityRef1: string | EntityRef | null | undefined, + entityRef2: string | EntityRef | null | undefined + ): boolean => { + return entityRef1 == null || entityRef2 == null + ? false + : href(entityRef1 as any) === href(entityRef2 as any) + } + + function copyFieldsRefs( + srcEntity: T, + fieldNames: K[] + ) { + return fieldNames.reduce( + (res, fieldName) => { + const curFieldVal = srcEntity[fieldName] + + if (isEntityRef(curFieldVal)) { + res[fieldName] = ref(curFieldVal as EntityRef) as any + } else { + res[fieldName] = curFieldVal as any + } + + return res + }, + {} as { + [P in K]: T[P] extends EntityRef ? EntityRef : T[P] + } + ) + } + + function copyFields( + srcEntity: T, + fieldNames: Array + ) { + return fieldNames.reduce((res, fieldName) => { + const curFieldVal = srcEntity[fieldName] + + if (curFieldVal != null) { + res[fieldName] = curFieldVal + } + + return res + }, {} as { [P in K]: T[P] }) + } + + return { + href, + attr, + meta, + ref, + positionRef, + refEqual, + copyFields, + copyFieldsRefs + } +} diff --git a/src/helpers/getRefMetaType.ts b/src/helpers/getRefMetaType.ts new file mode 100644 index 0000000..5c26ee5 --- /dev/null +++ b/src/helpers/getRefMetaType.ts @@ -0,0 +1,75 @@ +import { EntityRef, HrefMetaType } from '../types' + +/** + * Возвращает MetaType для указанной ссылки + * + * @param href Cсылка или href + * @returns MetaType для указанной ссылки + */ +export function getRefMetaType( + href: T +): HrefMetaType + +/** + * Возвращает MetaType для указанной ссылки + * + * @param ref Сокращенная ссылка + * @returns MetaType для указанной ссылки + */ +export function getRefMetaType(ref: T): HrefMetaType + +/** + * Возвращает MetaType + * + * @param entityRef Объект ссылка + * @returns MetaType + */ +export function getRefMetaType( + entityRef: EntityRef +): HrefMetaType + +export function getRefMetaType(anyRef: any) { + if (anyRef?.meta?.href) { + return getRefMetaType(anyRef.meta.href) + } + + if (anyRef.substr(0, 8) === 'https://') { + const parts = anyRef.substr(8).split('/') + if (parts[1] === 'api' && parts[2] === 'remap') { + return getRefMetaType(parts.slice(4).join('/')) + } else { + throw new Error(`Некорректный href - ${anyRef}`) + } + } + + // TODO Возможно тут нужно сравнивать не под подстроке а по массиву (path) + switch (true) { + case anyRef.indexOf('entity/customentity') === 0: + return 'customentity' + + case anyRef.indexOf('metadata/attributes') !== -1: + return 'attributemetadata' + + case anyRef.indexOf('metadata/states') !== -1: + return 'state' + + case anyRef.indexOf('context/companysettings/metadata/customEntities') === + 0: + return 'customentitymetadata' + + case anyRef.indexOf('context/companysettings/pricetype') === 0: + return 'pricetype' + + default: + const parts = anyRef.split('/') + if (parts[0] === 'entity' && parts[1]) { + if (parts[3] === 'positions') { + return `${parts[1]}position` + } else { + return parts[1] + } + } + + throw new Error('Неизвестный тип сокращенного href - ' + anyRef) + } +} diff --git a/src/index.ts b/src/index.ts index d6e3e1e..5d663d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ -export * from './getHelpers' +export * from './helpers/getHelpers' +export * from './helpers/getRefMetaType' export * from './types' diff --git a/src/types.ts b/src/types.ts index ee7be1a..a2d2316 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,12 +1,23 @@ -export type Path = string | string[] +import { + DocumentPositionType, + DocumentWithPositionsMetaType, + DomineEntityMetaType +} from 'moysklad-api-model' -export interface EntityRef { - meta: { - href: string - } +export interface Meta { + type: T + href: string } -export interface Entity extends EntityRef { +export interface EntityRef { + meta: Meta +} + +export function isEntityRef(val: any): val is EntityRef { + return val?.meta?.href != null +} + +export interface Entity extends EntityRef { id: string } @@ -18,4 +29,57 @@ export interface EntityWithAttributes extends Entity { attributes: EntityAttribute[] } -export type EntityRefOrPath = Path | EntityRef +// prettier-ignore + +export type HrefMetaType = + // 1. + Ref extends `https://${string}/api/remap/${string}/${infer Rest}` + ? HrefMetaType + + // 2. entity/../../../.. + : Ref extends `entity/${string}/${string}/${string}/${string}` + // metadata/.. + ? Ref extends `entity/${string}/metadata/${string}/${string}` + // metadata/attributes + ? Ref extends `entity/${string}/metadata/attributes/${string}` + ? 'attributemetadata' + + // metadata/state + : Ref extends `entity/${string}/metadata/states/${string}` + ? 'state' + + // metadata/? + : never + + // ../positions/.. + : Ref extends `entity/${string}/${string}/positions/${string}` + ? Ref extends `entity/${infer M}/${string}/positions/${string}` + ? M extends DocumentWithPositionsMetaType + ? DocumentPositionType[M] + : never + : never + + // entity/?/?/?/? + : never + + // 3. entity/../../.. + : Ref extends `entity/${string}/${string}/${string}` + ? never + + // 4. entity/../.. + : Ref extends `entity/${string}/${string}` + ? Ref extends `entity/${infer M}/${string}` + ? M extends DomineEntityMetaType + ? M + : never + : never + + // 5. + : Ref extends `context/companysettings/metadata/customEntities/${string}` + ? 'customentitymetadata' + + // 6. + : Ref extends `context/companysettings/pricetype/${string}` + ? 'pricetype' + + : never diff --git a/tests/index.test.ts b/tests/index.test.ts index 9cce521..57f46d5 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,7 +1,10 @@ import test from 'tape' import Moysklad from 'moysklad' -import { getHelpers } from '../src' +import { EntityRef, getHelpers } from '../src' +import { noop } from './tools' + +const ENDPOINT = 'https://online.moysklad.ru/api/remap/1.2' test('href', t => { const ms = Moysklad({ apiVersion: '1.2' }) @@ -10,18 +13,18 @@ test('href', t => { const ref1 = 'entity/customerorder/metadata/attributes/39f9f7bc-d4da-11e4-95df-0cc47a05161a' - const HREF1 = ms.buildUrl(ref1) + const href1 = ms.buildUrl(ref1) as `${typeof ENDPOINT}/${typeof ref1}` - const entityRef1 = { + const entityRef1: EntityRef<'attributemetadata'> = { meta: { type: 'attributemetadata', - href: HREF1 + href: href1 } } - t.equal(href(HREF1), HREF1, 'should return href for href') - t.equal(href(ref1), HREF1, 'should return href for ref') - t.equal(href(entityRef1), HREF1, 'should return href for entityRef') + t.equal(href(href1), href1, 'should return href for href') + t.equal(href(ref1), href1, 'should return href for ref') + t.equal(href(entityRef1), href1, 'should return href for entityRef') t.equal( href('https://online.moysklad.ru/api/remap/1.1/entity/customerorder'), @@ -165,24 +168,47 @@ test('ref', t => { const ref1 = 'entity/customerorder/metadata/attributes/39f9f7bc-d4da-11e4-95df-0cc47a05161a' - const HREF1 = ms.buildUrl(ref1) + const HREF1 = ms.buildUrl(ref1) as `${typeof ENDPOINT}/${typeof ref1}` - const entityRef1 = { + const entityRef1: EntityRef<'attributemetadata'> = { meta: { type: 'attributemetadata', href: HREF1 } } - const result1 = ref(HREF1) - t.deepEqual(result1.meta, entityRef1.meta, 'should return entityRef') + const result11: EntityRef<'attributemetadata'> = ref(HREF1) + + t.deepEqual(result11.meta, entityRef1.meta, 'should return entityRef (11)') + + const result12: EntityRef = ref(ref1) + + t.deepEqual(result12.meta, entityRef1.meta, 'should return entityRef (12)') + + const result2: EntityRef & { + foo: string + } = { + ...ref(HREF1), + foo: 'bar' + } + + t.deepEqual(result2.meta, entityRef1.meta, 'should return entityRef (2)') + t.strictEqual(result2.foo, 'bar', 'should return extended entityRef (2)') - const result2 = ref(HREF1, { foo: 'bar' }) - t.deepEqual(result2.foo, 'bar', 'should return extended entityRef') + // @ts-expect-error + const result3: { + meta: { + type: 'attributemetadata3' + href: string + } + } = ref(entityRef1) + noop(result3) - const result3 = ref(undefined, { foo: 'bar' }) + t.deepEqual(result2.meta, entityRef1.meta, 'should return entityRef (2)') + + const result9 = ref(undefined) t.deepEqual( - result3, + result9, undefined, 'should return undefined for undefined path arg' ) @@ -192,22 +218,28 @@ test('ref', t => { test('positionRef', t => { const ms = Moysklad({ apiVersion: '1.2' }) + const { ref, href, positionRef } = getHelpers(ms) const posId = '39f9f7bc-d4da-11e4-95df-0cc47a051618' const ref1 = 'entity/customerorder/39f9f7bc-d4da-11e4-95df-0cc47a05161a' - const posHref1 = href([ref1, 'positions', posId]) + const posHref1 = href( + `${ref1}/positions/${posId}` + ) as `${typeof ENDPOINT}/${typeof ref1}/positions/${string}` - const positionRef1 = ref(posHref1) + const positionRef1: EntityRef<'customerorderposition'> = ref(posHref1) const result1 = positionRef(ref1, posId) + t.deepEqual(result1, positionRef1, 'should return position entityRef') - const result2 = positionRef(ref1, posId, { + const result2 = { + ...positionRef(ref1, posId), quantity: 2 - }) + } + t.deepEqual(result2.quantity, 2, 'should return extended position entityRef') t.end() @@ -234,44 +266,112 @@ test('refEqual', t => { t.end() }) -test('copyEntRefs', t => { +test('copyFields', t => { const ms = Moysklad({ apiVersion: '1.2' }) - const { ref, copyEntRefs } = getHelpers(ms) + const { ref, copyFields } = getHelpers(ms) const ref1 = 'entity/customerorder/metadata/attributes/39f9f7bc-d4da-11e4-95df-0cc47a05161a' - const HREF1 = ms.buildUrl(ref1) + const HREF1 = ms.buildUrl(ref1) as `${typeof ENDPOINT}/${typeof ref1}` - const src = ref(HREF1, { - a: ref(HREF1), - b: ref(HREF1) - }) + const src = { + ...ref(HREF1), + a: { + ...ref('entity/customerorder/123-456'), + name: 'foo' + }, + b: ref(HREF1), + c: 42 + } - const result1 = copyEntRefs(src, ['a']) + const result1: { + a: EntityRef<'customerorder'> & { name: string } + c: number + } = copyFields(src, ['a', 'c']) - t.equal(result1.a.meta.href, HREF1, 'should copy entityRef field') + // @ts-expect-error + const result2: { + a: EntityRef<'customerorder'> + b: EntityRef<'attributemetadata'> + c: number + } = copyFields(src, ['a', 'c']) + + noop(result2) + + t.equal( + result1.a.meta.href, + `${ENDPOINT}/entity/customerorder/123-456`, + 'should copy field #1' + ) + + t.equal(result1.a.name, `foo`, 'should copy field #2') + + t.equal( + // @ts-expect-error + result1.b, + undefined, + 'should not copy field' + ) + + t.equal(result1.c, 42, 'should copy field #3') t.end() }) -test('copyEntRefs', t => { +test('copyFieldsRefs', t => { const ms = Moysklad({ apiVersion: '1.2' }) - const { ref, copyFields } = getHelpers(ms) + const { ref, copyFieldsRefs } = getHelpers(ms) const ref1 = 'entity/customerorder/metadata/attributes/39f9f7bc-d4da-11e4-95df-0cc47a05161a' - const HREF1 = ms.buildUrl(ref1) + const HREF1 = ms.buildUrl(ref1) as `${typeof ENDPOINT}/${typeof ref1}` + + const src = { + ...ref(HREF1), + a: { + ...ref('entity/customerorder/123-456'), + name: 'foo' + }, + b: ref(HREF1), + c: 42 + } + + const result1: { + a: EntityRef<'customerorder'> + c: number + } = copyFieldsRefs(src, ['a', 'c']) - const src = ref(HREF1, { - a: 'foo', - b: 2 - }) + // @ts-expect-error + const result2: { + a: EntityRef<'customerorder'> & { name: string } + c: number + } = copyFieldsRefs(src, ['a', 'c']) - const result1 = copyFields(src, ['a']) + // @ts-expect-error + const result3: { + a: EntityRef<'customerorder'> + b: EntityRef<'attributemetadata'> + c: number + } = copyFieldsRefs(src, ['a', 'c']) + + noop(result2, result3) + + t.equal( + result1.a.meta.href, + `${ENDPOINT}/entity/customerorder/123-456`, + 'should copy entityRef field' + ) + + t.equal( + // @ts-expect-error + result1.b, + undefined, + 'should not copy entityRef field' + ) - t.equal(result1.a, 'foo', 'should copy entityRef field') + t.equal(result1.c, 42, 'should copy not entityRef field') t.end() }) diff --git a/tests/tools.ts b/tests/tools.ts new file mode 100644 index 0000000..5926496 --- /dev/null +++ b/tests/tools.ts @@ -0,0 +1,5 @@ +export const noop = (...args: any[]) => { + args.length +} + +export type IsExtends = A extends B ? true : false diff --git a/tests/types/ref.test.ts b/tests/types/ref.test.ts new file mode 100644 index 0000000..55c4252 --- /dev/null +++ b/tests/types/ref.test.ts @@ -0,0 +1,27 @@ +import Moysklad from 'moysklad' + +import { EntityRef, getHelpers } from '../../src' +import { noop } from '../tools' + +const ms = Moysklad({ apiVersion: '1.2' }) +const { ref } = getHelpers(ms) + +// customentity + +const t10: EntityRef<'customentity'> = ref('entity/customentity/123-456') + +const t11: EntityRef<'customentity'> = ref( + 'https://online.moysklad.ru/api/remap/1.2/entity/customentity/123-456' +) + +// attributemetadata + +const t20: EntityRef<'attributemetadata'> = ref( + 'entity/demand/metadata/attributes/123-456' +) + +const t21: EntityRef<'customentity'> = ref( + 'https://online.moysklad.ru/api/remap/1.2/entity/customentity/123-456' +) + +noop(t10, t11, t20, t21) diff --git a/tests/types/types.test.ts b/tests/types/types.test.ts new file mode 100644 index 0000000..de71b60 --- /dev/null +++ b/tests/types/types.test.ts @@ -0,0 +1,36 @@ +import { HrefMetaType } from '../../src/types' +import { noop } from '../tools' + +const t10: HrefMetaType<'entity/demand/metadata/attributes/123-456'> = + 'attributemetadata' + +// @ts-expect-error +// never +const e11: HrefMetaType<'entity/demand/metadata/attributes3/123-456'> = 'demand' + +const t12: HrefMetaType<'https://online.moysklad.ru/api/remap/1.2/entity/demand/metadata/attributes/123-456'> = + 'attributemetadata' + +// @ts-expect-error +// never +const e12: HrefMetaType<'https://online.moysklad.ru/FOO/remap/1.2/entity/demand/metadata/attributes/123-456'> = + 'never' + +const t20: HrefMetaType<'entity/invoiceout/metadata/states/123-456'> = 'state' + +const t30: HrefMetaType<'context/companysettings/metadata/customEntities/123-456'> = + 'customentitymetadata' + +const t40: HrefMetaType<'context/companysettings/pricetype/123-456'> = + 'pricetype' + +const t50: HrefMetaType<'entity/customerorder/123-456'> = 'customerorder' + +const t60: HrefMetaType<'entity/purchaseorder/123-456/positions/123-456'> = + 'purchaseorderposition' + +// @ts-expect-error +// never +const t100: HrefMetaType<'foo'> = 'never' + +noop(t10, e11, t12, e12, t20, t30, t40, t50, t60, t100)