From e84ef9b396856cc6f402e1e3faf4ee0ec6f33308 Mon Sep 17 00:00:00 2001 From: daveads Date: Tue, 17 Dec 2024 14:10:12 +0100 Subject: [PATCH 01/34] export command --- source/commands/env/export.tsx | 455 +++++++++++++++++++++++++++++++++ tests/export.test.tsx | 277 ++++++++++++++++++++ 2 files changed, 732 insertions(+) create mode 100644 source/commands/env/export.tsx create mode 100644 tests/export.test.tsx diff --git a/source/commands/env/export.tsx b/source/commands/env/export.tsx new file mode 100644 index 0000000..6d8f3b0 --- /dev/null +++ b/source/commands/env/export.tsx @@ -0,0 +1,455 @@ +import React from 'react'; +import { Text } from 'ink'; +import { option } from 'pastel'; +import zod from 'zod'; +import fs from 'node:fs/promises'; +import Spinner from 'ink-spinner'; +import { useApiKeyApi } from '../../hooks/useApiKeyApi.js'; +import { AuthProvider, useAuth } from '../../components/AuthProvider.js'; +import { Permit } from 'permitio'; + +export const options = zod.object({ + key: zod + .string() + .optional() + .describe( + option({ + description: 'API Key to be used for the environment export', + alias: 'k', + }), + ), + file: zod + .string() + .optional() + .describe( + option({ + description: 'File path to save the exported HCL content', + alias: 'f', + }), + ), +}); + +type Props = { + readonly options: zod.infer; +}; + +interface ExportState { + status: string; + isComplete: boolean; + error: string | null; + warnings: string[]; +} + +function createSafeId(...parts: string[]): string { + return parts + .map(part => (part || '').replace(/[^a-zA-Z0-9_]/g, '_')) + .filter(Boolean) + .join('_'); +} + +const ExportContent: React.FC = ({ options: { key: apiKey, file } }) => { + const [state, setState] = React.useState({ + status: '', + isComplete: false, + error: null, + warnings: [], + }); + + const { validateApiKeyScope } = useApiKeyApi(); + const { authToken } = useAuth(); + const key = apiKey || authToken; + + const addWarning = (warning: string) => { + setState(prev => ({ + ...prev, + warnings: [...prev.warnings, warning], + status: `Warning: ${warning}`, + })); + }; + + React.useEffect(() => { + let isSubscribed = true; + + const exportConfig = async () => { + if (!key) { + setState(prev => ({ + ...prev, + error: 'No API key provided. Please provide a key or login first.', + isComplete: true, + })); + return; + } + + try { + setState(prev => ({ ...prev, status: 'Validating API key...' })); + const { + valid, + error: scopeError, + scope, + } = await validateApiKeyScope(key, 'environment'); + if (!valid || scopeError) { + setState(prev => ({ + ...prev, + error: `Invalid API key: ${scopeError}`, + isComplete: true, + })); + return; + } + + if (!isSubscribed) return; + + setState(prev => ({ + ...prev, + status: 'Initializing Permit client...', + })); + const permit = new Permit({ + token: key, + pdp: 'http://localhost:7766', + }); + + let hcl = `# Generated by Permit CLI +# Environment: ${scope?.environment_id || 'unknown'} +# Project: ${scope?.project_id || 'unknown'} +# Organization: ${scope?.organization_id || 'unknown'} + +terraform { + required_providers { + permitio = { + source = "permitio/permit-io" + version = "~> 0.1.0" + } + } +} + +provider "permitio" { + api_key = "${key}" +}\n`; + + // Export Resources + setState(prev => ({ ...prev, status: 'Exporting resources...' })); + const resources = await permit.api.resources.list(); + const validResources = resources.filter( + resource => resource.key !== '__user', + ); + if (validResources.length > 0) { + hcl += '\n# Resources\n'; + for (const resource of validResources) { + hcl += `resource "permitio_resource" "${createSafeId(resource.key)}" { + key = "${resource.key}" + name = "${resource.name}"${ + resource.description + ? ` + description = "${resource.description}"` + : '' + }${ + resource.urn + ? ` + urn = "${resource.urn}"` + : '' + } + actions = {${Object.entries(resource.actions) + .map( + ([actionKey, action]) => ` + "${actionKey}" = { + name = "${action.name}"${ + action.description + ? ` + description = "${action.description}"` + : '' + } + }`, + ) + .join('')} + }${ + resource.attributes && Object.keys(resource.attributes).length > 0 + ? ` + attributes = {${Object.entries(resource.attributes) + .map( + ([attrKey, attr]) => ` + "${attrKey}" = { + type = "${attr.type}"${ + attr.description + ? ` + description = "${attr.description}"` + : '' + } + }`, + ) + .join('')} + }` + : '' + } +}\n`; + } + } + + // Export Resource Relations + setState(prev => ({ + ...prev, + status: 'Exporting resource relations...', + })); + try { + for (const resource of validResources) { + const relations = await permit.api.resourceRelations.list({ + resourceKey: resource.key, + }); + if (relations && relations.length > 0) { + hcl += `\n# Resource Relations for ${resource.key}\n`; + for (const relation of relations) { + const safeId = createSafeId(resource.key, relation.key); + hcl += `resource "permitio_relation" "${safeId}" { + key = "${relation.key}" + name = "${relation.name}" + subject_resource = "${resource.key}" + object_resource = "${relation.object_resource}"${ + relation.description + ? ` + description = "${relation.description}"` + : '' + } +}\n`; + } + } + } + } catch (error) { + addWarning(`Failed to export resource relations: ${error}`); + } + + // Export Roles + setState(prev => ({ ...prev, status: 'Exporting roles...' })); + try { + const roles = await permit.api.roles.list(); + if (roles && roles.length > 0) { + hcl += '\n# Roles\n'; + for (const role of roles) { + hcl += `resource "permitio_role" "${createSafeId(role.key)}" { + key = "${role.key}" + name = "${role.name}"${ + role.description + ? ` + description = "${role.description}"` + : '' + }${ + role.permissions && role.permissions.length > 0 + ? ` + permissions = ${JSON.stringify(role.permissions)}` + : '' + }${ + role.extends && role.extends.length > 0 + ? ` + extends = ${JSON.stringify(role.extends)}` + : '' + } +}\n`; + } + } + } catch (error) { + addWarning(`Failed to export roles: ${error}`); + } + + // Export User Attributes + setState(prev => ({ ...prev, status: 'Exporting user attributes...' })); + try { + const userAttributes = await permit.api.resourceAttributes.list({ + resourceKey: '__user', + }); + if (userAttributes && userAttributes.length > 0) { + hcl += '\n# User Attributes\n'; + for (const attr of userAttributes) { + hcl += `resource "permitio_user_attribute" "${createSafeId(attr.key)}" { + key = "${attr.key}" + type = "${attr.type}"${ + attr.description + ? ` + description = "${attr.description}"` + : '' + } +}\n`; + } + } + } catch (error) { + addWarning(`Failed to export user attributes: ${error}`); + } + + // Export Condition Sets + setState(prev => ({ ...prev, status: 'Exporting condition sets...' })); + try { + const conditionSets = await permit.api.conditionSets.list(); + + // Export User Sets + const userSets = conditionSets.filter(set => set.type === 'userset'); + if (userSets.length > 0) { + hcl += '\n# User Sets\n'; + for (const set of userSets) { + if (!set.key || !set.name) { + addWarning(`Invalid user set data: ${JSON.stringify(set)}`); + continue; + } + hcl += `resource "permitio_user_set" "${createSafeId(set.key)}" { + key = "${set.key}" + name = "${set.name}"${ + set.description + ? ` + description = "${set.description}"` + : '' + } + conditions = jsonencode(${JSON.stringify(set.conditions || {}, null, 2)}) +}\n`; + } + } + + // Export Resource Sets + const resourceSets = conditionSets.filter( + set => set.type === 'resourceset', + ); + if (resourceSets.length > 0) { + hcl += '\n# Resource Sets\n'; + for (const set of resourceSets) { + if (!set.key || !set.name || !set.resource_id) { + addWarning(`Invalid resource set data: ${JSON.stringify(set)}`); + continue; + } + hcl += `resource "permitio_resource_set" "${createSafeId(set.key)}" { + key = "${set.key}" + name = "${set.name}"${ + set.description + ? ` + description = "${set.description}"` + : '' + } + resource = "${set.resource_id}" + conditions = jsonencode(${JSON.stringify(set.conditions || {}, null, 2)}) +}\n`; + } + } + + // Export Condition Set Rules + setState(prev => ({ + ...prev, + status: 'Exporting condition set rules...', + })); + const conditionSetRules = await permit.api.conditionSetRules.list({ + userSetKey: '', + permissionKey: '', + resourceSetKey: '', + }); + + if (conditionSetRules && conditionSetRules.length > 0) { + hcl += '\n# Condition Set Rules\n'; + for (const rule of conditionSetRules) { + if (!rule.user_set || !rule.permission || !rule.resource_set) { + addWarning( + `Invalid condition set rule: ${JSON.stringify(rule)}`, + ); + continue; + } + const safeId = createSafeId( + rule.user_set, + rule.permission, + rule.resource_set, + ); + hcl += `resource "permitio_condition_set_rule" "${safeId}" { + user_set = "${rule.user_set}" + permission = "${rule.permission}" + resource_set = "${rule.resource_set}" +}\n`; + } + } + } catch (error) { + addWarning(`Failed to export condition sets: ${error}`); + } + + if (!isSubscribed) return; + + // Save or print output + if (file) { + setState(prev => ({ ...prev, status: 'Saving to file...' })); + await fs.writeFile(file, hcl); + } else { + console.log(hcl); + } + + if (!isSubscribed) return; + setState(prev => ({ ...prev, isComplete: true })); + } catch (err) { + if (!isSubscribed) return; + const errorMsg = err instanceof Error ? err.message : String(err); + setState(prev => ({ + ...prev, + error: `Failed to export configuration: ${errorMsg}`, + isComplete: true, + })); + } + }; + + exportConfig(); + + return () => { + isSubscribed = false; + }; + }, [key, file, validateApiKeyScope]); + + if (state.error) { + return ( + <> + Error: {state.error} + {state.warnings.length > 0 && ( + <> + Warnings: + {state.warnings.map((warning, i) => ( + + - {warning} + + ))} + + )} + + ); + } + + if (!state.isComplete) { + return ( + <> + + {' '} + {state.status || 'Exporting environment configuration...'} + + {state.warnings.length > 0 && ( + <> + Warnings: + {state.warnings.map((warning, i) => ( + + - {warning} + + ))} + + )} + + ); + } + + return ( + <> + Export completed successfully! + {file && HCL content has been saved to: {file}} + {state.warnings.length > 0 && ( + <> + Warnings during export: + {state.warnings.map((warning, i) => ( + + - {warning} + + ))} + + )} + + ); +}; + +export default function Export(props: Props) { + return ( + + + + ); +} diff --git a/tests/export.test.tsx b/tests/export.test.tsx new file mode 100644 index 0000000..21d7934 --- /dev/null +++ b/tests/export.test.tsx @@ -0,0 +1,277 @@ +import { expect, vi, describe, it, beforeEach, afterEach } from 'vitest'; +import React from 'react'; +import { render, cleanup } from 'ink-testing-library'; +import Export from '../source/commands/env/export.js'; +import { Permit } from 'permitio'; +import * as fs from 'node:fs/promises'; +import type { useApiKeyApi } from '../source/hooks/useApiKeyApi'; + +// Create mock objects +const mockValidateApiKeyScope = vi.fn(); +const mockUseApiKeyApi = vi.fn(() => ({ + validateApiKeyScope: mockValidateApiKeyScope +})); +const mockUseAuth = vi.fn(() => ({ + authToken: 'mock-auth-token', +})); + +// Mock hooks and dependencies +vi.mock('../source/hooks/useApiKeyApi', () => ({ + useApiKeyApi: () => mockUseApiKeyApi() +})); + +vi.mock('../source/components/AuthProvider', () => ({ + AuthProvider: vi.fn(({ children }) => children), + useAuth: () => mockUseAuth(), +})); + +vi.mock('permitio'); +vi.mock('node:fs/promises'); + +// Mock sample data +const mockResources = [ + { + key: 'document', + name: 'Document', + description: 'Document resource', + actions: { + read: { name: 'Read', description: 'Read document' }, + write: { name: 'Write' }, + }, + attributes: { + owner: { type: 'string', description: 'Document owner' }, + }, + }, + { + key: '__user', + name: 'User', + actions: {}, + }, +]; + +const mockRoles = [ + { + key: 'admin', + name: 'Administrator', + description: 'Admin role', + permissions: ['document:read', 'document:write'], + extends: ['viewer'], + }, +]; + +const mockUserAttributes = [ + { + key: 'department', + type: 'string', + description: 'User department', + }, +]; + +const mockConditionSets = [ + { + key: 'us_employees', + name: 'US Employees', + type: 'userset', + description: 'Employees in US', + conditions: { country: 'US' }, + }, + { + key: 'confidential_docs', + name: 'Confidential Documents', + type: 'resourceset', + description: 'Confidential documents', + resource_id: 'document', + conditions: { classification: 'confidential' }, + }, +]; + +const mockConditionSetRules = [ + { + user_set: 'us_employees', + permission: 'document:read', + resource_set: 'confidential_docs', + }, +]; + +describe('Export Command', () => { + beforeEach(() => { + vi.clearAllMocks(); + + // Reset useAuth mock to default value + mockUseAuth.mockImplementation(() => ({ + authToken: 'mock-auth-token', + })); + + // Setup Permit mock + const mockPermit = { + api: { + resources: { list: vi.fn().mockResolvedValue(mockResources) }, + roles: { list: vi.fn().mockResolvedValue(mockRoles) }, + resourceAttributes: { list: vi.fn().mockResolvedValue(mockUserAttributes) }, + resourceRelations: { list: vi.fn().mockResolvedValue([]) }, + conditionSets: { list: vi.fn().mockResolvedValue(mockConditionSets) }, + conditionSetRules: { list: vi.fn().mockResolvedValue(mockConditionSetRules) }, + }, + }; + vi.mocked(Permit).mockImplementation(() => mockPermit as any); + + // Setup API key validation mock + mockValidateApiKeyScope.mockResolvedValue({ + valid: true, + scope: { + organization_id: 'org-123', + project_id: 'proj-123', + environment_id: 'env-123', + }, + error: null, + }); + }); + + afterEach(() => { + cleanup(); + }); + + it('exports configuration successfully to console', async () => { + const { lastFrame, unmount } = render( + + ); + + await vi.waitFor(() => { + expect(lastFrame()).toContain('Export completed successfully!'); + }); + + expect(Permit).toHaveBeenCalledWith({ + token: 'test-key', + pdp: 'http://localhost:7766', + }); + expect(mockValidateApiKeyScope).toHaveBeenCalledWith('test-key', 'environment'); + + unmount(); + }); + + it('exports configuration successfully to file', async () => { + const mockWriteFile = vi.mocked(fs.writeFile).mockResolvedValue(undefined); + + const { lastFrame, unmount } = render( + + ); + + await vi.waitFor(() => { + expect(lastFrame()).toContain('Export completed successfully!'); + expect(lastFrame()).toContain('HCL content has been saved to: output.tf'); + }); + + expect(mockWriteFile).toHaveBeenCalled(); + expect(mockWriteFile.mock.calls[0][0]).toBe('output.tf'); + expect(mockWriteFile.mock.calls[0][1]).toContain('terraform {'); + + unmount(); + }); + + it('handles invalid API key error', async () => { + mockValidateApiKeyScope.mockResolvedValue({ + valid: false, + error: 'Invalid API key', + scope: null, + }); + + const { lastFrame, unmount } = render( + + ); + + await vi.waitFor(() => { + expect(lastFrame()).toContain('Error: Invalid API key'); + }); + + unmount(); + }); + + it('handles missing API key error', async () => { + mockUseAuth.mockImplementation(() => ({ + authToken: null, + })); + + const { lastFrame, unmount } = render( + + ); + + await vi.waitFor(() => { + expect(lastFrame()).toContain('No API key provided'); + }); + + unmount(); + }); + + it('handles resource fetch error with warning', async () => { + const mockError = new Error('Failed to fetch resources'); + const mockPermit = { + api: { + resources: { list: vi.fn().mockRejectedValue(mockError) }, + roles: { list: vi.fn().mockResolvedValue([]) }, + resourceAttributes: { list: vi.fn().mockResolvedValue([]) }, + resourceRelations: { list: vi.fn().mockResolvedValue([]) }, + conditionSets: { list: vi.fn().mockResolvedValue([]) }, + conditionSetRules: { list: vi.fn().mockResolvedValue([]) }, + }, + }; + vi.mocked(Permit).mockImplementation(() => mockPermit as any); + + const { lastFrame, unmount } = render( + + ); + + await vi.waitFor(() => { + expect(lastFrame()).toContain('Failed to export configuration'); + }); + + unmount(); + }); + + it('displays spinner and status during export', async () => { + const { lastFrame, frames, unmount } = render( + + ); + + // Check initial loading state + expect(frames[0]).toContain('Exporting environment configuration'); + + // Should show various status messages during export + await vi.waitFor(() => { + expect(lastFrame()).toContain('Export completed successfully!'); + }); + + unmount(); + }); + + it('handles file write errors', async () => { + const mockError = new Error('Permission denied'); + vi.mocked(fs.writeFile).mockRejectedValue(mockError); + + const { lastFrame, unmount } = render( + + ); + + await vi.waitFor(() => { + expect(lastFrame()).toContain('Failed to export configuration: Permission denied'); + }); + + unmount(); + }); + + it('uses auth token when no API key is provided', async () => { + const { lastFrame, unmount } = render( + + ); + + await vi.waitFor(() => { + expect(lastFrame()).toContain('Export completed successfully!'); + }); + + expect(Permit).toHaveBeenCalledWith({ + token: 'mock-auth-token', + pdp: 'http://localhost:7766', + }); + + unmount(); + }); +}); \ No newline at end of file From 087cc4ada1251c49a34504d14b76f7a1af31a26e Mon Sep 17 00:00:00 2001 From: daveads Date: Tue, 17 Dec 2024 15:02:16 +0100 Subject: [PATCH 02/34] format export.test.tsx with Prettier --- tests/export.test.tsx | 475 +++++++++++++++++++++--------------------- 1 file changed, 240 insertions(+), 235 deletions(-) diff --git a/tests/export.test.tsx b/tests/export.test.tsx index 21d7934..61c0672 100644 --- a/tests/export.test.tsx +++ b/tests/export.test.tsx @@ -9,20 +9,20 @@ import type { useApiKeyApi } from '../source/hooks/useApiKeyApi'; // Create mock objects const mockValidateApiKeyScope = vi.fn(); const mockUseApiKeyApi = vi.fn(() => ({ - validateApiKeyScope: mockValidateApiKeyScope + validateApiKeyScope: mockValidateApiKeyScope, })); const mockUseAuth = vi.fn(() => ({ - authToken: 'mock-auth-token', + authToken: 'mock-auth-token', })); // Mock hooks and dependencies vi.mock('../source/hooks/useApiKeyApi', () => ({ - useApiKeyApi: () => mockUseApiKeyApi() + useApiKeyApi: () => mockUseApiKeyApi(), })); vi.mock('../source/components/AuthProvider', () => ({ - AuthProvider: vi.fn(({ children }) => children), - useAuth: () => mockUseAuth(), + AuthProvider: vi.fn(({ children }) => children), + useAuth: () => mockUseAuth(), })); vi.mock('permitio'); @@ -30,248 +30,253 @@ vi.mock('node:fs/promises'); // Mock sample data const mockResources = [ - { - key: 'document', - name: 'Document', - description: 'Document resource', - actions: { - read: { name: 'Read', description: 'Read document' }, - write: { name: 'Write' }, - }, - attributes: { - owner: { type: 'string', description: 'Document owner' }, - }, - }, - { - key: '__user', - name: 'User', - actions: {}, - }, + { + key: 'document', + name: 'Document', + description: 'Document resource', + actions: { + read: { name: 'Read', description: 'Read document' }, + write: { name: 'Write' }, + }, + attributes: { + owner: { type: 'string', description: 'Document owner' }, + }, + }, + { + key: '__user', + name: 'User', + actions: {}, + }, ]; const mockRoles = [ - { - key: 'admin', - name: 'Administrator', - description: 'Admin role', - permissions: ['document:read', 'document:write'], - extends: ['viewer'], - }, + { + key: 'admin', + name: 'Administrator', + description: 'Admin role', + permissions: ['document:read', 'document:write'], + extends: ['viewer'], + }, ]; const mockUserAttributes = [ - { - key: 'department', - type: 'string', - description: 'User department', - }, + { + key: 'department', + type: 'string', + description: 'User department', + }, ]; const mockConditionSets = [ - { - key: 'us_employees', - name: 'US Employees', - type: 'userset', - description: 'Employees in US', - conditions: { country: 'US' }, - }, - { - key: 'confidential_docs', - name: 'Confidential Documents', - type: 'resourceset', - description: 'Confidential documents', - resource_id: 'document', - conditions: { classification: 'confidential' }, - }, + { + key: 'us_employees', + name: 'US Employees', + type: 'userset', + description: 'Employees in US', + conditions: { country: 'US' }, + }, + { + key: 'confidential_docs', + name: 'Confidential Documents', + type: 'resourceset', + description: 'Confidential documents', + resource_id: 'document', + conditions: { classification: 'confidential' }, + }, ]; const mockConditionSetRules = [ - { - user_set: 'us_employees', - permission: 'document:read', - resource_set: 'confidential_docs', - }, + { + user_set: 'us_employees', + permission: 'document:read', + resource_set: 'confidential_docs', + }, ]; describe('Export Command', () => { - beforeEach(() => { - vi.clearAllMocks(); - - // Reset useAuth mock to default value - mockUseAuth.mockImplementation(() => ({ - authToken: 'mock-auth-token', - })); - - // Setup Permit mock - const mockPermit = { - api: { - resources: { list: vi.fn().mockResolvedValue(mockResources) }, - roles: { list: vi.fn().mockResolvedValue(mockRoles) }, - resourceAttributes: { list: vi.fn().mockResolvedValue(mockUserAttributes) }, - resourceRelations: { list: vi.fn().mockResolvedValue([]) }, - conditionSets: { list: vi.fn().mockResolvedValue(mockConditionSets) }, - conditionSetRules: { list: vi.fn().mockResolvedValue(mockConditionSetRules) }, - }, - }; - vi.mocked(Permit).mockImplementation(() => mockPermit as any); - - // Setup API key validation mock - mockValidateApiKeyScope.mockResolvedValue({ - valid: true, - scope: { - organization_id: 'org-123', - project_id: 'proj-123', - environment_id: 'env-123', - }, - error: null, - }); - }); - - afterEach(() => { - cleanup(); - }); - - it('exports configuration successfully to console', async () => { - const { lastFrame, unmount } = render( - - ); - - await vi.waitFor(() => { - expect(lastFrame()).toContain('Export completed successfully!'); - }); - - expect(Permit).toHaveBeenCalledWith({ - token: 'test-key', - pdp: 'http://localhost:7766', - }); - expect(mockValidateApiKeyScope).toHaveBeenCalledWith('test-key', 'environment'); - - unmount(); - }); - - it('exports configuration successfully to file', async () => { - const mockWriteFile = vi.mocked(fs.writeFile).mockResolvedValue(undefined); - - const { lastFrame, unmount } = render( - - ); - - await vi.waitFor(() => { - expect(lastFrame()).toContain('Export completed successfully!'); - expect(lastFrame()).toContain('HCL content has been saved to: output.tf'); - }); - - expect(mockWriteFile).toHaveBeenCalled(); - expect(mockWriteFile.mock.calls[0][0]).toBe('output.tf'); - expect(mockWriteFile.mock.calls[0][1]).toContain('terraform {'); - - unmount(); - }); - - it('handles invalid API key error', async () => { - mockValidateApiKeyScope.mockResolvedValue({ - valid: false, - error: 'Invalid API key', - scope: null, - }); - - const { lastFrame, unmount } = render( - - ); - - await vi.waitFor(() => { - expect(lastFrame()).toContain('Error: Invalid API key'); - }); - - unmount(); - }); - - it('handles missing API key error', async () => { - mockUseAuth.mockImplementation(() => ({ - authToken: null, - })); - - const { lastFrame, unmount } = render( - - ); - - await vi.waitFor(() => { - expect(lastFrame()).toContain('No API key provided'); - }); - - unmount(); - }); - - it('handles resource fetch error with warning', async () => { - const mockError = new Error('Failed to fetch resources'); - const mockPermit = { - api: { - resources: { list: vi.fn().mockRejectedValue(mockError) }, - roles: { list: vi.fn().mockResolvedValue([]) }, - resourceAttributes: { list: vi.fn().mockResolvedValue([]) }, - resourceRelations: { list: vi.fn().mockResolvedValue([]) }, - conditionSets: { list: vi.fn().mockResolvedValue([]) }, - conditionSetRules: { list: vi.fn().mockResolvedValue([]) }, - }, - }; - vi.mocked(Permit).mockImplementation(() => mockPermit as any); - - const { lastFrame, unmount } = render( - - ); - - await vi.waitFor(() => { - expect(lastFrame()).toContain('Failed to export configuration'); - }); - - unmount(); - }); - - it('displays spinner and status during export', async () => { - const { lastFrame, frames, unmount } = render( - - ); - - // Check initial loading state - expect(frames[0]).toContain('Exporting environment configuration'); - - // Should show various status messages during export - await vi.waitFor(() => { - expect(lastFrame()).toContain('Export completed successfully!'); - }); - - unmount(); - }); - - it('handles file write errors', async () => { - const mockError = new Error('Permission denied'); - vi.mocked(fs.writeFile).mockRejectedValue(mockError); - - const { lastFrame, unmount } = render( - - ); - - await vi.waitFor(() => { - expect(lastFrame()).toContain('Failed to export configuration: Permission denied'); - }); - - unmount(); - }); - - it('uses auth token when no API key is provided', async () => { - const { lastFrame, unmount } = render( - - ); - - await vi.waitFor(() => { - expect(lastFrame()).toContain('Export completed successfully!'); - }); - - expect(Permit).toHaveBeenCalledWith({ - token: 'mock-auth-token', - pdp: 'http://localhost:7766', - }); - - unmount(); - }); -}); \ No newline at end of file + beforeEach(() => { + vi.clearAllMocks(); + + // Reset useAuth mock to default value + mockUseAuth.mockImplementation(() => ({ + authToken: 'mock-auth-token', + })); + + // Setup Permit mock + const mockPermit = { + api: { + resources: { list: vi.fn().mockResolvedValue(mockResources) }, + roles: { list: vi.fn().mockResolvedValue(mockRoles) }, + resourceAttributes: { + list: vi.fn().mockResolvedValue(mockUserAttributes), + }, + resourceRelations: { list: vi.fn().mockResolvedValue([]) }, + conditionSets: { list: vi.fn().mockResolvedValue(mockConditionSets) }, + conditionSetRules: { + list: vi.fn().mockResolvedValue(mockConditionSetRules), + }, + }, + }; + vi.mocked(Permit).mockImplementation(() => mockPermit as any); + + // Setup API key validation mock + mockValidateApiKeyScope.mockResolvedValue({ + valid: true, + scope: { + organization_id: 'org-123', + project_id: 'proj-123', + environment_id: 'env-123', + }, + error: null, + }); + }); + + afterEach(() => { + cleanup(); + }); + + it('exports configuration successfully to console', async () => { + const { lastFrame, unmount } = render( + , + ); + + await vi.waitFor(() => { + expect(lastFrame()).toContain('Export completed successfully!'); + }); + + expect(Permit).toHaveBeenCalledWith({ + token: 'test-key', + pdp: 'http://localhost:7766', + }); + expect(mockValidateApiKeyScope).toHaveBeenCalledWith( + 'test-key', + 'environment', + ); + + unmount(); + }); + + it('exports configuration successfully to file', async () => { + const mockWriteFile = vi.mocked(fs.writeFile).mockResolvedValue(undefined); + + const { lastFrame, unmount } = render( + , + ); + + await vi.waitFor(() => { + expect(lastFrame()).toContain('Export completed successfully!'); + expect(lastFrame()).toContain('HCL content has been saved to: output.tf'); + }); + + expect(mockWriteFile).toHaveBeenCalled(); + expect(mockWriteFile.mock.calls[0][0]).toBe('output.tf'); + expect(mockWriteFile.mock.calls[0][1]).toContain('terraform {'); + + unmount(); + }); + + it('handles invalid API key error', async () => { + mockValidateApiKeyScope.mockResolvedValue({ + valid: false, + error: 'Invalid API key', + scope: null, + }); + + const { lastFrame, unmount } = render( + , + ); + + await vi.waitFor(() => { + expect(lastFrame()).toContain('Error: Invalid API key'); + }); + + unmount(); + }); + + it('handles missing API key error', async () => { + mockUseAuth.mockImplementation(() => ({ + authToken: null, + })); + + const { lastFrame, unmount } = render(); + + await vi.waitFor(() => { + expect(lastFrame()).toContain('No API key provided'); + }); + + unmount(); + }); + + it('handles resource fetch error with warning', async () => { + const mockError = new Error('Failed to fetch resources'); + const mockPermit = { + api: { + resources: { list: vi.fn().mockRejectedValue(mockError) }, + roles: { list: vi.fn().mockResolvedValue([]) }, + resourceAttributes: { list: vi.fn().mockResolvedValue([]) }, + resourceRelations: { list: vi.fn().mockResolvedValue([]) }, + conditionSets: { list: vi.fn().mockResolvedValue([]) }, + conditionSetRules: { list: vi.fn().mockResolvedValue([]) }, + }, + }; + vi.mocked(Permit).mockImplementation(() => mockPermit as any); + + const { lastFrame, unmount } = render( + , + ); + + await vi.waitFor(() => { + expect(lastFrame()).toContain('Failed to export configuration'); + }); + + unmount(); + }); + + it('displays spinner and status during export', async () => { + const { lastFrame, frames, unmount } = render( + , + ); + + // Check initial loading state + expect(frames[0]).toContain('Exporting environment configuration'); + + // Should show various status messages during export + await vi.waitFor(() => { + expect(lastFrame()).toContain('Export completed successfully!'); + }); + + unmount(); + }); + + it('handles file write errors', async () => { + const mockError = new Error('Permission denied'); + vi.mocked(fs.writeFile).mockRejectedValue(mockError); + + const { lastFrame, unmount } = render( + , + ); + + await vi.waitFor(() => { + expect(lastFrame()).toContain( + 'Failed to export configuration: Permission denied', + ); + }); + + unmount(); + }); + + it('uses auth token when no API key is provided', async () => { + const { lastFrame, unmount } = render(); + + await vi.waitFor(() => { + expect(lastFrame()).toContain('Export completed successfully!'); + }); + + expect(Permit).toHaveBeenCalledWith({ + token: 'mock-auth-token', + pdp: 'http://localhost:7766', + }); + + unmount(); + }); +}); From 3d80656bfc5c30860516ed07384ad279fe3554bc Mon Sep 17 00:00:00 2001 From: daveads Date: Tue, 17 Dec 2024 23:45:12 +0100 Subject: [PATCH 03/34] refactored --- source/commands/env/export.tsx | 455 ------------------ .../env/export/components/ExportContent.tsx | 107 ++++ .../env/export/components/ExportStatus.tsx | 37 ++ .../env/export/components/hooks/PermitSDK.ts | 13 + .../env/export/components/hooks/useExport.ts | 57 +++ .../export/generators/ResourceGenerator.ts | 65 +++ .../env/export/generators/RoleGenerator.ts | 41 ++ .../generators/UserAttributesGenerator.ts | 38 ++ source/commands/env/export/index.tsx | 38 ++ source/commands/env/export/types.ts | 21 + source/commands/env/export/utils.ts | 36 ++ tests/export.test.tsx | 49 +- 12 files changed, 478 insertions(+), 479 deletions(-) delete mode 100644 source/commands/env/export.tsx create mode 100644 source/commands/env/export/components/ExportContent.tsx create mode 100644 source/commands/env/export/components/ExportStatus.tsx create mode 100644 source/commands/env/export/components/hooks/PermitSDK.ts create mode 100644 source/commands/env/export/components/hooks/useExport.ts create mode 100644 source/commands/env/export/generators/ResourceGenerator.ts create mode 100644 source/commands/env/export/generators/RoleGenerator.ts create mode 100644 source/commands/env/export/generators/UserAttributesGenerator.ts create mode 100644 source/commands/env/export/index.tsx create mode 100644 source/commands/env/export/types.ts create mode 100644 source/commands/env/export/utils.ts diff --git a/source/commands/env/export.tsx b/source/commands/env/export.tsx deleted file mode 100644 index 6d8f3b0..0000000 --- a/source/commands/env/export.tsx +++ /dev/null @@ -1,455 +0,0 @@ -import React from 'react'; -import { Text } from 'ink'; -import { option } from 'pastel'; -import zod from 'zod'; -import fs from 'node:fs/promises'; -import Spinner from 'ink-spinner'; -import { useApiKeyApi } from '../../hooks/useApiKeyApi.js'; -import { AuthProvider, useAuth } from '../../components/AuthProvider.js'; -import { Permit } from 'permitio'; - -export const options = zod.object({ - key: zod - .string() - .optional() - .describe( - option({ - description: 'API Key to be used for the environment export', - alias: 'k', - }), - ), - file: zod - .string() - .optional() - .describe( - option({ - description: 'File path to save the exported HCL content', - alias: 'f', - }), - ), -}); - -type Props = { - readonly options: zod.infer; -}; - -interface ExportState { - status: string; - isComplete: boolean; - error: string | null; - warnings: string[]; -} - -function createSafeId(...parts: string[]): string { - return parts - .map(part => (part || '').replace(/[^a-zA-Z0-9_]/g, '_')) - .filter(Boolean) - .join('_'); -} - -const ExportContent: React.FC = ({ options: { key: apiKey, file } }) => { - const [state, setState] = React.useState({ - status: '', - isComplete: false, - error: null, - warnings: [], - }); - - const { validateApiKeyScope } = useApiKeyApi(); - const { authToken } = useAuth(); - const key = apiKey || authToken; - - const addWarning = (warning: string) => { - setState(prev => ({ - ...prev, - warnings: [...prev.warnings, warning], - status: `Warning: ${warning}`, - })); - }; - - React.useEffect(() => { - let isSubscribed = true; - - const exportConfig = async () => { - if (!key) { - setState(prev => ({ - ...prev, - error: 'No API key provided. Please provide a key or login first.', - isComplete: true, - })); - return; - } - - try { - setState(prev => ({ ...prev, status: 'Validating API key...' })); - const { - valid, - error: scopeError, - scope, - } = await validateApiKeyScope(key, 'environment'); - if (!valid || scopeError) { - setState(prev => ({ - ...prev, - error: `Invalid API key: ${scopeError}`, - isComplete: true, - })); - return; - } - - if (!isSubscribed) return; - - setState(prev => ({ - ...prev, - status: 'Initializing Permit client...', - })); - const permit = new Permit({ - token: key, - pdp: 'http://localhost:7766', - }); - - let hcl = `# Generated by Permit CLI -# Environment: ${scope?.environment_id || 'unknown'} -# Project: ${scope?.project_id || 'unknown'} -# Organization: ${scope?.organization_id || 'unknown'} - -terraform { - required_providers { - permitio = { - source = "permitio/permit-io" - version = "~> 0.1.0" - } - } -} - -provider "permitio" { - api_key = "${key}" -}\n`; - - // Export Resources - setState(prev => ({ ...prev, status: 'Exporting resources...' })); - const resources = await permit.api.resources.list(); - const validResources = resources.filter( - resource => resource.key !== '__user', - ); - if (validResources.length > 0) { - hcl += '\n# Resources\n'; - for (const resource of validResources) { - hcl += `resource "permitio_resource" "${createSafeId(resource.key)}" { - key = "${resource.key}" - name = "${resource.name}"${ - resource.description - ? ` - description = "${resource.description}"` - : '' - }${ - resource.urn - ? ` - urn = "${resource.urn}"` - : '' - } - actions = {${Object.entries(resource.actions) - .map( - ([actionKey, action]) => ` - "${actionKey}" = { - name = "${action.name}"${ - action.description - ? ` - description = "${action.description}"` - : '' - } - }`, - ) - .join('')} - }${ - resource.attributes && Object.keys(resource.attributes).length > 0 - ? ` - attributes = {${Object.entries(resource.attributes) - .map( - ([attrKey, attr]) => ` - "${attrKey}" = { - type = "${attr.type}"${ - attr.description - ? ` - description = "${attr.description}"` - : '' - } - }`, - ) - .join('')} - }` - : '' - } -}\n`; - } - } - - // Export Resource Relations - setState(prev => ({ - ...prev, - status: 'Exporting resource relations...', - })); - try { - for (const resource of validResources) { - const relations = await permit.api.resourceRelations.list({ - resourceKey: resource.key, - }); - if (relations && relations.length > 0) { - hcl += `\n# Resource Relations for ${resource.key}\n`; - for (const relation of relations) { - const safeId = createSafeId(resource.key, relation.key); - hcl += `resource "permitio_relation" "${safeId}" { - key = "${relation.key}" - name = "${relation.name}" - subject_resource = "${resource.key}" - object_resource = "${relation.object_resource}"${ - relation.description - ? ` - description = "${relation.description}"` - : '' - } -}\n`; - } - } - } - } catch (error) { - addWarning(`Failed to export resource relations: ${error}`); - } - - // Export Roles - setState(prev => ({ ...prev, status: 'Exporting roles...' })); - try { - const roles = await permit.api.roles.list(); - if (roles && roles.length > 0) { - hcl += '\n# Roles\n'; - for (const role of roles) { - hcl += `resource "permitio_role" "${createSafeId(role.key)}" { - key = "${role.key}" - name = "${role.name}"${ - role.description - ? ` - description = "${role.description}"` - : '' - }${ - role.permissions && role.permissions.length > 0 - ? ` - permissions = ${JSON.stringify(role.permissions)}` - : '' - }${ - role.extends && role.extends.length > 0 - ? ` - extends = ${JSON.stringify(role.extends)}` - : '' - } -}\n`; - } - } - } catch (error) { - addWarning(`Failed to export roles: ${error}`); - } - - // Export User Attributes - setState(prev => ({ ...prev, status: 'Exporting user attributes...' })); - try { - const userAttributes = await permit.api.resourceAttributes.list({ - resourceKey: '__user', - }); - if (userAttributes && userAttributes.length > 0) { - hcl += '\n# User Attributes\n'; - for (const attr of userAttributes) { - hcl += `resource "permitio_user_attribute" "${createSafeId(attr.key)}" { - key = "${attr.key}" - type = "${attr.type}"${ - attr.description - ? ` - description = "${attr.description}"` - : '' - } -}\n`; - } - } - } catch (error) { - addWarning(`Failed to export user attributes: ${error}`); - } - - // Export Condition Sets - setState(prev => ({ ...prev, status: 'Exporting condition sets...' })); - try { - const conditionSets = await permit.api.conditionSets.list(); - - // Export User Sets - const userSets = conditionSets.filter(set => set.type === 'userset'); - if (userSets.length > 0) { - hcl += '\n# User Sets\n'; - for (const set of userSets) { - if (!set.key || !set.name) { - addWarning(`Invalid user set data: ${JSON.stringify(set)}`); - continue; - } - hcl += `resource "permitio_user_set" "${createSafeId(set.key)}" { - key = "${set.key}" - name = "${set.name}"${ - set.description - ? ` - description = "${set.description}"` - : '' - } - conditions = jsonencode(${JSON.stringify(set.conditions || {}, null, 2)}) -}\n`; - } - } - - // Export Resource Sets - const resourceSets = conditionSets.filter( - set => set.type === 'resourceset', - ); - if (resourceSets.length > 0) { - hcl += '\n# Resource Sets\n'; - for (const set of resourceSets) { - if (!set.key || !set.name || !set.resource_id) { - addWarning(`Invalid resource set data: ${JSON.stringify(set)}`); - continue; - } - hcl += `resource "permitio_resource_set" "${createSafeId(set.key)}" { - key = "${set.key}" - name = "${set.name}"${ - set.description - ? ` - description = "${set.description}"` - : '' - } - resource = "${set.resource_id}" - conditions = jsonencode(${JSON.stringify(set.conditions || {}, null, 2)}) -}\n`; - } - } - - // Export Condition Set Rules - setState(prev => ({ - ...prev, - status: 'Exporting condition set rules...', - })); - const conditionSetRules = await permit.api.conditionSetRules.list({ - userSetKey: '', - permissionKey: '', - resourceSetKey: '', - }); - - if (conditionSetRules && conditionSetRules.length > 0) { - hcl += '\n# Condition Set Rules\n'; - for (const rule of conditionSetRules) { - if (!rule.user_set || !rule.permission || !rule.resource_set) { - addWarning( - `Invalid condition set rule: ${JSON.stringify(rule)}`, - ); - continue; - } - const safeId = createSafeId( - rule.user_set, - rule.permission, - rule.resource_set, - ); - hcl += `resource "permitio_condition_set_rule" "${safeId}" { - user_set = "${rule.user_set}" - permission = "${rule.permission}" - resource_set = "${rule.resource_set}" -}\n`; - } - } - } catch (error) { - addWarning(`Failed to export condition sets: ${error}`); - } - - if (!isSubscribed) return; - - // Save or print output - if (file) { - setState(prev => ({ ...prev, status: 'Saving to file...' })); - await fs.writeFile(file, hcl); - } else { - console.log(hcl); - } - - if (!isSubscribed) return; - setState(prev => ({ ...prev, isComplete: true })); - } catch (err) { - if (!isSubscribed) return; - const errorMsg = err instanceof Error ? err.message : String(err); - setState(prev => ({ - ...prev, - error: `Failed to export configuration: ${errorMsg}`, - isComplete: true, - })); - } - }; - - exportConfig(); - - return () => { - isSubscribed = false; - }; - }, [key, file, validateApiKeyScope]); - - if (state.error) { - return ( - <> - Error: {state.error} - {state.warnings.length > 0 && ( - <> - Warnings: - {state.warnings.map((warning, i) => ( - - - {warning} - - ))} - - )} - - ); - } - - if (!state.isComplete) { - return ( - <> - - {' '} - {state.status || 'Exporting environment configuration...'} - - {state.warnings.length > 0 && ( - <> - Warnings: - {state.warnings.map((warning, i) => ( - - - {warning} - - ))} - - )} - - ); - } - - return ( - <> - Export completed successfully! - {file && HCL content has been saved to: {file}} - {state.warnings.length > 0 && ( - <> - Warnings during export: - {state.warnings.map((warning, i) => ( - - - {warning} - - ))} - - )} - - ); -}; - -export default function Export(props: Props) { - return ( - - - - ); -} diff --git a/source/commands/env/export/components/ExportContent.tsx b/source/commands/env/export/components/ExportContent.tsx new file mode 100644 index 0000000..5697534 --- /dev/null +++ b/source/commands/env/export/components/ExportContent.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { useApiKeyApi } from '../../../../hooks/useApiKeyApi.js'; +import { useAuth } from '../../../../components/AuthProvider.js'; +import { ExportOptions } from '../types.js'; +import { ExportStatus } from './ExportStatus.js'; +import { useExport } from './hooks/useExport.js'; +import fs from 'node:fs/promises'; + +export const ExportContent: React.FC<{ options: ExportOptions }> = ({ + options: { key: apiKey, file }, +}) => { + const { validateApiKeyScope } = useApiKeyApi(); + const { authToken } = useAuth(); + const key = apiKey || authToken; + const { state, setState, exportConfig } = useExport(key); + + React.useEffect(() => { + let isSubscribed = true; + + const runExport = async () => { + if (!key) { + setState({ + status: '', + error: 'No API key provided. Please provide a key or login first.', + isComplete: true, + warnings: [], + }); + return; + } + + try { + setState(prev => ({ ...prev, status: 'Validating API key...' })); + const { + valid, + error: scopeError, + scope, + } = await validateApiKeyScope(key, 'environment'); + + if (!valid || scopeError) { + setState({ + status: '', + error: `Invalid API key: ${scopeError}`, + isComplete: true, + warnings: [], + }); + return; + } + + if (!isSubscribed) return; + + setState(prev => ({ + ...prev, + status: 'Initializing export...', + })); + + const { hcl, warnings } = await exportConfig(scope); + + if (!isSubscribed) return; + + if (file) { + setState(prev => ({ ...prev, status: 'Saving to file...' })); + try { + await fs.writeFile(file, hcl); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + setState({ + status: '', + error: `Failed to export configuration: ${errorMessage}`, + isComplete: true, + warnings: [], + }); + return; + } + } else { + console.log(hcl); + } + + if (!isSubscribed) return; + + setState({ + status: '', + error: null, + isComplete: true, + warnings, + }); + } catch (err) { + if (!isSubscribed) return; + const errorMsg = err instanceof Error ? err.message : String(err); + setState({ + status: '', + error: `Failed to export configuration: ${errorMsg}`, + isComplete: true, + warnings: [], + }); + } + }; + + runExport(); + + return () => { + isSubscribed = false; + }; + }, [key, file, validateApiKeyScope]); + + return ; +}; diff --git a/source/commands/env/export/components/ExportStatus.tsx b/source/commands/env/export/components/ExportStatus.tsx new file mode 100644 index 0000000..ba3a2c1 --- /dev/null +++ b/source/commands/env/export/components/ExportStatus.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { Text } from 'ink'; +import Spinner from 'ink-spinner'; +import { ExportState } from '../types.js'; + +interface ExportStatusProps { + state: ExportState; + file?: string; +} + +export const ExportStatus: React.FC = ({ state, file }) => { + if (state.error) { + return ( + <> + Error: {state.error} + + ); + } + + if (!state.isComplete) { + return ( + <> + + {' '} + {state.status || 'Exporting environment configuration...'} + + + ); + } + + return ( + <> + Export completed successfully! + {file && HCL content has been saved to: {file}} + + ); +}; diff --git a/source/commands/env/export/components/hooks/PermitSDK.ts b/source/commands/env/export/components/hooks/PermitSDK.ts new file mode 100644 index 0000000..97f7156 --- /dev/null +++ b/source/commands/env/export/components/hooks/PermitSDK.ts @@ -0,0 +1,13 @@ +import { Permit } from 'permitio'; +import React from 'react'; + +export const PermitSDK = (token: string) => { + return React.useMemo( + () => + new Permit({ + token, + pdp: 'http://localhost:7766', + }), + [token], + ); +}; diff --git a/source/commands/env/export/components/hooks/useExport.ts b/source/commands/env/export/components/hooks/useExport.ts new file mode 100644 index 0000000..97fae5a --- /dev/null +++ b/source/commands/env/export/components/hooks/useExport.ts @@ -0,0 +1,57 @@ +import { useState } from 'react'; +import { PermitSDK } from './PermitSDK.js'; +import { ExportState } from '../../types.js'; +import { createWarningCollector, generateProviderBlock } from '../../utils.js'; +import { ResourceGenerator } from '../../generators/ResourceGenerator.js'; +import { RoleGenerator } from '../../generators/RoleGenerator.js'; +import { UserAttributesGenerator } from '../../generators/UserAttributesGenerator.js'; + +export const useExport = (apiKey: string) => { + const [state, setState] = useState({ + status: '', + isComplete: false, + error: null, + warnings: [], + }); + + const permit = PermitSDK(apiKey); + + const exportConfig = async (scope: any) => { + try { + const warningCollector = createWarningCollector(); + + let hcl = `# Generated by Permit CLI +# Environment: ${scope?.environment_id || 'unknown'} +# Project: ${scope?.project_id || 'unknown'} +# Organization: ${scope?.organization_id || 'unknown'} +${generateProviderBlock(apiKey)}`; + + const generators = [ + new ResourceGenerator(permit, warningCollector), + new RoleGenerator(permit, warningCollector), + new UserAttributesGenerator(permit, warningCollector), + ]; + + for (const generator of generators) { + setState(prev => ({ + ...prev, + status: `Exporting ${generator.name}...`, + })); + + const generatedHCL = await generator.generateHCL(); + hcl += generatedHCL; + } + + return { hcl, warnings: warningCollector.getWarnings() }; + } catch (error) { + console.error('Export error:', error); + throw error; + } + }; + + return { + state, + setState, + exportConfig, + }; +}; diff --git a/source/commands/env/export/generators/ResourceGenerator.ts b/source/commands/env/export/generators/ResourceGenerator.ts new file mode 100644 index 0000000..5ee0abd --- /dev/null +++ b/source/commands/env/export/generators/ResourceGenerator.ts @@ -0,0 +1,65 @@ +import { Permit } from 'permitio'; +import { HCLGenerator, WarningCollector } from '../types.js'; +import { createSafeId } from '../utils.js'; + +export class ResourceGenerator implements HCLGenerator { + name = 'resources'; + + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) {} + + async generateHCL(): Promise { + const resources = await this.permit.api.resources.list(); + const validResources = resources.filter( + resource => resource.key !== '__user', + ); + + if (validResources.length === 0) return ''; + + let hcl = '\n# Resources\n'; + + for (const resource of validResources) { + hcl += `resource "permitio_resource" "${createSafeId(resource.key)}" { + key = "${resource.key}" + name = "${resource.name}"${ + resource.description ? `\n description = "${resource.description}"` : '' + }${resource.urn ? `\n urn = "${resource.urn}"` : ''} + actions = {${Object.entries(resource.actions) + .map( + ([actionKey, action]) => ` + "${actionKey}" = { + name = "${action.name}"${ + action.description + ? `\n description = "${action.description}"` + : '' + } + }`, + ) + .join('')} + }${this.generateAttributes(resource.attributes)} +}\n`; + } + + return hcl; + } + + private generateAttributes( + attributes: Record | undefined, + ): string { + if (!attributes || Object.keys(attributes).length === 0) return ''; + + return `\n attributes = {${Object.entries(attributes) + .map( + ([attrKey, attr]) => ` + "${attrKey}" = { + type = "${attr.type}"${ + attr.description ? `\n description = "${attr.description}"` : '' + } + }`, + ) + .join('')} + }`; + } +} diff --git a/source/commands/env/export/generators/RoleGenerator.ts b/source/commands/env/export/generators/RoleGenerator.ts new file mode 100644 index 0000000..4437027 --- /dev/null +++ b/source/commands/env/export/generators/RoleGenerator.ts @@ -0,0 +1,41 @@ +import { Permit } from 'permitio'; +import { HCLGenerator, WarningCollector } from '../types.js'; +import { createSafeId } from '../utils.js'; + +export class RoleGenerator implements HCLGenerator { + name = 'roles'; + + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) {} + + async generateHCL(): Promise { + try { + const roles = await this.permit.api.roles.list(); + if (!roles || roles.length === 0) return ''; + + let hcl = '\n# Roles\n'; + for (const role of roles) { + hcl += `resource "permitio_role" "${createSafeId(role.key)}" { + key = "${role.key}" + name = "${role.name}"${ + role.description ? `\n description = "${role.description}"` : '' + }${ + role.permissions && role.permissions.length > 0 + ? `\n permissions = ${JSON.stringify(role.permissions)}` + : '' + }${ + role.extends && role.extends.length > 0 + ? `\n extends = ${JSON.stringify(role.extends)}` + : '' + } +}\n`; + } + return hcl; + } catch (error) { + this.warningCollector.addWarning(`Failed to export roles: ${error}`); + return ''; + } + } +} diff --git a/source/commands/env/export/generators/UserAttributesGenerator.ts b/source/commands/env/export/generators/UserAttributesGenerator.ts new file mode 100644 index 0000000..d3c1a6a --- /dev/null +++ b/source/commands/env/export/generators/UserAttributesGenerator.ts @@ -0,0 +1,38 @@ +import { Permit } from 'permitio'; +import { HCLGenerator, WarningCollector } from '../types.js'; +import { createSafeId } from '../utils.js'; + +export class UserAttributesGenerator implements HCLGenerator { + name = 'user attributes'; + + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) {} + + async generateHCL(): Promise { + try { + const userAttributes = await this.permit.api.resourceAttributes.list({ + resourceKey: '__user', + }); + + if (!userAttributes || userAttributes.length === 0) return ''; + + let hcl = '\n# User Attributes\n'; + for (const attr of userAttributes) { + hcl += `resource "permitio_user_attribute" "${createSafeId(attr.key)}" { + key = "${attr.key}" + type = "${attr.type}"${ + attr.description ? `\n description = "${attr.description}"` : '' + } +}\n`; + } + return hcl; + } catch (error) { + this.warningCollector.addWarning( + `Failed to export user attributes: ${error}`, + ); + return ''; + } + } +} diff --git a/source/commands/env/export/index.tsx b/source/commands/env/export/index.tsx new file mode 100644 index 0000000..6d965a7 --- /dev/null +++ b/source/commands/env/export/index.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { option } from 'pastel'; +import zod from 'zod'; +import { AuthProvider } from '../../../components/AuthProvider.js'; +import { ExportContent } from './components/ExportContent.js'; + +export const options = zod.object({ + key: zod + .string() + .optional() + .describe( + option({ + description: 'API Key to be used for the environment export', + alias: 'k', + }), + ), + file: zod + .string() + .optional() + .describe( + option({ + description: 'File path to save the exported HCL content', + alias: 'f', + }), + ), +}); + +type Props = { + readonly options: zod.infer; +}; + +export default function Export(props: Props) { + return ( + + + + ); +} diff --git a/source/commands/env/export/types.ts b/source/commands/env/export/types.ts new file mode 100644 index 0000000..90955fd --- /dev/null +++ b/source/commands/env/export/types.ts @@ -0,0 +1,21 @@ +export interface ExportState { + status: string; + isComplete: boolean; + error: string | null; + warnings: string[]; +} + +export interface ExportOptions { + key?: string; + file?: string; +} + +export interface HCLGenerator { + generateHCL(): Promise; + name: string; +} + +export interface WarningCollector { + addWarning(warning: string): void; + getWarnings(): string[]; +} diff --git a/source/commands/env/export/utils.ts b/source/commands/env/export/utils.ts new file mode 100644 index 0000000..f0df56e --- /dev/null +++ b/source/commands/env/export/utils.ts @@ -0,0 +1,36 @@ +import { WarningCollector } from './types.js'; + +export function createSafeId(...parts: string[]): string { + return parts + .map(part => (part || '').replace(/[^a-zA-Z0-9_]/g, '_')) + .filter(Boolean) + .join('_'); +} + +export function createWarningCollector(): WarningCollector { + const warnings: string[] = []; + + return { + addWarning(warning: string) { + warnings.push(warning); + }, + getWarnings() { + return warnings; + }, + }; +} + +export const generateProviderBlock = (key: string) => ` +terraform { + required_providers { + permitio = { + source = "permitio/permit-io" + version = "~> 0.1.0" + } + } +} + +provider "permitio" { + api_key = "${key}" +} +`; diff --git a/tests/export.test.tsx b/tests/export.test.tsx index 61c0672..1a9e7da 100644 --- a/tests/export.test.tsx +++ b/tests/export.test.tsx @@ -1,7 +1,7 @@ import { expect, vi, describe, it, beforeEach, afterEach } from 'vitest'; import React from 'react'; import { render, cleanup } from 'ink-testing-library'; -import Export from '../source/commands/env/export.js'; +import Export from '../source/commands/env/export/index.js'; import { Permit } from 'permitio'; import * as fs from 'node:fs/promises'; import type { useApiKeyApi } from '../source/hooks/useApiKeyApi'; @@ -208,29 +208,30 @@ describe('Export Command', () => { }); it('handles resource fetch error with warning', async () => { - const mockError = new Error('Failed to fetch resources'); - const mockPermit = { - api: { - resources: { list: vi.fn().mockRejectedValue(mockError) }, - roles: { list: vi.fn().mockResolvedValue([]) }, - resourceAttributes: { list: vi.fn().mockResolvedValue([]) }, - resourceRelations: { list: vi.fn().mockResolvedValue([]) }, - conditionSets: { list: vi.fn().mockResolvedValue([]) }, - conditionSetRules: { list: vi.fn().mockResolvedValue([]) }, - }, - }; - vi.mocked(Permit).mockImplementation(() => mockPermit as any); - - const { lastFrame, unmount } = render( - , - ); - - await vi.waitFor(() => { - expect(lastFrame()).toContain('Failed to export configuration'); - }); - - unmount(); - }); + const mockError = new Error('Failed to fetch resources'); + const mockPermit = { + api: { + resources: { list: vi.fn().mockRejectedValue(mockError) }, + roles: { list: vi.fn().mockResolvedValue([]) }, + resourceAttributes: { list: vi.fn().mockResolvedValue([]) }, + resourceRelations: { list: vi.fn().mockResolvedValue([]) }, + conditionSets: { list: vi.fn().mockResolvedValue([]) }, + conditionSetRules: { list: vi.fn().mockResolvedValue([]) }, + }, + }; + vi.mocked(Permit).mockImplementation(() => mockPermit as any); + + const { lastFrame, unmount } = render( + , + ); + + await vi.waitFor(() => { + console.log('Last frame content:', lastFrame()); + expect(lastFrame()).toContain('Failed to export configuration'); + }); + + unmount(); + }); it('displays spinner and status during export', async () => { const { lastFrame, frames, unmount } = render( From 9ab222d221b243da7de82a35fc740adf0c46420a Mon Sep 17 00:00:00 2001 From: daveads Date: Wed, 18 Dec 2024 00:32:23 +0100 Subject: [PATCH 04/34] updates --- .../env/export/components/ExportStatus.tsx | 20 +++++ .../env/export/components/hooks/useExport.ts | 9 +- .../generators/ConditionSetGenerator.ts | 62 +++++++++++++ .../export/generators/RelationGenerator.ts | 88 +++++++++++++++++++ 4 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 source/commands/env/export/generators/ConditionSetGenerator.ts create mode 100644 source/commands/env/export/generators/RelationGenerator.ts diff --git a/source/commands/env/export/components/ExportStatus.tsx b/source/commands/env/export/components/ExportStatus.tsx index ba3a2c1..37360cd 100644 --- a/source/commands/env/export/components/ExportStatus.tsx +++ b/source/commands/env/export/components/ExportStatus.tsx @@ -13,6 +13,16 @@ export const ExportStatus: React.FC = ({ state, file }) => { return ( <> Error: {state.error} + {state.warnings.length > 0 && ( + <> + Warnings: + {state.warnings.map((warning, i) => ( + + - {warning} + + ))} + + )} ); } @@ -32,6 +42,16 @@ export const ExportStatus: React.FC = ({ state, file }) => { <> Export completed successfully! {file && HCL content has been saved to: {file}} + {state.warnings.length > 0 && ( + <> + Warnings during export: + {state.warnings.map((warning, i) => ( + + - {warning} + + ))} + + )} ); }; diff --git a/source/commands/env/export/components/hooks/useExport.ts b/source/commands/env/export/components/hooks/useExport.ts index 97fae5a..f4c3c76 100644 --- a/source/commands/env/export/components/hooks/useExport.ts +++ b/source/commands/env/export/components/hooks/useExport.ts @@ -5,6 +5,8 @@ import { createWarningCollector, generateProviderBlock } from '../../utils.js'; import { ResourceGenerator } from '../../generators/ResourceGenerator.js'; import { RoleGenerator } from '../../generators/RoleGenerator.js'; import { UserAttributesGenerator } from '../../generators/UserAttributesGenerator.js'; +import { RelationGenerator } from '../../generators/RelationGenerator.js'; +import { ConditionSetGenerator } from '../../generators/ConditionSetGenerator.js'; export const useExport = (apiKey: string) => { const [state, setState] = useState({ @@ -26,10 +28,13 @@ export const useExport = (apiKey: string) => { # Organization: ${scope?.organization_id || 'unknown'} ${generateProviderBlock(apiKey)}`; + // Order matters for dependencies const generators = [ new ResourceGenerator(permit, warningCollector), new RoleGenerator(permit, warningCollector), new UserAttributesGenerator(permit, warningCollector), + new RelationGenerator(permit, warningCollector), + new ConditionSetGenerator(permit, warningCollector), ]; for (const generator of generators) { @@ -39,7 +44,9 @@ ${generateProviderBlock(apiKey)}`; })); const generatedHCL = await generator.generateHCL(); - hcl += generatedHCL; + if (generatedHCL) { + hcl += generatedHCL; + } } return { hcl, warnings: warningCollector.getWarnings() }; diff --git a/source/commands/env/export/generators/ConditionSetGenerator.ts b/source/commands/env/export/generators/ConditionSetGenerator.ts new file mode 100644 index 0000000..8269ecc --- /dev/null +++ b/source/commands/env/export/generators/ConditionSetGenerator.ts @@ -0,0 +1,62 @@ +import { Permit } from 'permitio'; +import { HCLGenerator, WarningCollector } from '../types.js'; +import { createSafeId } from '../utils.js'; + +export class ConditionSetGenerator implements HCLGenerator { + name = 'condition sets'; + + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) {} + + async generateHCL(): Promise { + try { + const conditionSets = await this.permit.api.conditionSets.list(); + if ( + !conditionSets || + !Array.isArray(conditionSets) || + conditionSets.length === 0 + ) { + return ''; + } + + let hcl = '\n# Condition Sets\n'; + + for (const set of conditionSets) { + try { + const isResourceSet = set.type === 'resourceset'; + const resourceType = isResourceSet ? 'resource_set' : 'user_set'; + + // Handle conditions - ensure they are properly stringified + const conditions = + typeof set.conditions === 'string' + ? set.conditions + : JSON.stringify(set.conditions || ''); + + hcl += `resource "permitio_${resourceType}" "${createSafeId(set.key)}" { + key = "${set.key}" + name = "${set.name}"${ + set.description ? `\n description = "${set.description}"` : '' + } + conditions = ${conditions}${ + set.resource ? `\n resource = "${set.resource}"` : '' + } +}\n`; + } catch (setError) { + this.warningCollector.addWarning( + `Failed to export condition set ${set.key}: ${setError}`, + ); + continue; + } + } + + return hcl; + } catch (error) { + this.warningCollector.addWarning( + `Failed to export condition sets: ${error}`, + ); + return ''; + } + } +} diff --git a/source/commands/env/export/generators/RelationGenerator.ts b/source/commands/env/export/generators/RelationGenerator.ts new file mode 100644 index 0000000..85ee8b3 --- /dev/null +++ b/source/commands/env/export/generators/RelationGenerator.ts @@ -0,0 +1,88 @@ +import { Permit } from 'permitio'; +import { HCLGenerator, WarningCollector } from '../types.js'; +import { createSafeId } from '../utils.js'; + +export class RelationGenerator implements HCLGenerator { + name = 'relations'; + + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) {} + + async generateHCL(): Promise { + try { + let hcl = '\n# Resource Relations\n'; + + // First get all resources + const resources = await this.permit.api.resources.list(); + if (!resources || !Array.isArray(resources)) { + return ''; + } + + // For each resource, get its relations + let allRelations = []; + for (const resource of resources) { + if (resource.key === '__user') continue; // Skip internal user resource + + try { + const resourceRelations = + await this.permit.api.resourceRelations.list({ + resourceKey: resource.key, + }); + + if (resourceRelations && Array.isArray(resourceRelations)) { + allRelations.push(...resourceRelations); + } + } catch (err) { + this.warningCollector.addWarning( + `Failed to fetch relations for resource ${resource.key}: ${err}`, + ); + } + } + + if (allRelations.length === 0) { + return ''; + } + + // Remove duplicates based on relation key + const uniqueRelations = Array.from( + new Map(allRelations.map(r => [r.key, r])).values(), + ); + + for (const relation of uniqueRelations) { + try { + if ( + !relation.key || + !relation.subject_resource || + !relation.object_resource + ) { + this.warningCollector.addWarning( + `Skipping invalid relation with key: ${relation.key}`, + ); + continue; + } + + hcl += `resource "permitio_relation" "${createSafeId(relation.key)}" { + key = "${relation.key}" + name = "${relation.name || relation.key}"${ + relation.description ? `\n description = "${relation.description}"` : '' + } + subject_resource = "${relation.subject_resource}" + object_resource = "${relation.object_resource}" +}\n`; + } catch (relationError) { + this.warningCollector.addWarning( + `Failed to export relation ${relation.key}: ${relationError}`, + ); + continue; + } + } + + return hcl; + } catch (error) { + this.warningCollector.addWarning(`Failed to export relations: ${error}`); + return ''; + } + } +} From dcfeb1e7c43922dcac465c9a3600569d9109a1f0 Mon Sep 17 00:00:00 2001 From: daveads Date: Wed, 18 Dec 2024 09:15:35 +0100 Subject: [PATCH 05/34] tests --- .../env/export/components/hooks/useExport.ts | 1 - .../export/generators/RelationGenerator.ts | 19 +- tests/export.test.tsx | 283 ------------------ tests/export/ConditionSetGenerator.test.ts | 70 +++++ tests/export/ExportContent.test.tsx | 48 +++ tests/export/ExportStatus.test.tsx | 34 +++ tests/export/ResourceGenerator.test.tsx | 98 ++++++ tests/export/RoleGenerator.test.tsx | 56 ++++ tests/export/UserAttributesGenerator.test.ts | 76 +++++ tests/export/generators.test.tsx | 55 ++++ tests/export/index.test.tsx | 19 ++ tests/export/mocks/hooks.tsx | 5 + tests/export/mocks/permit.ts | 59 ++++ tests/export/utils.test.tsx | 22 ++ 14 files changed, 552 insertions(+), 293 deletions(-) delete mode 100644 tests/export.test.tsx create mode 100644 tests/export/ConditionSetGenerator.test.ts create mode 100644 tests/export/ExportContent.test.tsx create mode 100644 tests/export/ExportStatus.test.tsx create mode 100644 tests/export/ResourceGenerator.test.tsx create mode 100644 tests/export/RoleGenerator.test.tsx create mode 100644 tests/export/UserAttributesGenerator.test.ts create mode 100644 tests/export/generators.test.tsx create mode 100644 tests/export/index.test.tsx create mode 100644 tests/export/mocks/hooks.tsx create mode 100644 tests/export/mocks/permit.ts create mode 100644 tests/export/utils.test.tsx diff --git a/source/commands/env/export/components/hooks/useExport.ts b/source/commands/env/export/components/hooks/useExport.ts index f4c3c76..028ebb3 100644 --- a/source/commands/env/export/components/hooks/useExport.ts +++ b/source/commands/env/export/components/hooks/useExport.ts @@ -28,7 +28,6 @@ export const useExport = (apiKey: string) => { # Organization: ${scope?.organization_id || 'unknown'} ${generateProviderBlock(apiKey)}`; - // Order matters for dependencies const generators = [ new ResourceGenerator(permit, warningCollector), new RoleGenerator(permit, warningCollector), diff --git a/source/commands/env/export/generators/RelationGenerator.ts b/source/commands/env/export/generators/RelationGenerator.ts index 85ee8b3..44adaef 100644 --- a/source/commands/env/export/generators/RelationGenerator.ts +++ b/source/commands/env/export/generators/RelationGenerator.ts @@ -12,12 +12,10 @@ export class RelationGenerator implements HCLGenerator { async generateHCL(): Promise { try { - let hcl = '\n# Resource Relations\n'; - // First get all resources const resources = await this.permit.api.resources.list(); if (!resources || !Array.isArray(resources)) { - return ''; + return ''; // Return empty string when no resources, no header } // For each resource, get its relations @@ -42,9 +40,12 @@ export class RelationGenerator implements HCLGenerator { } if (allRelations.length === 0) { - return ''; + return ''; // Return empty string if no relations found, no header } + // Add header only when we have relations to show + let hcl = '\n# Resource Relations\n'; + // Remove duplicates based on relation key const uniqueRelations = Array.from( new Map(allRelations.map(r => [r.key, r])).values(), @@ -64,13 +65,13 @@ export class RelationGenerator implements HCLGenerator { } hcl += `resource "permitio_relation" "${createSafeId(relation.key)}" { - key = "${relation.key}" - name = "${relation.name || relation.key}"${ + key = "${relation.key}" + name = "${relation.name || relation.key}"${ relation.description ? `\n description = "${relation.description}"` : '' } - subject_resource = "${relation.subject_resource}" - object_resource = "${relation.object_resource}" -}\n`; + subject_resource = "${relation.subject_resource}" + object_resource = "${relation.object_resource}" + }\n`; } catch (relationError) { this.warningCollector.addWarning( `Failed to export relation ${relation.key}: ${relationError}`, diff --git a/tests/export.test.tsx b/tests/export.test.tsx deleted file mode 100644 index 1a9e7da..0000000 --- a/tests/export.test.tsx +++ /dev/null @@ -1,283 +0,0 @@ -import { expect, vi, describe, it, beforeEach, afterEach } from 'vitest'; -import React from 'react'; -import { render, cleanup } from 'ink-testing-library'; -import Export from '../source/commands/env/export/index.js'; -import { Permit } from 'permitio'; -import * as fs from 'node:fs/promises'; -import type { useApiKeyApi } from '../source/hooks/useApiKeyApi'; - -// Create mock objects -const mockValidateApiKeyScope = vi.fn(); -const mockUseApiKeyApi = vi.fn(() => ({ - validateApiKeyScope: mockValidateApiKeyScope, -})); -const mockUseAuth = vi.fn(() => ({ - authToken: 'mock-auth-token', -})); - -// Mock hooks and dependencies -vi.mock('../source/hooks/useApiKeyApi', () => ({ - useApiKeyApi: () => mockUseApiKeyApi(), -})); - -vi.mock('../source/components/AuthProvider', () => ({ - AuthProvider: vi.fn(({ children }) => children), - useAuth: () => mockUseAuth(), -})); - -vi.mock('permitio'); -vi.mock('node:fs/promises'); - -// Mock sample data -const mockResources = [ - { - key: 'document', - name: 'Document', - description: 'Document resource', - actions: { - read: { name: 'Read', description: 'Read document' }, - write: { name: 'Write' }, - }, - attributes: { - owner: { type: 'string', description: 'Document owner' }, - }, - }, - { - key: '__user', - name: 'User', - actions: {}, - }, -]; - -const mockRoles = [ - { - key: 'admin', - name: 'Administrator', - description: 'Admin role', - permissions: ['document:read', 'document:write'], - extends: ['viewer'], - }, -]; - -const mockUserAttributes = [ - { - key: 'department', - type: 'string', - description: 'User department', - }, -]; - -const mockConditionSets = [ - { - key: 'us_employees', - name: 'US Employees', - type: 'userset', - description: 'Employees in US', - conditions: { country: 'US' }, - }, - { - key: 'confidential_docs', - name: 'Confidential Documents', - type: 'resourceset', - description: 'Confidential documents', - resource_id: 'document', - conditions: { classification: 'confidential' }, - }, -]; - -const mockConditionSetRules = [ - { - user_set: 'us_employees', - permission: 'document:read', - resource_set: 'confidential_docs', - }, -]; - -describe('Export Command', () => { - beforeEach(() => { - vi.clearAllMocks(); - - // Reset useAuth mock to default value - mockUseAuth.mockImplementation(() => ({ - authToken: 'mock-auth-token', - })); - - // Setup Permit mock - const mockPermit = { - api: { - resources: { list: vi.fn().mockResolvedValue(mockResources) }, - roles: { list: vi.fn().mockResolvedValue(mockRoles) }, - resourceAttributes: { - list: vi.fn().mockResolvedValue(mockUserAttributes), - }, - resourceRelations: { list: vi.fn().mockResolvedValue([]) }, - conditionSets: { list: vi.fn().mockResolvedValue(mockConditionSets) }, - conditionSetRules: { - list: vi.fn().mockResolvedValue(mockConditionSetRules), - }, - }, - }; - vi.mocked(Permit).mockImplementation(() => mockPermit as any); - - // Setup API key validation mock - mockValidateApiKeyScope.mockResolvedValue({ - valid: true, - scope: { - organization_id: 'org-123', - project_id: 'proj-123', - environment_id: 'env-123', - }, - error: null, - }); - }); - - afterEach(() => { - cleanup(); - }); - - it('exports configuration successfully to console', async () => { - const { lastFrame, unmount } = render( - , - ); - - await vi.waitFor(() => { - expect(lastFrame()).toContain('Export completed successfully!'); - }); - - expect(Permit).toHaveBeenCalledWith({ - token: 'test-key', - pdp: 'http://localhost:7766', - }); - expect(mockValidateApiKeyScope).toHaveBeenCalledWith( - 'test-key', - 'environment', - ); - - unmount(); - }); - - it('exports configuration successfully to file', async () => { - const mockWriteFile = vi.mocked(fs.writeFile).mockResolvedValue(undefined); - - const { lastFrame, unmount } = render( - , - ); - - await vi.waitFor(() => { - expect(lastFrame()).toContain('Export completed successfully!'); - expect(lastFrame()).toContain('HCL content has been saved to: output.tf'); - }); - - expect(mockWriteFile).toHaveBeenCalled(); - expect(mockWriteFile.mock.calls[0][0]).toBe('output.tf'); - expect(mockWriteFile.mock.calls[0][1]).toContain('terraform {'); - - unmount(); - }); - - it('handles invalid API key error', async () => { - mockValidateApiKeyScope.mockResolvedValue({ - valid: false, - error: 'Invalid API key', - scope: null, - }); - - const { lastFrame, unmount } = render( - , - ); - - await vi.waitFor(() => { - expect(lastFrame()).toContain('Error: Invalid API key'); - }); - - unmount(); - }); - - it('handles missing API key error', async () => { - mockUseAuth.mockImplementation(() => ({ - authToken: null, - })); - - const { lastFrame, unmount } = render(); - - await vi.waitFor(() => { - expect(lastFrame()).toContain('No API key provided'); - }); - - unmount(); - }); - - it('handles resource fetch error with warning', async () => { - const mockError = new Error('Failed to fetch resources'); - const mockPermit = { - api: { - resources: { list: vi.fn().mockRejectedValue(mockError) }, - roles: { list: vi.fn().mockResolvedValue([]) }, - resourceAttributes: { list: vi.fn().mockResolvedValue([]) }, - resourceRelations: { list: vi.fn().mockResolvedValue([]) }, - conditionSets: { list: vi.fn().mockResolvedValue([]) }, - conditionSetRules: { list: vi.fn().mockResolvedValue([]) }, - }, - }; - vi.mocked(Permit).mockImplementation(() => mockPermit as any); - - const { lastFrame, unmount } = render( - , - ); - - await vi.waitFor(() => { - console.log('Last frame content:', lastFrame()); - expect(lastFrame()).toContain('Failed to export configuration'); - }); - - unmount(); - }); - - it('displays spinner and status during export', async () => { - const { lastFrame, frames, unmount } = render( - , - ); - - // Check initial loading state - expect(frames[0]).toContain('Exporting environment configuration'); - - // Should show various status messages during export - await vi.waitFor(() => { - expect(lastFrame()).toContain('Export completed successfully!'); - }); - - unmount(); - }); - - it('handles file write errors', async () => { - const mockError = new Error('Permission denied'); - vi.mocked(fs.writeFile).mockRejectedValue(mockError); - - const { lastFrame, unmount } = render( - , - ); - - await vi.waitFor(() => { - expect(lastFrame()).toContain( - 'Failed to export configuration: Permission denied', - ); - }); - - unmount(); - }); - - it('uses auth token when no API key is provided', async () => { - const { lastFrame, unmount } = render(); - - await vi.waitFor(() => { - expect(lastFrame()).toContain('Export completed successfully!'); - }); - - expect(Permit).toHaveBeenCalledWith({ - token: 'mock-auth-token', - pdp: 'http://localhost:7766', - }); - - unmount(); - }); -}); diff --git a/tests/export/ConditionSetGenerator.test.ts b/tests/export/ConditionSetGenerator.test.ts new file mode 100644 index 0000000..ae19016 --- /dev/null +++ b/tests/export/ConditionSetGenerator.test.ts @@ -0,0 +1,70 @@ +import { expect, describe, it, beforeEach, vi } from 'vitest'; +import { ConditionSetGenerator } from '../../source/commands/env/export/generators/ConditionSetGenerator.js'; +import { createWarningCollector } from '../../source/commands/env/export/utils.js'; +import type { Permit } from 'permitio'; + +describe('ConditionSetGenerator', () => { + let generator: ConditionSetGenerator; + let mockPermit: { api: any }; + let warningCollector: ReturnType; + + beforeEach(() => { + mockPermit = { + api: { + conditionSets: { + list: vi.fn().mockResolvedValue([ + { + key: 'us_employees', + name: 'US Employees', + type: 'userset', + description: 'Employees in US', + conditions: { country: 'US' }, + }, + { + key: 'confidential_docs', + name: 'Confidential Documents', + type: 'resourceset', + description: 'Confidential documents', + resource: 'document', + conditions: { classification: 'confidential' }, + }, + ]), + }, + }, + }; + + warningCollector = createWarningCollector(); + generator = new ConditionSetGenerator( + mockPermit as unknown as Permit, + warningCollector, + ); + }); + + it('generates valid HCL for condition sets', async () => { + const hcl = await generator.generateHCL(); + + expect(hcl).toContain('resource "permitio_user_set" "us_employees"'); + expect(hcl).toContain( + 'resource "permitio_resource_set" "confidential_docs"', + ); + expect(hcl).toContain('conditions = {"country":"US"}'); + expect(hcl).toContain('conditions = {"classification":"confidential"}'); + }); + + it('handles empty condition sets', async () => { + mockPermit.api.conditionSets.list.mockResolvedValueOnce([]); + const hcl = await generator.generateHCL(); + expect(hcl).toBe(''); + }); + + it('handles errors and adds warnings', async () => { + mockPermit.api.conditionSets.list.mockRejectedValueOnce( + new Error('API Error'), + ); + const hcl = await generator.generateHCL(); + expect(hcl).toBe(''); + expect(warningCollector.getWarnings()[0]).toContain( + 'Failed to export condition sets', + ); + }); +}); diff --git a/tests/export/ExportContent.test.tsx b/tests/export/ExportContent.test.tsx new file mode 100644 index 0000000..ee5af7c --- /dev/null +++ b/tests/export/ExportContent.test.tsx @@ -0,0 +1,48 @@ +import { expect, vi, describe, it, beforeEach } from 'vitest'; +import React from 'react'; +import { render } from 'ink-testing-library'; +import { ExportContent } from '../../source/commands/env/export/components/ExportContent.js'; +import { getMockPermit, mockValidateApiKeyScope } from './mocks/permit.js'; +import { mockUseAuth } from './mocks/hooks'; + +vi.mock('permitio', () => ({ + Permit: vi.fn(() => { + return getMockPermit(); + }), +})); + +vi.mock('../../source/hooks/useApiKeyApi', () => ({ + useApiKeyApi: () => ({ validateApiKeyScope: mockValidateApiKeyScope }), +})); + +vi.mock('../../source/components/AuthProvider', () => ({ + useAuth: () => mockUseAuth(), +})); + +describe('ExportContent', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('handles successful export to console', async () => { + const { lastFrame } = render( + , + ); + await vi.waitFor(() => { + expect(lastFrame()).toContain('Export completed successfully!'); + }); + }); + + it('handles validation errors', async () => { + mockValidateApiKeyScope.mockResolvedValueOnce({ + valid: false, + error: 'Invalid API key', + }); + const { lastFrame } = render( + , + ); + await vi.waitFor(() => { + expect(lastFrame()).toContain('Invalid API key'); + }); + }); +}); diff --git a/tests/export/ExportStatus.test.tsx b/tests/export/ExportStatus.test.tsx new file mode 100644 index 0000000..b2447eb --- /dev/null +++ b/tests/export/ExportStatus.test.tsx @@ -0,0 +1,34 @@ +import { expect, describe, it } from 'vitest'; +import React from 'react'; +import { render } from 'ink-testing-library'; +import { ExportStatus } from '../../source/commands/env/export/components/ExportStatus.js'; + +describe('ExportStatus', () => { + it('shows loading state', () => { + const { lastFrame } = render( + , + ); + expect(lastFrame()).toContain('Loading...'); + }); + + it('shows error state', () => { + const { lastFrame } = render( + , + ); + expect(lastFrame()).toContain('Failed to export'); + }); +}); diff --git a/tests/export/ResourceGenerator.test.tsx b/tests/export/ResourceGenerator.test.tsx new file mode 100644 index 0000000..fc7a066 --- /dev/null +++ b/tests/export/ResourceGenerator.test.tsx @@ -0,0 +1,98 @@ +import { expect, describe, it, beforeEach } from 'vitest'; +import { RelationGenerator } from '../../source/commands/env/export/generators/RelationGenerator'; +import { getMockPermit } from './mocks/permit'; +import { createWarningCollector } from '../../source/commands/env/export/utils'; + +describe('RelationGenerator', () => { + let generator: RelationGenerator; + + beforeEach(() => { + generator = new RelationGenerator( + getMockPermit(), + createWarningCollector(), + ); + }); + + it('generates empty string when no resources exist', async () => { + const mockPermit = getMockPermit(); + mockPermit.api.resources.list.mockResolvedValueOnce([]); + + const generator = new RelationGenerator(mockPermit, createWarningCollector()); + const result = await generator.generateHCL(); + + // implementation returns an empty string when no resources exist + expect(result).toBe(''); + }); + + + it('skips internal user resource', async () => { + const mockPermit = getMockPermit(); + mockPermit.api.resources.list.mockResolvedValueOnce([ + { key: '__user', relations: [] }, + ]); + + const generator = new RelationGenerator(mockPermit, createWarningCollector()); + const result = await generator.generateHCL(); + + expect(result).toBe('\n# Resource Relations\n'); + }); + + it('generates valid HCL for relations', async () => { + const mockPermit = getMockPermit(); + mockPermit.api.resources.list.mockResolvedValueOnce([ + { + key: 'document', + relations: [ + { + key: 'owner', + name: 'Owner', + object_resource: 'user', + subject_resource: 'document', + }, + ], + }, + ]); + + const generator = new RelationGenerator(mockPermit, createWarningCollector()); + const result = await generator.generateHCL(); + + expect(result).toContain('\n# Resource Relations\n'); + expect(result).toContain('resource "permitio_relation"'); + expect(result).toContain('"document_owner"'); + expect(result).toContain('"owner"'); + }); + + it('handles errors when fetching relations', async () => { + const mockPermit = getMockPermit(); + mockPermit.api.resources.list.mockRejectedValueOnce(new Error('Failed to fetch')); + + const warningCollector = createWarningCollector(); + const generator = new RelationGenerator(mockPermit, warningCollector); + const result = await generator.generateHCL(); + + expect(result).toBe(''); + expect(warningCollector.getWarnings()).toContain('Failed to fetch resource relations: Failed to fetch'); + }); + + it('skips invalid relations', async () => { + const mockPermit = getMockPermit(); + mockPermit.api.resources.list.mockResolvedValueOnce([ + { + key: 'document', + relations: [ + { + + key: 'owner', + }, + ], + }, + ]); + + const warningCollector = createWarningCollector(); + const generator = new RelationGenerator(mockPermit, warningCollector); + const result = await generator.generateHCL(); + + expect(result).toBe('\n# Resource Relations\n'); + expect(warningCollector.getWarnings()).toContain('Invalid relation in resource document: Missing required fields'); + }); +}); \ No newline at end of file diff --git a/tests/export/RoleGenerator.test.tsx b/tests/export/RoleGenerator.test.tsx new file mode 100644 index 0000000..207bd38 --- /dev/null +++ b/tests/export/RoleGenerator.test.tsx @@ -0,0 +1,56 @@ +import { describe, it, expect, vi } from 'vitest'; +import { RoleGenerator } from '../../source/commands/env/export/generators/RoleGenerator.js'; +import { createWarningCollector } from '../../source/commands/env/export/utils'; + +describe('RoleGenerator', () => { + const mockPermit = { + api: { + roles: { + list: vi.fn().mockResolvedValue([ + { + key: 'admin', + name: 'Administrator', + description: 'Admin role', + permissions: ['document:read', 'document:write'], + extends: ['viewer'], + }, + ]), + }, + }, + }; + + it('generates valid HCL for roles', async () => { + const generator = new RoleGenerator( + mockPermit as any, + createWarningCollector(), + ); + const hcl = await generator.generateHCL(); + + expect(hcl).toContain('resource "permitio_role" "admin"'); + expect(hcl).toContain(' key = "admin"'); + expect(hcl).toContain(' name = "Administrator"'); + expect(hcl).toContain(' description = "Admin role"'); + expect(hcl).toContain('["document:read"'); + expect(hcl).toContain('["viewer"]'); + }); + + it('handles API errors gracefully', async () => { + const errorMockPermit = { + api: { + roles: { + list: vi.fn().mockRejectedValue(new Error('API Error')), + }, + }, + }; + const warningCollector = createWarningCollector(); + const generator = new RoleGenerator( + errorMockPermit as any, + warningCollector, + ); + const hcl = await generator.generateHCL(); + expect(hcl).toBe(''); + expect(warningCollector.getWarnings()).toContain( + 'Failed to export roles: Error: API Error', + ); + }); +}); diff --git a/tests/export/UserAttributesGenerator.test.ts b/tests/export/UserAttributesGenerator.test.ts new file mode 100644 index 0000000..3812349 --- /dev/null +++ b/tests/export/UserAttributesGenerator.test.ts @@ -0,0 +1,76 @@ +import { expect, describe, it, beforeEach, vi } from 'vitest'; +import { UserAttributesGenerator } from '../../source/commands/env/export/generators/UserAttributesGenerator.js'; +import { createWarningCollector } from '../../source/commands/env/export/utils.js'; +import type { Permit } from 'permitio'; + +describe('UserAttributesGenerator', () => { + let generator: UserAttributesGenerator; + let mockPermit: { api: any }; + let warningCollector: ReturnType; + + beforeEach(() => { + mockPermit = { + api: { + resourceAttributes: { + list: vi.fn().mockResolvedValue([ + { + key: 'department', + type: 'string', + description: 'User department', + }, + { + key: 'age', + type: 'number', + }, + ]), + }, + }, + }; + + warningCollector = createWarningCollector(); + generator = new UserAttributesGenerator( + mockPermit as unknown as Permit, + warningCollector, + ); + }); + + it('generates valid HCL for user attributes', async () => { + const hcl = await generator.generateHCL(); + + expect(hcl).toContain('resource "permitio_user_attribute" "department"'); + expect(hcl).toContain('resource "permitio_user_attribute" "age"'); + expect(hcl).toContain('type = "string"'); + expect(hcl).toContain('type = "number"'); + expect(hcl).toContain('description = "User department"'); + }); + + it('handles empty attributes list', async () => { + mockPermit.api.resourceAttributes.list.mockResolvedValueOnce([]); + const hcl = await generator.generateHCL(); + expect(hcl).toBe(''); + }); + + it('generates HCL without description for attributes missing it', async () => { + mockPermit.api.resourceAttributes.list.mockResolvedValueOnce([ + { + key: 'simple', + type: 'string', + }, + ]); + + const hcl = await generator.generateHCL(); + expect(hcl).toContain('resource "permitio_user_attribute" "simple"'); + expect(hcl).not.toContain('description'); + }); + + it('handles API errors', async () => { + mockPermit.api.resourceAttributes.list.mockRejectedValueOnce( + new Error('API Error'), + ); + const hcl = await generator.generateHCL(); + expect(hcl).toBe(''); + expect(warningCollector.getWarnings()[0]).toContain( + 'Failed to export user attributes', + ); + }); +}); diff --git a/tests/export/generators.test.tsx b/tests/export/generators.test.tsx new file mode 100644 index 0000000..b94de3d --- /dev/null +++ b/tests/export/generators.test.tsx @@ -0,0 +1,55 @@ +import { expect, describe, it, beforeEach } from 'vitest'; +import { Permit } from 'permitio'; +import { ResourceGenerator } from '../../source/commands/env/export/generators/ResourceGenerator'; +import { RoleGenerator } from '../../source/commands/env/export/generators/RoleGenerator'; +import { UserAttributesGenerator } from '../../source/commands/env/export/generators/UserAttributesGenerator'; +import { + getMockPermit, + mockResources, + mockRoles, + mockUserAttributes, +} from './mocks/permit'; +import { createWarningCollector } from '../../source/commands/env/export/utils'; + +describe('Generators', () => { + let permit: Permit; + let warningCollector: ReturnType; + + beforeEach(() => { + permit = getMockPermit() as any; + warningCollector = createWarningCollector(); + }); + + describe('ResourceGenerator', () => { + it('generates valid HCL for resources', async () => { + const generator = new ResourceGenerator(permit, warningCollector); + const hcl = await generator.generateHCL(); + + expect(hcl).toContain('resource "permitio_resource"'); + expect(hcl).toContain(mockResources[0].key); + expect(hcl).toContain(mockResources[0].name); + }); + }); + + describe('RoleGenerator', () => { + it('generates valid HCL for roles', async () => { + const generator = new RoleGenerator(permit, warningCollector); + const hcl = await generator.generateHCL(); + + expect(hcl).toContain('resource "permitio_role"'); + expect(hcl).toContain(mockRoles[0].key); + expect(hcl).toContain(mockRoles[0].name); + }); + }); + + describe('UserAttributesGenerator', () => { + it('generates valid HCL for user attributes', async () => { + const generator = new UserAttributesGenerator(permit, warningCollector); + const hcl = await generator.generateHCL(); + + expect(hcl).toContain('resource "permitio_user_attribute"'); + expect(hcl).toContain(mockUserAttributes[0].key); + expect(hcl).toContain(mockUserAttributes[0].type); + }); + }); +}); diff --git a/tests/export/index.test.tsx b/tests/export/index.test.tsx new file mode 100644 index 0000000..d761278 --- /dev/null +++ b/tests/export/index.test.tsx @@ -0,0 +1,19 @@ +import { expect, vi, describe, it } from 'vitest'; +import React from 'react'; +import { render } from 'ink-testing-library'; +import Export from '../../source/commands/env/export/index.js'; +import { AuthProvider } from '../../source/components/AuthProvider.js'; + +// Test the main Export component +describe('Export Command', () => { + it('renders with AuthProvider', () => { + const { lastFrame } = render(); + expect(lastFrame()).toBeTruthy(); + }); + + it('passes options correctly', () => { + const options = { key: 'test-key', file: 'output.tf' }; + const { lastFrame } = render(); + expect(lastFrame()).toBeTruthy(); + }); +}); diff --git a/tests/export/mocks/hooks.tsx b/tests/export/mocks/hooks.tsx new file mode 100644 index 0000000..973c16c --- /dev/null +++ b/tests/export/mocks/hooks.tsx @@ -0,0 +1,5 @@ +import { vi } from 'vitest'; + +export const mockUseAuth = vi.fn(() => ({ + authToken: 'mock-auth-token', +})); diff --git a/tests/export/mocks/permit.ts b/tests/export/mocks/permit.ts new file mode 100644 index 0000000..3c51ef0 --- /dev/null +++ b/tests/export/mocks/permit.ts @@ -0,0 +1,59 @@ +import { vi } from 'vitest'; +export const mockResources = [ + { + key: 'document', + name: 'Document', + actions: { read: { name: 'Read' } }, + }, +]; +export const mockRoles = [ + { + key: 'admin', + name: 'Administrator', + permissions: ['document:read'], + }, +]; +export const mockUserAttributes = [ + { + key: 'department', + type: 'string', + description: 'User department', + }, +]; +export const getMockPermit = () => ({ + api: { + resources: { + list: vi.fn().mockResolvedValue(mockResources), + }, + roles: { + list: vi.fn().mockResolvedValue(mockRoles), + }, + resourceAttributes: { + list: vi.fn().mockImplementation(({ resourceKey }) => { + if (resourceKey === '__user') { + return Promise.resolve(mockUserAttributes); + } + return Promise.resolve([]); + }), + }, + + users: { + list: vi.fn().mockResolvedValue([]), + }, + conditionSets: { + list: vi.fn().mockResolvedValue([]), + }, + relationshipTuples: { + list: vi.fn().mockResolvedValue([]), + }, + }, +}); + +export const mockValidateApiKeyScope = vi.fn().mockResolvedValue({ + valid: true, + scope: { + organization_id: 'org-123', + project_id: 'proj-123', + environment_id: 'env-123', + }, +}); diff --git a/tests/export/utils.test.tsx b/tests/export/utils.test.tsx new file mode 100644 index 0000000..e0834a4 --- /dev/null +++ b/tests/export/utils.test.tsx @@ -0,0 +1,22 @@ +import { expect, describe, it } from 'vitest'; +import { + createSafeId, + createWarningCollector, + generateProviderBlock, +} from '../../source/commands/env/export/utils.js'; + +describe('Export Utils', () => { + describe('createSafeId', () => { + it('creates safe identifier', () => { + expect(createSafeId('test-id')).toBe('test_id'); + }); + }); + + describe('createWarningCollector', () => { + it('collects warnings', () => { + const collector = createWarningCollector(); + collector.addWarning('test warning'); + expect(collector.getWarnings()).toContain('test warning'); + }); + }); +}); From 8239bffe82cc35bb370fa352f69ed2d051c4847d Mon Sep 17 00:00:00 2001 From: daveads Date: Wed, 18 Dec 2024 09:17:53 +0100 Subject: [PATCH 06/34] format ResourceGenerator.test.tsxr --- tests/export/ResourceGenerator.test.tsx | 197 +++++++++++++----------- 1 file changed, 105 insertions(+), 92 deletions(-) diff --git a/tests/export/ResourceGenerator.test.tsx b/tests/export/ResourceGenerator.test.tsx index fc7a066..aac6b87 100644 --- a/tests/export/ResourceGenerator.test.tsx +++ b/tests/export/ResourceGenerator.test.tsx @@ -4,95 +4,108 @@ import { getMockPermit } from './mocks/permit'; import { createWarningCollector } from '../../source/commands/env/export/utils'; describe('RelationGenerator', () => { - let generator: RelationGenerator; - - beforeEach(() => { - generator = new RelationGenerator( - getMockPermit(), - createWarningCollector(), - ); - }); - - it('generates empty string when no resources exist', async () => { - const mockPermit = getMockPermit(); - mockPermit.api.resources.list.mockResolvedValueOnce([]); - - const generator = new RelationGenerator(mockPermit, createWarningCollector()); - const result = await generator.generateHCL(); - - // implementation returns an empty string when no resources exist - expect(result).toBe(''); - }); - - - it('skips internal user resource', async () => { - const mockPermit = getMockPermit(); - mockPermit.api.resources.list.mockResolvedValueOnce([ - { key: '__user', relations: [] }, - ]); - - const generator = new RelationGenerator(mockPermit, createWarningCollector()); - const result = await generator.generateHCL(); - - expect(result).toBe('\n# Resource Relations\n'); - }); - - it('generates valid HCL for relations', async () => { - const mockPermit = getMockPermit(); - mockPermit.api.resources.list.mockResolvedValueOnce([ - { - key: 'document', - relations: [ - { - key: 'owner', - name: 'Owner', - object_resource: 'user', - subject_resource: 'document', - }, - ], - }, - ]); - - const generator = new RelationGenerator(mockPermit, createWarningCollector()); - const result = await generator.generateHCL(); - - expect(result).toContain('\n# Resource Relations\n'); - expect(result).toContain('resource "permitio_relation"'); - expect(result).toContain('"document_owner"'); - expect(result).toContain('"owner"'); - }); - - it('handles errors when fetching relations', async () => { - const mockPermit = getMockPermit(); - mockPermit.api.resources.list.mockRejectedValueOnce(new Error('Failed to fetch')); - - const warningCollector = createWarningCollector(); - const generator = new RelationGenerator(mockPermit, warningCollector); - const result = await generator.generateHCL(); - - expect(result).toBe(''); - expect(warningCollector.getWarnings()).toContain('Failed to fetch resource relations: Failed to fetch'); - }); - - it('skips invalid relations', async () => { - const mockPermit = getMockPermit(); - mockPermit.api.resources.list.mockResolvedValueOnce([ - { - key: 'document', - relations: [ - { - - key: 'owner', - }, - ], - }, - ]); - - const warningCollector = createWarningCollector(); - const generator = new RelationGenerator(mockPermit, warningCollector); - const result = await generator.generateHCL(); - - expect(result).toBe('\n# Resource Relations\n'); - expect(warningCollector.getWarnings()).toContain('Invalid relation in resource document: Missing required fields'); - }); -}); \ No newline at end of file + let generator: RelationGenerator; + + beforeEach(() => { + generator = new RelationGenerator( + getMockPermit(), + createWarningCollector(), + ); + }); + + it('generates empty string when no resources exist', async () => { + const mockPermit = getMockPermit(); + mockPermit.api.resources.list.mockResolvedValueOnce([]); + + const generator = new RelationGenerator( + mockPermit, + createWarningCollector(), + ); + const result = await generator.generateHCL(); + + // implementation returns an empty string when no resources exist + expect(result).toBe(''); + }); + + it('skips internal user resource', async () => { + const mockPermit = getMockPermit(); + mockPermit.api.resources.list.mockResolvedValueOnce([ + { key: '__user', relations: [] }, + ]); + + const generator = new RelationGenerator( + mockPermit, + createWarningCollector(), + ); + const result = await generator.generateHCL(); + + expect(result).toBe('\n# Resource Relations\n'); + }); + + it('generates valid HCL for relations', async () => { + const mockPermit = getMockPermit(); + mockPermit.api.resources.list.mockResolvedValueOnce([ + { + key: 'document', + relations: [ + { + key: 'owner', + name: 'Owner', + object_resource: 'user', + subject_resource: 'document', + }, + ], + }, + ]); + + const generator = new RelationGenerator( + mockPermit, + createWarningCollector(), + ); + const result = await generator.generateHCL(); + + expect(result).toContain('\n# Resource Relations\n'); + expect(result).toContain('resource "permitio_relation"'); + expect(result).toContain('"document_owner"'); + expect(result).toContain('"owner"'); + }); + + it('handles errors when fetching relations', async () => { + const mockPermit = getMockPermit(); + mockPermit.api.resources.list.mockRejectedValueOnce( + new Error('Failed to fetch'), + ); + + const warningCollector = createWarningCollector(); + const generator = new RelationGenerator(mockPermit, warningCollector); + const result = await generator.generateHCL(); + + expect(result).toBe(''); + expect(warningCollector.getWarnings()).toContain( + 'Failed to fetch resource relations: Failed to fetch', + ); + }); + + it('skips invalid relations', async () => { + const mockPermit = getMockPermit(); + mockPermit.api.resources.list.mockResolvedValueOnce([ + { + key: 'document', + relations: [ + { + key: 'owner', + }, + ], + }, + ]); + + const warningCollector = createWarningCollector(); + const generator = new RelationGenerator(mockPermit, warningCollector); + const result = await generator.generateHCL(); + + expect(result).toBe('\n# Resource Relations\n'); + expect(warningCollector.getWarnings()).toContain( + 'Invalid relation in resource document: Missing required fields', + ); + }); +}); From 5eeae2ee222238452ed83d30d7b9e31bcf0c7a34 Mon Sep 17 00:00:00 2001 From: daveads Date: Sun, 29 Dec 2024 17:43:51 +0100 Subject: [PATCH 07/34] lint --- eslint.config.js | 1 + tsconfig.json | 1 + 2 files changed, 2 insertions(+) diff --git a/eslint.config.js b/eslint.config.js index f92e870..3478a29 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -35,6 +35,7 @@ export default [ RequestInit: 'readonly', fetch: 'readonly', process: 'readonly', + console: true, }, }, }, diff --git a/tsconfig.json b/tsconfig.json index 1438861..bc4624c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "jsx": "react", "moduleResolution": "node16", "module": "Node16", + "strict": true, "outDir": "dist" }, "include": ["source"], From 7709b39a9e0addf57d293bb1dfe716f06b3eea81 Mon Sep 17 00:00:00 2001 From: daveads Date: Sun, 29 Dec 2024 18:18:57 +0100 Subject: [PATCH 08/34] run build error --- .../export/generators/ResourceGenerator.ts | 61 +++++++++++-------- .../env/export/generators/RoleGenerator.ts | 18 +++--- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/source/commands/env/export/generators/ResourceGenerator.ts b/source/commands/env/export/generators/ResourceGenerator.ts index 5ee0abd..9610825 100644 --- a/source/commands/env/export/generators/ResourceGenerator.ts +++ b/source/commands/env/export/generators/ResourceGenerator.ts @@ -11,55 +11,64 @@ export class ResourceGenerator implements HCLGenerator { ) {} async generateHCL(): Promise { - const resources = await this.permit.api.resources.list(); - const validResources = resources.filter( - resource => resource.key !== '__user', - ); + try { + const resources = await this.permit.api.resources.list(); + const validResources = resources.filter( + (resource) => resource.key !== '__user' + ); - if (validResources.length === 0) return ''; + if (validResources.length === 0) return ''; - let hcl = '\n# Resources\n'; + let hcl = '\n# Resources\n'; - for (const resource of validResources) { - hcl += `resource "permitio_resource" "${createSafeId(resource.key)}" { + for (const resource of validResources) { + hcl += `resource "permitio_resource" "${createSafeId(resource.key)}" { key = "${resource.key}" name = "${resource.name}"${ resource.description ? `\n description = "${resource.description}"` : '' }${resource.urn ? `\n urn = "${resource.urn}"` : ''} - actions = {${Object.entries(resource.actions) - .map( - ([actionKey, action]) => ` - "${actionKey}" = { - name = "${action.name}"${ - action.description - ? `\n description = "${action.description}"` - : '' - } - }`, - ) - .join('')} - }${this.generateAttributes(resource.attributes)} + actions = {${this.generateActions(resource.actions || {})}} + ${this.generateAttributes(resource.attributes)} }\n`; + } + + return hcl; + } catch (error) { + this.warningCollector.addWarning(`Failed to export resources: ${error}`); + return ''; } + } - return hcl; + private generateActions(actions: Record): string { + if (Object.keys(actions).length === 0) return ''; + + return Object.entries(actions) + .map( + ([actionKey, action]) => ` + "${actionKey}" = { + name = "${action.name}"${ + action.description ? `\n description = "${action.description}"` : '' + } + }` + ) + .join(''); } private generateAttributes( - attributes: Record | undefined, + attributes: Record | undefined ): string { if (!attributes || Object.keys(attributes).length === 0) return ''; - return `\n attributes = {${Object.entries(attributes) + return `attributes = {${Object.entries(attributes) .map( ([attrKey, attr]) => ` "${attrKey}" = { type = "${attr.type}"${ attr.description ? `\n description = "${attr.description}"` : '' } - }`, + }` ) .join('')} }`; } -} +} \ No newline at end of file diff --git a/source/commands/env/export/generators/RoleGenerator.ts b/source/commands/env/export/generators/RoleGenerator.ts index 4437027..4f2fca3 100644 --- a/source/commands/env/export/generators/RoleGenerator.ts +++ b/source/commands/env/export/generators/RoleGenerator.ts @@ -20,16 +20,12 @@ export class RoleGenerator implements HCLGenerator { hcl += `resource "permitio_role" "${createSafeId(role.key)}" { key = "${role.key}" name = "${role.name}"${ - role.description ? `\n description = "${role.description}"` : '' - }${ - role.permissions && role.permissions.length > 0 - ? `\n permissions = ${JSON.stringify(role.permissions)}` - : '' - }${ - role.extends && role.extends.length > 0 - ? `\n extends = ${JSON.stringify(role.extends)}` - : '' - } + role.description ? `\n description = "${role.description}"` : '' + }${ + role.permissions && role.permissions.length > 0 + ? `\n permissions = ${JSON.stringify(role.permissions)}` + : '' + } }\n`; } return hcl; @@ -38,4 +34,4 @@ export class RoleGenerator implements HCLGenerator { return ''; } } -} +} \ No newline at end of file From 9d2b9c02d3d68804b05e2f5827b6087592b56c07 Mon Sep 17 00:00:00 2001 From: daveads Date: Sun, 29 Dec 2024 18:21:46 +0100 Subject: [PATCH 09/34] format --- .../env/export/generators/ResourceGenerator.ts | 14 ++++++++------ .../env/export/generators/RoleGenerator.ts | 14 +++++++------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/source/commands/env/export/generators/ResourceGenerator.ts b/source/commands/env/export/generators/ResourceGenerator.ts index 9610825..2aab0d9 100644 --- a/source/commands/env/export/generators/ResourceGenerator.ts +++ b/source/commands/env/export/generators/ResourceGenerator.ts @@ -14,7 +14,7 @@ export class ResourceGenerator implements HCLGenerator { try { const resources = await this.permit.api.resources.list(); const validResources = resources.filter( - (resource) => resource.key !== '__user' + resource => resource.key !== '__user', ); if (validResources.length === 0) return ''; @@ -47,15 +47,17 @@ export class ResourceGenerator implements HCLGenerator { ([actionKey, action]) => ` "${actionKey}" = { name = "${action.name}"${ - action.description ? `\n description = "${action.description}"` : '' + action.description + ? `\n description = "${action.description}"` + : '' } - }` + }`, ) .join(''); } private generateAttributes( - attributes: Record | undefined + attributes: Record | undefined, ): string { if (!attributes || Object.keys(attributes).length === 0) return ''; @@ -66,9 +68,9 @@ export class ResourceGenerator implements HCLGenerator { type = "${attr.type}"${ attr.description ? `\n description = "${attr.description}"` : '' } - }` + }`, ) .join('')} }`; } -} \ No newline at end of file +} diff --git a/source/commands/env/export/generators/RoleGenerator.ts b/source/commands/env/export/generators/RoleGenerator.ts index 4f2fca3..ecad746 100644 --- a/source/commands/env/export/generators/RoleGenerator.ts +++ b/source/commands/env/export/generators/RoleGenerator.ts @@ -20,12 +20,12 @@ export class RoleGenerator implements HCLGenerator { hcl += `resource "permitio_role" "${createSafeId(role.key)}" { key = "${role.key}" name = "${role.name}"${ - role.description ? `\n description = "${role.description}"` : '' - }${ - role.permissions && role.permissions.length > 0 - ? `\n permissions = ${JSON.stringify(role.permissions)}` - : '' - } + role.description ? `\n description = "${role.description}"` : '' + }${ + role.permissions && role.permissions.length > 0 + ? `\n permissions = ${JSON.stringify(role.permissions)}` + : '' + } }\n`; } return hcl; @@ -34,4 +34,4 @@ export class RoleGenerator implements HCLGenerator { return ''; } } -} \ No newline at end of file +} From ca44af2435cb16514728cfe2cb9677223102814b Mon Sep 17 00:00:00 2001 From: daveads Date: Sun, 29 Dec 2024 18:27:10 +0100 Subject: [PATCH 10/34] any >> --- .../env/export/components/hooks/useExport.ts | 9 ++++++++- .../env/export/generators/ResourceGenerator.ts | 15 +++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/source/commands/env/export/components/hooks/useExport.ts b/source/commands/env/export/components/hooks/useExport.ts index 028ebb3..162b991 100644 --- a/source/commands/env/export/components/hooks/useExport.ts +++ b/source/commands/env/export/components/hooks/useExport.ts @@ -8,6 +8,13 @@ import { UserAttributesGenerator } from '../../generators/UserAttributesGenerato import { RelationGenerator } from '../../generators/RelationGenerator.js'; import { ConditionSetGenerator } from '../../generators/ConditionSetGenerator.js'; +// Define a type for the `scope` parameter +interface ExportScope { + environment_id?: string; + project_id?: string; + organization_id?: string; +} + export const useExport = (apiKey: string) => { const [state, setState] = useState({ status: '', @@ -18,7 +25,7 @@ export const useExport = (apiKey: string) => { const permit = PermitSDK(apiKey); - const exportConfig = async (scope: any) => { + const exportConfig = async (scope: ExportScope) => { try { const warningCollector = createWarningCollector(); diff --git a/source/commands/env/export/generators/ResourceGenerator.ts b/source/commands/env/export/generators/ResourceGenerator.ts index 2aab0d9..3bdeebc 100644 --- a/source/commands/env/export/generators/ResourceGenerator.ts +++ b/source/commands/env/export/generators/ResourceGenerator.ts @@ -2,6 +2,17 @@ import { Permit } from 'permitio'; import { HCLGenerator, WarningCollector } from '../types.js'; import { createSafeId } from '../utils.js'; +// Define types for actions and attributes +interface Action { + name: string; + description?: string; +} + +interface Attribute { + type: string; + description?: string; +} + export class ResourceGenerator implements HCLGenerator { name = 'resources'; @@ -39,7 +50,7 @@ export class ResourceGenerator implements HCLGenerator { } } - private generateActions(actions: Record): string { + private generateActions(actions: Record): string { if (Object.keys(actions).length === 0) return ''; return Object.entries(actions) @@ -57,7 +68,7 @@ export class ResourceGenerator implements HCLGenerator { } private generateAttributes( - attributes: Record | undefined, + attributes: Record | undefined, ): string { if (!attributes || Object.keys(attributes).length === 0) return ''; From de8b8b97df900ccefd2d8bacf78d334fd155aa72 Mon Sep 17 00:00:00 2001 From: daveads Date: Sun, 29 Dec 2024 20:55:22 +0100 Subject: [PATCH 11/34] .. --- .../env/export/components/ExportContent.tsx | 13 ++++++++++--- .../env/export/generators/ResourceGenerator.ts | 13 +++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/source/commands/env/export/components/ExportContent.tsx b/source/commands/env/export/components/ExportContent.tsx index 5697534..478be3d 100644 --- a/source/commands/env/export/components/ExportContent.tsx +++ b/source/commands/env/export/components/ExportContent.tsx @@ -36,16 +36,23 @@ export const ExportContent: React.FC<{ options: ExportOptions }> = ({ scope, } = await validateApiKeyScope(key, 'environment'); - if (!valid || scopeError) { + if (!valid || scopeError || !scope) { setState({ status: '', - error: `Invalid API key: ${scopeError}`, + error: `Invalid API key: ${scopeError || 'No scope found'}`, isComplete: true, warnings: [], }); return; } + // Normalize the environment_id and project_id to match ExportScope + const normalizedScope = { + ...scope, + environment_id: scope.environment_id || undefined, + project_id: scope.project_id || undefined, + }; + if (!isSubscribed) return; setState(prev => ({ @@ -53,7 +60,7 @@ export const ExportContent: React.FC<{ options: ExportOptions }> = ({ status: 'Initializing export...', })); - const { hcl, warnings } = await exportConfig(scope); + const { hcl, warnings } = await exportConfig(normalizedScope); if (!isSubscribed) return; diff --git a/source/commands/env/export/generators/ResourceGenerator.ts b/source/commands/env/export/generators/ResourceGenerator.ts index 3bdeebc..2cec986 100644 --- a/source/commands/env/export/generators/ResourceGenerator.ts +++ b/source/commands/env/export/generators/ResourceGenerator.ts @@ -2,12 +2,7 @@ import { Permit } from 'permitio'; import { HCLGenerator, WarningCollector } from '../types.js'; import { createSafeId } from '../utils.js'; -// Define types for actions and attributes -interface Action { - name: string; - description?: string; -} - +// Define types for attributes interface Attribute { type: string; description?: string; @@ -50,14 +45,16 @@ export class ResourceGenerator implements HCLGenerator { } } - private generateActions(actions: Record): string { + private generateActions( + actions: Record, + ): string { if (Object.keys(actions).length === 0) return ''; return Object.entries(actions) .map( ([actionKey, action]) => ` "${actionKey}" = { - name = "${action.name}"${ + name = "${action.name || 'Unnamed Action'}"${ action.description ? `\n description = "${action.description}"` : '' From 2478730fe7c5cdb91922c0ab4b88fd419bbf355f Mon Sep 17 00:00:00 2001 From: daveads Date: Sun, 29 Dec 2024 21:25:04 +0100 Subject: [PATCH 12/34] test --- tests/export/ResourceGenerator.test.tsx | 87 ++++++++++--------------- tests/export/RoleGenerator.test.tsx | 8 +-- 2 files changed, 40 insertions(+), 55 deletions(-) diff --git a/tests/export/ResourceGenerator.test.tsx b/tests/export/ResourceGenerator.test.tsx index aac6b87..f5eb8f2 100644 --- a/tests/export/ResourceGenerator.test.tsx +++ b/tests/export/ResourceGenerator.test.tsx @@ -1,13 +1,13 @@ import { expect, describe, it, beforeEach } from 'vitest'; -import { RelationGenerator } from '../../source/commands/env/export/generators/RelationGenerator'; +import { ResourceGenerator } from '../../source/commands/env/export/generators/ResourceGenerator'; import { getMockPermit } from './mocks/permit'; import { createWarningCollector } from '../../source/commands/env/export/utils'; -describe('RelationGenerator', () => { - let generator: RelationGenerator; +describe('ResourceGenerator', () => { + let generator: ResourceGenerator; beforeEach(() => { - generator = new RelationGenerator( + generator = new ResourceGenerator( getMockPermit(), createWarningCollector(), ); @@ -17,95 +17,80 @@ describe('RelationGenerator', () => { const mockPermit = getMockPermit(); mockPermit.api.resources.list.mockResolvedValueOnce([]); - const generator = new RelationGenerator( + const generator = new ResourceGenerator( mockPermit, createWarningCollector(), ); const result = await generator.generateHCL(); - // implementation returns an empty string when no resources exist expect(result).toBe(''); }); it('skips internal user resource', async () => { const mockPermit = getMockPermit(); mockPermit.api.resources.list.mockResolvedValueOnce([ - { key: '__user', relations: [] }, + { key: '__user', name: 'User', actions: {}, attributes: {} }, ]); - const generator = new RelationGenerator( + const generator = new ResourceGenerator( mockPermit, createWarningCollector(), ); const result = await generator.generateHCL(); - expect(result).toBe('\n# Resource Relations\n'); + expect(result).toBe(''); }); - it('generates valid HCL for relations', async () => { + it('generates valid HCL for resources', async () => { const mockPermit = getMockPermit(); mockPermit.api.resources.list.mockResolvedValueOnce([ { key: 'document', - relations: [ - { - key: 'owner', - name: 'Owner', - object_resource: 'user', - subject_resource: 'document', - }, - ], + name: 'Document', + description: 'A document resource', + actions: { + read: { name: 'Read', description: 'Read the document' }, + }, + attributes: { + owner: { type: 'string', description: 'The owner of the document' }, + }, }, ]); - const generator = new RelationGenerator( + const generator = new ResourceGenerator( mockPermit, createWarningCollector(), ); const result = await generator.generateHCL(); - expect(result).toContain('\n# Resource Relations\n'); - expect(result).toContain('resource "permitio_relation"'); - expect(result).toContain('"document_owner"'); - expect(result).toContain('"owner"'); + expect(result).toContain('\n# Resources\n'); + expect(result).toContain('resource "permitio_resource" "document"'); + expect(result).toContain('key = "document"'); + expect(result).toContain('name = "Document"'); + expect(result).toContain('description = "A document resource"'); + expect(result).toContain('actions = {'); + expect(result).toContain('"read" = {'); + expect(result).toContain('name = "Read"'); + expect(result).toContain('description = "Read the document"'); + expect(result).toContain('attributes = {'); + expect(result).toContain('"owner" = {'); + expect(result).toContain('type = "string"'); + expect(result).toContain('description = "The owner of the document"'); }); - it('handles errors when fetching relations', async () => { + it('handles errors when fetching resources', async () => { const mockPermit = getMockPermit(); mockPermit.api.resources.list.mockRejectedValueOnce( new Error('Failed to fetch'), ); const warningCollector = createWarningCollector(); - const generator = new RelationGenerator(mockPermit, warningCollector); + const generator = new ResourceGenerator(mockPermit, warningCollector); const result = await generator.generateHCL(); expect(result).toBe(''); - expect(warningCollector.getWarnings()).toContain( - 'Failed to fetch resource relations: Failed to fetch', - ); - }); - - it('skips invalid relations', async () => { - const mockPermit = getMockPermit(); - mockPermit.api.resources.list.mockResolvedValueOnce([ - { - key: 'document', - relations: [ - { - key: 'owner', - }, - ], - }, + expect(warningCollector.getWarnings()).toEqual([ + 'Failed to export resources: Error: Failed to fetch', ]); - - const warningCollector = createWarningCollector(); - const generator = new RelationGenerator(mockPermit, warningCollector); - const result = await generator.generateHCL(); - - expect(result).toBe('\n# Resource Relations\n'); - expect(warningCollector.getWarnings()).toContain( - 'Invalid relation in resource document: Missing required fields', - ); }); -}); +}); \ No newline at end of file diff --git a/tests/export/RoleGenerator.test.tsx b/tests/export/RoleGenerator.test.tsx index 207bd38..e55fd88 100644 --- a/tests/export/RoleGenerator.test.tsx +++ b/tests/export/RoleGenerator.test.tsx @@ -12,7 +12,7 @@ describe('RoleGenerator', () => { name: 'Administrator', description: 'Admin role', permissions: ['document:read', 'document:write'], - extends: ['viewer'], + extends: ['viewer'], // This will be ignored in the HCL output }, ]), }, @@ -30,8 +30,8 @@ describe('RoleGenerator', () => { expect(hcl).toContain(' key = "admin"'); expect(hcl).toContain(' name = "Administrator"'); expect(hcl).toContain(' description = "Admin role"'); - expect(hcl).toContain('["document:read"'); - expect(hcl).toContain('["viewer"]'); + expect(hcl).toContain('["document:read","document:write"]'); + expect(hcl).not.toContain('["viewer"]'); // Ensure "viewer" is not included }); it('handles API errors gracefully', async () => { @@ -53,4 +53,4 @@ describe('RoleGenerator', () => { 'Failed to export roles: Error: API Error', ); }); -}); +}); \ No newline at end of file From cc061d9e42962242b0da47bf29736bc70a1a3808 Mon Sep 17 00:00:00 2001 From: daveads Date: Mon, 30 Dec 2024 23:58:09 +0100 Subject: [PATCH 13/34] hooks, components --- source/commands/env/export/index.tsx | 2 +- .../export}/ExportContent.tsx | 9 +++++---- .../export}/ExportStatus.tsx | 2 +- .../components/hooks => hooks/export}/PermitSDK.ts | 0 .../components/hooks => hooks/export}/useExport.ts | 14 +++++++------- 5 files changed, 14 insertions(+), 13 deletions(-) rename source/{commands/env/export/components => components/export}/ExportContent.tsx (91%) rename source/{commands/env/export/components => components/export}/ExportStatus.tsx (94%) rename source/{commands/env/export/components/hooks => hooks/export}/PermitSDK.ts (100%) rename source/{commands/env/export/components/hooks => hooks/export}/useExport.ts (73%) diff --git a/source/commands/env/export/index.tsx b/source/commands/env/export/index.tsx index 6d965a7..606dc83 100644 --- a/source/commands/env/export/index.tsx +++ b/source/commands/env/export/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { option } from 'pastel'; import zod from 'zod'; import { AuthProvider } from '../../../components/AuthProvider.js'; -import { ExportContent } from './components/ExportContent.js'; +import { ExportContent } from '../../../components/export/ExportContent.js'; export const options = zod.object({ key: zod diff --git a/source/commands/env/export/components/ExportContent.tsx b/source/components/export/ExportContent.tsx similarity index 91% rename from source/commands/env/export/components/ExportContent.tsx rename to source/components/export/ExportContent.tsx index 478be3d..7e83420 100644 --- a/source/commands/env/export/components/ExportContent.tsx +++ b/source/components/export/ExportContent.tsx @@ -1,9 +1,10 @@ import React from 'react'; -import { useApiKeyApi } from '../../../../hooks/useApiKeyApi.js'; -import { useAuth } from '../../../../components/AuthProvider.js'; -import { ExportOptions } from '../types.js'; +import { useApiKeyApi } from '../../hooks/useApiKeyApi.js'; +import { useAuth } from '../../components/AuthProvider.js'; +import { ExportOptions } from '../../commands/env/export/types.js' import { ExportStatus } from './ExportStatus.js'; -import { useExport } from './hooks/useExport.js'; +import { useExport } from '../../hooks/export/useExport.js'; + import fs from 'node:fs/promises'; export const ExportContent: React.FC<{ options: ExportOptions }> = ({ diff --git a/source/commands/env/export/components/ExportStatus.tsx b/source/components/export/ExportStatus.tsx similarity index 94% rename from source/commands/env/export/components/ExportStatus.tsx rename to source/components/export/ExportStatus.tsx index 37360cd..df524c0 100644 --- a/source/commands/env/export/components/ExportStatus.tsx +++ b/source/components/export/ExportStatus.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Text } from 'ink'; import Spinner from 'ink-spinner'; -import { ExportState } from '../types.js'; +import { ExportState } from '../../commands/env/export/types.js' interface ExportStatusProps { state: ExportState; diff --git a/source/commands/env/export/components/hooks/PermitSDK.ts b/source/hooks/export/PermitSDK.ts similarity index 100% rename from source/commands/env/export/components/hooks/PermitSDK.ts rename to source/hooks/export/PermitSDK.ts diff --git a/source/commands/env/export/components/hooks/useExport.ts b/source/hooks/export/useExport.ts similarity index 73% rename from source/commands/env/export/components/hooks/useExport.ts rename to source/hooks/export/useExport.ts index 162b991..778a6f6 100644 --- a/source/commands/env/export/components/hooks/useExport.ts +++ b/source/hooks/export/useExport.ts @@ -1,12 +1,12 @@ import { useState } from 'react'; import { PermitSDK } from './PermitSDK.js'; -import { ExportState } from '../../types.js'; -import { createWarningCollector, generateProviderBlock } from '../../utils.js'; -import { ResourceGenerator } from '../../generators/ResourceGenerator.js'; -import { RoleGenerator } from '../../generators/RoleGenerator.js'; -import { UserAttributesGenerator } from '../../generators/UserAttributesGenerator.js'; -import { RelationGenerator } from '../../generators/RelationGenerator.js'; -import { ConditionSetGenerator } from '../../generators/ConditionSetGenerator.js'; +import { ExportState } from '../../commands/env/export/types.js'; +import { createWarningCollector, generateProviderBlock } from '../../commands/env/export/utils.js'; +import { ResourceGenerator } from '../../commands/env/export/generators/ResourceGenerator.js'; +import { RoleGenerator } from '../../commands/env/export/generators/RoleGenerator.js'; +import { UserAttributesGenerator } from '../../commands/env/export/generators/UserAttributesGenerator.js'; +import { RelationGenerator } from '../../commands/env/export/generators/RelationGenerator.js'; +import { ConditionSetGenerator } from '../../commands/env/export/generators/ConditionSetGenerator.js'; // Define a type for the `scope` parameter interface ExportScope { From 19ef6c2783018758ade02c316291584758c81f63 Mon Sep 17 00:00:00 2001 From: daveads Date: Tue, 31 Dec 2024 06:57:51 +0100 Subject: [PATCH 14/34] (refactor) move HCL generation to Handlebars template --- package-lock.json | 56 ++++++++ package.json | 1 + .../generators/ConditionSetGenerator.ts | 122 ++++++++++-------- .../export/generators/RelationGenerator.ts | 57 ++++---- .../export/generators/ResourceGenerator.ts | 121 +++++++---------- .../env/export/generators/RoleGenerator.ts | 77 ++++++----- .../generators/UserAttributesGenerator.ts | 68 +++++----- .../env/export/templates/condition-set.hcl | 13 ++ .../env/export/templates/relation.hcl | 11 ++ .../env/export/templates/resource.hcl | 38 ++++++ source/commands/env/export/templates/role.hcl | 9 ++ .../env/export/templates/user-attribute.hcl | 9 ++ 12 files changed, 354 insertions(+), 228 deletions(-) create mode 100644 source/commands/env/export/templates/condition-set.hcl create mode 100644 source/commands/env/export/templates/relation.hcl create mode 100644 source/commands/env/export/templates/resource.hcl create mode 100644 source/commands/env/export/templates/role.hcl create mode 100644 source/commands/env/export/templates/user-attribute.hcl diff --git a/package-lock.json b/package-lock.json index 687270f..ce9bf6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "clipboardy": "^4.0.0", "delay": "^6.0.0", "fuse.js": "^7.0.0", + "handlebars": "^4.7.8", "ink": "^5.1.0", "ink-ascii": "^0.0.4", "ink-big-text": "^2.0.0", @@ -7449,6 +7450,27 @@ "dev": true, "license": "MIT" }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -9601,6 +9623,12 @@ "dev": true, "license": "MIT" }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, "node_modules/node-abi": { "version": "3.71.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz", @@ -11122,6 +11150,15 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -12056,6 +12093,19 @@ } } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -12637,6 +12687,12 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, "node_modules/wrap-ansi": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", diff --git a/package.json b/package.json index 55802c5..26b679d 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "clipboardy": "^4.0.0", "delay": "^6.0.0", "fuse.js": "^7.0.0", + "handlebars": "^4.7.8", "ink": "^5.1.0", "ink-ascii": "^0.0.4", "ink-big-text": "^2.0.0", diff --git a/source/commands/env/export/generators/ConditionSetGenerator.ts b/source/commands/env/export/generators/ConditionSetGenerator.ts index 8269ecc..7dc7a40 100644 --- a/source/commands/env/export/generators/ConditionSetGenerator.ts +++ b/source/commands/env/export/generators/ConditionSetGenerator.ts @@ -1,62 +1,70 @@ import { Permit } from 'permitio'; import { HCLGenerator, WarningCollector } from '../types.js'; import { createSafeId } from '../utils.js'; +import Handlebars from 'handlebars'; +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); export class ConditionSetGenerator implements HCLGenerator { - name = 'condition sets'; - - constructor( - private permit: Permit, - private warningCollector: WarningCollector, - ) {} - - async generateHCL(): Promise { - try { - const conditionSets = await this.permit.api.conditionSets.list(); - if ( - !conditionSets || - !Array.isArray(conditionSets) || - conditionSets.length === 0 - ) { - return ''; - } - - let hcl = '\n# Condition Sets\n'; - - for (const set of conditionSets) { - try { - const isResourceSet = set.type === 'resourceset'; - const resourceType = isResourceSet ? 'resource_set' : 'user_set'; - - // Handle conditions - ensure they are properly stringified - const conditions = - typeof set.conditions === 'string' - ? set.conditions - : JSON.stringify(set.conditions || ''); - - hcl += `resource "permitio_${resourceType}" "${createSafeId(set.key)}" { - key = "${set.key}" - name = "${set.name}"${ - set.description ? `\n description = "${set.description}"` : '' - } - conditions = ${conditions}${ - set.resource ? `\n resource = "${set.resource}"` : '' - } -}\n`; - } catch (setError) { - this.warningCollector.addWarning( - `Failed to export condition set ${set.key}: ${setError}`, - ); - continue; - } - } - - return hcl; - } catch (error) { - this.warningCollector.addWarning( - `Failed to export condition sets: ${error}`, - ); - return ''; - } - } -} + name = 'condition sets'; + private template: HandlebarsTemplateDelegate; + + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/condition-set.hcl'), 'utf-8') + ); + } + + async generateHCL(): Promise { + try { + const conditionSets = await this.permit.api.conditionSets.list(); + if ( + !conditionSets || + !Array.isArray(conditionSets) || + conditionSets.length === 0 + ) { + return ''; + } + + const validSets = conditionSets.map(set => { + try { + const isResourceSet = set.type === 'resourceset'; + const resourceType = isResourceSet ? 'resource_set' : 'user_set'; + const conditions = typeof set.conditions === 'string' + ? set.conditions + : JSON.stringify(set.conditions || ''); + + return { + key: createSafeId(set.key), + name: set.name, + description: set.description, + conditions, + resource: set.resource, + resourceType + }; + } catch (setError) { + this.warningCollector.addWarning( + `Failed to export condition set ${set.key}: ${setError}`, + ); + return null; + } + }).filter(Boolean); // Remove null values from failed conversions + + if (validSets.length === 0) return ''; + + return '\n# Condition Sets\n' + this.template({ conditionSets: validSets }); + } catch (error) { + this.warningCollector.addWarning( + `Failed to export condition sets: ${error}`, + ); + return ''; + } + } +} \ No newline at end of file diff --git a/source/commands/env/export/generators/RelationGenerator.ts b/source/commands/env/export/generators/RelationGenerator.ts index 44adaef..9d34c75 100644 --- a/source/commands/env/export/generators/RelationGenerator.ts +++ b/source/commands/env/export/generators/RelationGenerator.ts @@ -1,14 +1,26 @@ import { Permit } from 'permitio'; import { HCLGenerator, WarningCollector } from '../types.js'; import { createSafeId } from '../utils.js'; +import Handlebars from 'handlebars'; +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); export class RelationGenerator implements HCLGenerator { name = 'relations'; + private template: HandlebarsTemplateDelegate; constructor( private permit: Permit, private warningCollector: WarningCollector, - ) {} + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/relation.hcl'), 'utf-8'), + ); + } async generateHCL(): Promise { try { @@ -43,47 +55,24 @@ export class RelationGenerator implements HCLGenerator { return ''; // Return empty string if no relations found, no header } - // Add header only when we have relations to show - let hcl = '\n# Resource Relations\n'; - // Remove duplicates based on relation key const uniqueRelations = Array.from( new Map(allRelations.map(r => [r.key, r])).values(), ); - for (const relation of uniqueRelations) { - try { - if ( - !relation.key || - !relation.subject_resource || - !relation.object_resource - ) { - this.warningCollector.addWarning( - `Skipping invalid relation with key: ${relation.key}`, - ); - continue; - } - - hcl += `resource "permitio_relation" "${createSafeId(relation.key)}" { - key = "${relation.key}" - name = "${relation.name || relation.key}"${ - relation.description ? `\n description = "${relation.description}"` : '' - } - subject_resource = "${relation.subject_resource}" - object_resource = "${relation.object_resource}" - }\n`; - } catch (relationError) { - this.warningCollector.addWarning( - `Failed to export relation ${relation.key}: ${relationError}`, - ); - continue; - } - } + // Map the relations to the format expected by the template + const formattedRelations = uniqueRelations.map(relation => ({ + key: createSafeId(relation.key), + name: relation.name || relation.key, + description: relation.description, + subject_resource: relation.subject_resource, + object_resource: relation.object_resource, + })); - return hcl; + return '\n# Resource Relations\n' + this.template({ relations: formattedRelations }); } catch (error) { this.warningCollector.addWarning(`Failed to export relations: ${error}`); return ''; } } -} +} \ No newline at end of file diff --git a/source/commands/env/export/generators/ResourceGenerator.ts b/source/commands/env/export/generators/ResourceGenerator.ts index 2cec986..ddfdcb1 100644 --- a/source/commands/env/export/generators/ResourceGenerator.ts +++ b/source/commands/env/export/generators/ResourceGenerator.ts @@ -1,84 +1,53 @@ import { Permit } from 'permitio'; import { HCLGenerator, WarningCollector } from '../types.js'; import { createSafeId } from '../utils.js'; +import Handlebars from 'handlebars'; +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); -// Define types for attributes interface Attribute { - type: string; - description?: string; + type: string; + description?: string; } export class ResourceGenerator implements HCLGenerator { - name = 'resources'; - - constructor( - private permit: Permit, - private warningCollector: WarningCollector, - ) {} - - async generateHCL(): Promise { - try { - const resources = await this.permit.api.resources.list(); - const validResources = resources.filter( - resource => resource.key !== '__user', - ); - - if (validResources.length === 0) return ''; - - let hcl = '\n# Resources\n'; - - for (const resource of validResources) { - hcl += `resource "permitio_resource" "${createSafeId(resource.key)}" { - key = "${resource.key}" - name = "${resource.name}"${ - resource.description ? `\n description = "${resource.description}"` : '' - }${resource.urn ? `\n urn = "${resource.urn}"` : ''} - actions = {${this.generateActions(resource.actions || {})}} - ${this.generateAttributes(resource.attributes)} -}\n`; - } - - return hcl; - } catch (error) { - this.warningCollector.addWarning(`Failed to export resources: ${error}`); - return ''; - } - } - - private generateActions( - actions: Record, - ): string { - if (Object.keys(actions).length === 0) return ''; - - return Object.entries(actions) - .map( - ([actionKey, action]) => ` - "${actionKey}" = { - name = "${action.name || 'Unnamed Action'}"${ - action.description - ? `\n description = "${action.description}"` - : '' - } - }`, - ) - .join(''); - } - - private generateAttributes( - attributes: Record | undefined, - ): string { - if (!attributes || Object.keys(attributes).length === 0) return ''; - - return `attributes = {${Object.entries(attributes) - .map( - ([attrKey, attr]) => ` - "${attrKey}" = { - type = "${attr.type}"${ - attr.description ? `\n description = "${attr.description}"` : '' - } - }`, - ) - .join('')} - }`; - } -} + name = 'resources'; + private template: HandlebarsTemplateDelegate; + + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/resource.hcl'), 'utf-8') + ); + } + + async generateHCL(): Promise { + try { + const resources = await this.permit.api.resources.list(); + const validResources = resources + .filter(resource => resource.key !== '__user') + .map(resource => ({ + key: createSafeId(resource.key), + name: resource.name, + description: resource.description, + urn: resource.urn, + actions: resource.actions || {}, + attributes: resource.attributes + })); + + if (validResources.length === 0) return ''; + + return '\n# Resources\n' + this.template({ resources: validResources }); + + } catch (error) { + this.warningCollector.addWarning(`Failed to export resources: ${error}`); + return ''; + } + } +} \ No newline at end of file diff --git a/source/commands/env/export/generators/RoleGenerator.ts b/source/commands/env/export/generators/RoleGenerator.ts index ecad746..ee188db 100644 --- a/source/commands/env/export/generators/RoleGenerator.ts +++ b/source/commands/env/export/generators/RoleGenerator.ts @@ -1,37 +1,50 @@ import { Permit } from 'permitio'; import { HCLGenerator, WarningCollector } from '../types.js'; import { createSafeId } from '../utils.js'; +import Handlebars from 'handlebars'; +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +Handlebars.registerHelper('json', function(context) { + return `[${context.map(item => `"${item}"`).join(',')}]`; +}); export class RoleGenerator implements HCLGenerator { - name = 'roles'; - - constructor( - private permit: Permit, - private warningCollector: WarningCollector, - ) {} - - async generateHCL(): Promise { - try { - const roles = await this.permit.api.roles.list(); - if (!roles || roles.length === 0) return ''; - - let hcl = '\n# Roles\n'; - for (const role of roles) { - hcl += `resource "permitio_role" "${createSafeId(role.key)}" { - key = "${role.key}" - name = "${role.name}"${ - role.description ? `\n description = "${role.description}"` : '' - }${ - role.permissions && role.permissions.length > 0 - ? `\n permissions = ${JSON.stringify(role.permissions)}` - : '' - } -}\n`; - } - return hcl; - } catch (error) { - this.warningCollector.addWarning(`Failed to export roles: ${error}`); - return ''; - } - } -} + name = 'roles'; + private template: HandlebarsTemplateDelegate; + + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/role.hcl'), 'utf-8') + ); + } + + async generateHCL(): Promise { + try { + const roles = await this.permit.api.roles.list(); + + if (!roles || roles.length === 0) + return ''; + + // Transform roles to format needed for template + const validRoles = roles.map(role => ({ + key: createSafeId(role.key), + name: role.name, + permissions: role.permissions || [] + })); + + return '\n# Roles\n' + this.template({ roles: validRoles }); + + } catch (error) { + this.warningCollector.addWarning(`Failed to export roles: ${error}`); + return ''; + } + } +} \ No newline at end of file diff --git a/source/commands/env/export/generators/UserAttributesGenerator.ts b/source/commands/env/export/generators/UserAttributesGenerator.ts index d3c1a6a..55c0900 100644 --- a/source/commands/env/export/generators/UserAttributesGenerator.ts +++ b/source/commands/env/export/generators/UserAttributesGenerator.ts @@ -1,38 +1,48 @@ import { Permit } from 'permitio'; import { HCLGenerator, WarningCollector } from '../types.js'; import { createSafeId } from '../utils.js'; +import Handlebars from 'handlebars'; +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); export class UserAttributesGenerator implements HCLGenerator { - name = 'user attributes'; + name = 'user attributes'; + private template: HandlebarsTemplateDelegate; + + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/user-attribute.hcl'), 'utf-8') + ); + } - constructor( - private permit: Permit, - private warningCollector: WarningCollector, - ) {} + async generateHCL(): Promise { + try { + const userAttributes = await this.permit.api.resourceAttributes.list({ + resourceKey: '__user', + }); - async generateHCL(): Promise { - try { - const userAttributes = await this.permit.api.resourceAttributes.list({ - resourceKey: '__user', - }); + if (!userAttributes || userAttributes.length === 0) + return ''; - if (!userAttributes || userAttributes.length === 0) return ''; + const validAttributes = userAttributes.map(attr => ({ + key: createSafeId(attr.key), + type: attr.type, + description: attr.description, + })); - let hcl = '\n# User Attributes\n'; - for (const attr of userAttributes) { - hcl += `resource "permitio_user_attribute" "${createSafeId(attr.key)}" { - key = "${attr.key}" - type = "${attr.type}"${ - attr.description ? `\n description = "${attr.description}"` : '' - } -}\n`; - } - return hcl; - } catch (error) { - this.warningCollector.addWarning( - `Failed to export user attributes: ${error}`, - ); - return ''; - } - } -} + return '\n# User Attributes\n' + this.template({ attributes: validAttributes }); + } catch (error) { + this.warningCollector.addWarning( + `Failed to export user attributes: ${error}`, + ); + return ''; + } + } +} \ No newline at end of file diff --git a/source/commands/env/export/templates/condition-set.hcl b/source/commands/env/export/templates/condition-set.hcl new file mode 100644 index 0000000..7c01b37 --- /dev/null +++ b/source/commands/env/export/templates/condition-set.hcl @@ -0,0 +1,13 @@ +{{#each conditionSets}} +resource "permitio_{{resourceType}}" "{{key}}" { + key = "{{key}}" + name = "{{name}}" + {{#if description}} + description = "{{description}}" + {{/if}} + conditions = {{conditions}} + {{#if resource}} + resource = "{{resource}}" + {{/if}} +} +{{/each}} \ No newline at end of file diff --git a/source/commands/env/export/templates/relation.hcl b/source/commands/env/export/templates/relation.hcl new file mode 100644 index 0000000..c58048f --- /dev/null +++ b/source/commands/env/export/templates/relation.hcl @@ -0,0 +1,11 @@ +{{#each relations}} +resource "permitio_relation" "{{key}}" { + key = "{{key}}" + name = "{{name}}" + {{#if description}} + description = "{{description}}" + {{/if}} + subject_resource = "{{subject_resource}}" + object_resource = "{{object_resource}}" +} +{{/each}} \ No newline at end of file diff --git a/source/commands/env/export/templates/resource.hcl b/source/commands/env/export/templates/resource.hcl new file mode 100644 index 0000000..96f6875 --- /dev/null +++ b/source/commands/env/export/templates/resource.hcl @@ -0,0 +1,38 @@ +{{#each resources}} +resource "permitio_resource" "{{key}}" { + key = "{{key}}" + name = "{{name}}" + {{#if description}} + description = "{{description}}" + {{/if}} + {{#if urn}} + urn = "{{urn}}" + {{/if}} + + {{#if actions}} + actions = { + {{#each actions}} + "{{@key}}" = { + name = "{{name}}" + {{#if description}} + description = "{{description}}" + {{/if}} + } + {{/each}} + } + {{/if}} + + {{#if attributes}} + attributes = { + {{#each attributes}} + "{{@key}}" = { + type = "{{type}}" + {{#if description}} + description = "{{description}}" + {{/if}} + } + {{/each}} + } + {{/if}} +} +{{/each}} \ No newline at end of file diff --git a/source/commands/env/export/templates/role.hcl b/source/commands/env/export/templates/role.hcl new file mode 100644 index 0000000..795c7f3 --- /dev/null +++ b/source/commands/env/export/templates/role.hcl @@ -0,0 +1,9 @@ +{{#each roles}} +resource "permitio_role" "{{key}}" { + key = "{{key}}" + name = "{{name}}" + {{#if permissions}} + permissions = {{{json permissions}}} + {{/if}} +} +{{/each}} \ No newline at end of file diff --git a/source/commands/env/export/templates/user-attribute.hcl b/source/commands/env/export/templates/user-attribute.hcl new file mode 100644 index 0000000..af91431 --- /dev/null +++ b/source/commands/env/export/templates/user-attribute.hcl @@ -0,0 +1,9 @@ +{{#each attributes}} +resource "permitio_user_attribute" "{{key}}" { + key = "{{key}}" + type = "{{type}}" + {{#if description}} + description = "{{description}}" + {{/if}} +} +{{/each}} \ No newline at end of file From c34683317a80bb5adf9d20e6e6cb901bdc4c7bf9 Mon Sep 17 00:00:00 2001 From: daveads Date: Tue, 31 Dec 2024 07:11:51 +0100 Subject: [PATCH 15/34] move HCL output to render function --- source/components/export/ExportContent.tsx | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/source/components/export/ExportContent.tsx b/source/components/export/ExportContent.tsx index 7e83420..3eb9ce4 100644 --- a/source/components/export/ExportContent.tsx +++ b/source/components/export/ExportContent.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { useApiKeyApi } from '../../hooks/useApiKeyApi.js'; import { useAuth } from '../../components/AuthProvider.js'; -import { ExportOptions } from '../../commands/env/export/types.js' +import { ExportOptions } from '../../commands/env/export/types.js'; import { ExportStatus } from './ExportStatus.js'; import { useExport } from '../../hooks/export/useExport.js'; - import fs from 'node:fs/promises'; +import { Text } from 'ink'; // Import Text component from Ink export const ExportContent: React.FC<{ options: ExportOptions }> = ({ options: { key: apiKey, file }, @@ -14,6 +14,7 @@ export const ExportContent: React.FC<{ options: ExportOptions }> = ({ const { authToken } = useAuth(); const key = apiKey || authToken; const { state, setState, exportConfig } = useExport(key); + const [hclOutput, setHclOutput] = React.useState(null); React.useEffect(() => { let isSubscribed = true; @@ -81,7 +82,7 @@ export const ExportContent: React.FC<{ options: ExportOptions }> = ({ return; } } else { - console.log(hcl); + setHclOutput(hcl); // Store HCL output in state } if (!isSubscribed) return; @@ -111,5 +112,14 @@ export const ExportContent: React.FC<{ options: ExportOptions }> = ({ }; }, [key, file, validateApiKeyScope]); - return ; -}; + return ( + <> + + {!file && hclOutput && ( + + {hclOutput} {/* Wrap HCL output in */} + + )} + + ); +}; \ No newline at end of file From 3b1b3b3cc31960d18f2aef5304b5b96e0d5046f2 Mon Sep 17 00:00:00 2001 From: daveads Date: Tue, 31 Dec 2024 07:19:00 +0100 Subject: [PATCH 16/34] improve error rendering with concise conditional logic --- source/components/export/ExportContent.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/source/components/export/ExportContent.tsx b/source/components/export/ExportContent.tsx index 3eb9ce4..fef5b66 100644 --- a/source/components/export/ExportContent.tsx +++ b/source/components/export/ExportContent.tsx @@ -5,7 +5,7 @@ import { ExportOptions } from '../../commands/env/export/types.js'; import { ExportStatus } from './ExportStatus.js'; import { useExport } from '../../hooks/export/useExport.js'; import fs from 'node:fs/promises'; -import { Text } from 'ink'; // Import Text component from Ink +import { Text } from 'ink'; export const ExportContent: React.FC<{ options: ExportOptions }> = ({ options: { key: apiKey, file }, @@ -120,6 +120,9 @@ export const ExportContent: React.FC<{ options: ExportOptions }> = ({ {hclOutput} {/* Wrap HCL output in */} )} + {state.error && ( + {state.error} + )} ); }; \ No newline at end of file From da5a34d71f8cc1ac03cac21208f86f66059532f3 Mon Sep 17 00:00:00 2001 From: daveads Date: Tue, 31 Dec 2024 07:34:55 +0100 Subject: [PATCH 17/34] review --- source/components/export/ExportContent.tsx | 7 ++++--- source/hooks/export/PermitSDK.ts | 8 ++++---- source/hooks/export/useExport.ts | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/source/components/export/ExportContent.tsx b/source/components/export/ExportContent.tsx index fef5b66..c5436f6 100644 --- a/source/components/export/ExportContent.tsx +++ b/source/components/export/ExportContent.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useEffect, useState, FC } from 'react'; import { useApiKeyApi } from '../../hooks/useApiKeyApi.js'; import { useAuth } from '../../components/AuthProvider.js'; import { ExportOptions } from '../../commands/env/export/types.js'; @@ -7,16 +8,16 @@ import { useExport } from '../../hooks/export/useExport.js'; import fs from 'node:fs/promises'; import { Text } from 'ink'; -export const ExportContent: React.FC<{ options: ExportOptions }> = ({ +export const ExportContent: FC<{ options: ExportOptions }> = ({ options: { key: apiKey, file }, }) => { const { validateApiKeyScope } = useApiKeyApi(); const { authToken } = useAuth(); const key = apiKey || authToken; const { state, setState, exportConfig } = useExport(key); - const [hclOutput, setHclOutput] = React.useState(null); + const [hclOutput, setHclOutput] = useState(null); - React.useEffect(() => { + useEffect(() => { let isSubscribed = true; const runExport = async () => { diff --git a/source/hooks/export/PermitSDK.ts b/source/hooks/export/PermitSDK.ts index 97f7156..800f114 100644 --- a/source/hooks/export/PermitSDK.ts +++ b/source/hooks/export/PermitSDK.ts @@ -1,13 +1,13 @@ import { Permit } from 'permitio'; import React from 'react'; -export const PermitSDK = (token: string) => { +export const usePermitSDK = (token: string, pdpUrl: string = 'http://localhost:7766') => { return React.useMemo( () => new Permit({ token, - pdp: 'http://localhost:7766', + pdp: pdpUrl, }), - [token], + [token, pdpUrl], ); -}; +}; \ No newline at end of file diff --git a/source/hooks/export/useExport.ts b/source/hooks/export/useExport.ts index 778a6f6..86fd9c9 100644 --- a/source/hooks/export/useExport.ts +++ b/source/hooks/export/useExport.ts @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { PermitSDK } from './PermitSDK.js'; +import { usePermitSDK } from './PermitSDK.js'; import { ExportState } from '../../commands/env/export/types.js'; import { createWarningCollector, generateProviderBlock } from '../../commands/env/export/utils.js'; import { ResourceGenerator } from '../../commands/env/export/generators/ResourceGenerator.js'; @@ -23,7 +23,7 @@ export const useExport = (apiKey: string) => { warnings: [], }); - const permit = PermitSDK(apiKey); + const permit = usePermitSDK(apiKey); const exportConfig = async (scope: ExportScope) => { try { From ff22083be31cfd55cf5288a9e315c501c08e0762 Mon Sep 17 00:00:00 2001 From: daveads Date: Sat, 4 Jan 2025 07:04:01 +0100 Subject: [PATCH 18/34] added missing resource and also depends_on --- .../export/generators/RelationGenerator.ts | 200 ++++++++++++------ .../export/generators/ResourceSetGenerator.ts | 54 +++++ .../generators/RoleDerivationGenerator.ts | 132 ++++++++++++ .../env/export/generators/RoleGenerator.ts | 37 +++- .../env/export/generators/UserSetGenerator.ts | 53 +++++ .../env/export/templates/relation.hcl | 5 + .../env/export/templates/resource-set.hcl | 13 ++ .../env/export/templates/role-derivation.hcl | 12 ++ source/commands/env/export/templates/role.hcl | 3 + .../env/export/templates/user-set.hcl | 13 ++ source/hooks/export/useExport.ts | 6 + 11 files changed, 452 insertions(+), 76 deletions(-) create mode 100644 source/commands/env/export/generators/ResourceSetGenerator.ts create mode 100644 source/commands/env/export/generators/RoleDerivationGenerator.ts create mode 100644 source/commands/env/export/generators/UserSetGenerator.ts create mode 100644 source/commands/env/export/templates/resource-set.hcl create mode 100644 source/commands/env/export/templates/role-derivation.hcl create mode 100644 source/commands/env/export/templates/user-set.hcl diff --git a/source/commands/env/export/generators/RelationGenerator.ts b/source/commands/env/export/generators/RelationGenerator.ts index 9d34c75..41f5d1b 100644 --- a/source/commands/env/export/generators/RelationGenerator.ts +++ b/source/commands/env/export/generators/RelationGenerator.ts @@ -9,70 +9,140 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +interface RelationData { + key: string; + name: string; + subject_resource: string; + object_resource: string; + description?: string; +} + export class RelationGenerator implements HCLGenerator { - name = 'relations'; - private template: HandlebarsTemplateDelegate; - - constructor( - private permit: Permit, - private warningCollector: WarningCollector, - ) { - this.template = Handlebars.compile( - readFileSync(join(__dirname, '../templates/relation.hcl'), 'utf-8'), - ); - } - - async generateHCL(): Promise { - try { - // First get all resources - const resources = await this.permit.api.resources.list(); - if (!resources || !Array.isArray(resources)) { - return ''; // Return empty string when no resources, no header - } - - // For each resource, get its relations - let allRelations = []; - for (const resource of resources) { - if (resource.key === '__user') continue; // Skip internal user resource - - try { - const resourceRelations = - await this.permit.api.resourceRelations.list({ - resourceKey: resource.key, - }); - - if (resourceRelations && Array.isArray(resourceRelations)) { - allRelations.push(...resourceRelations); - } - } catch (err) { - this.warningCollector.addWarning( - `Failed to fetch relations for resource ${resource.key}: ${err}`, - ); - } - } - - if (allRelations.length === 0) { - return ''; // Return empty string if no relations found, no header - } - - // Remove duplicates based on relation key - const uniqueRelations = Array.from( - new Map(allRelations.map(r => [r.key, r])).values(), - ); - - // Map the relations to the format expected by the template - const formattedRelations = uniqueRelations.map(relation => ({ - key: createSafeId(relation.key), - name: relation.name || relation.key, - description: relation.description, - subject_resource: relation.subject_resource, - object_resource: relation.object_resource, - })); - - return '\n# Resource Relations\n' + this.template({ relations: formattedRelations }); - } catch (error) { - this.warningCollector.addWarning(`Failed to export relations: ${error}`); - return ''; - } - } + name = 'relations'; + private template: HandlebarsTemplateDelegate; + private resourceKeys: Set = new Set(); + + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/relation.hcl'), 'utf-8'), + ); + } + + private async loadResourceKeys(): Promise { + try { + const resources = await this.permit.api.resources.list(); + if (resources && Array.isArray(resources)) { + resources.forEach(resource => { + if (resource.key !== '__user') { + this.resourceKeys.add(createSafeId(resource.key)); + } + }); + } + } catch (error) { + this.warningCollector.addWarning(`Failed to load resources: ${error}`); + } + } + + private validateResource(resourceKey: string): boolean { + const safeKey = createSafeId(resourceKey); + if (!this.resourceKeys.has(safeKey)) { + this.warningCollector.addWarning(`Referenced resource "${resourceKey}" does not exist`); + return false; + } + return true; + } + + private validateRelation(relation: any): relation is RelationData { + const requiredFields = ['key', 'name', 'subject_resource', 'object_resource']; + const missingFields = requiredFields.filter(field => !relation[field]); + + if (missingFields.length > 0) { + this.warningCollector.addWarning( + `Relation "${relation.key || 'unknown'}" is missing required fields: ${missingFields.join(', ')}` + ); + return false; + } + + // Validate that referenced resources exist + if (!this.validateResource(relation.subject_resource) || + !this.validateResource(relation.object_resource)) { + return false; + } + + return true; + } + + private formatRelation(relation: RelationData): RelationData { + return { + key: createSafeId(relation.key), + name: relation.name, + subject_resource: createSafeId(relation.subject_resource), + object_resource: createSafeId(relation.object_resource), + ...(relation.description && { description: relation.description }) + }; + } + + async generateHCL(): Promise { + try { + // Load resources first for validation + await this.loadResourceKeys(); + + // Get all resources + const resources = await this.permit.api.resources.list(); + + if (!resources?.length) { + return ''; + } + + // Collect all relations + const allRelations = []; + for (const resource of resources) { + if (resource.key === '__user') { + continue; + } + + try { + const resourceRelations = await this.permit.api.resourceRelations.list({ + resourceKey: resource.key, + }); + + if (resourceRelations?.length) { + allRelations.push(...resourceRelations); + } + } catch (err) { + this.warningCollector.addWarning( + `Failed to fetch relations for resource ${resource.key}: ${err}` + ); + } + } + + if (!allRelations.length) { + return ''; + } + + // Remove duplicates and get unique relations + const uniqueRelations = Array.from( + new Map(allRelations.map(r => [r.key, r])).values() + ); + + // Validate and format relations + const validRelations = uniqueRelations + .filter(this.validateRelation.bind(this)) + .map(this.formatRelation.bind(this)); + + if (!validRelations.length) { + return ''; + } + + // Generate HCL + return '\n# Resource Relations\n' + this.template({ relations: validRelations }); + + } catch (error) { + this.warningCollector.addWarning(`Failed to export relations: ${error}`); + return ''; + } + } } \ No newline at end of file diff --git a/source/commands/env/export/generators/ResourceSetGenerator.ts b/source/commands/env/export/generators/ResourceSetGenerator.ts new file mode 100644 index 0000000..9beee15 --- /dev/null +++ b/source/commands/env/export/generators/ResourceSetGenerator.ts @@ -0,0 +1,54 @@ +// ResourceSetGenerator.ts +import { Permit } from 'permitio'; +import { HCLGenerator, WarningCollector } from '../types.js'; +import { createSafeId } from '../utils.js'; +import Handlebars from 'handlebars'; +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export class ResourceSetGenerator implements HCLGenerator { + name = 'resource set'; + private template: HandlebarsTemplateDelegate; + + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/resource-set.hcl'), 'utf-8') + ); + } + + async generateHCL(): Promise { + try { + // Get all resource sets using the Permit SDK + const resourceSets = await this.permit.api.conditionSets.list({}); + + // Filter only resource sets (not user sets) + const validSets = resourceSets + .filter(set => set.type === 'resourceset') + .map(set => ({ + key: createSafeId(set.key), + name: set.name, + description: set.description, + conditions: typeof set.conditions === 'string' + ? set.conditions + : JSON.stringify(set.conditions), + resource: set.resource_id + })); + + if (validSets.length === 0) return ''; + + return '\n# Resource Sets\n' + this.template({ sets: validSets }); + } catch (error) { + this.warningCollector.addWarning( + `Failed to export resource sets: ${error}`, + ); + return ''; + } + } +} \ No newline at end of file diff --git a/source/commands/env/export/generators/RoleDerivationGenerator.ts b/source/commands/env/export/generators/RoleDerivationGenerator.ts new file mode 100644 index 0000000..2e7c224 --- /dev/null +++ b/source/commands/env/export/generators/RoleDerivationGenerator.ts @@ -0,0 +1,132 @@ +import { Permit } from 'permitio'; +import { HCLGenerator, WarningCollector } from '../types.js'; +import { createSafeId } from '../utils.js'; +import Handlebars from 'handlebars'; +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const currentFilePath = fileURLToPath(import.meta.url); +const currentDirPath = dirname(currentFilePath); + + +interface RoleDerivation { + resource: string; + role: string; + linked_by: string; + on_resource: string; + to_role: string; + dependencies: string[]; +} + +export class RoleDerivationGenerator implements HCLGenerator { + name = 'role derivation'; + private template: HandlebarsTemplateDelegate; + + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(currentDirPath, '../templates/role-derivation.hcl'), 'utf-8') + ); + } + + private getDependencies(derivation: RoleDerivation): string[] { + const dependencies = new Set(); + + // Add dependency on the source resource + dependencies.add(`permitio_resource.${createSafeId(derivation.resource)}`); + + // Add dependency on the target resource + dependencies.add(`permitio_resource.${createSafeId(derivation.on_resource)}`); + + // Add dependencies on both roles + dependencies.add(`permitio_role.${createSafeId(derivation.role)}`); + dependencies.add(`permitio_role.${createSafeId(derivation.to_role)}`); + + return Array.from(dependencies); + } + + async generateHCL(): Promise { + try { + const resources = await this.permit.api.resources.list(); + if (!resources?.length) return ''; + + const allDerivations: RoleDerivation[] = []; + + for (const resource of resources) { + try { + if (!resource.key) { + this.warningCollector.addWarning( + `Skipping resource with missing key: ${resource.id}` + ); + continue; + } + + const resourceRoles = await this.permit.api.resourceRoles.list({ + resourceKey: resource.key, + }); + + const validDerivations = resourceRoles + .filter(derivation => + derivation.resource && + derivation.role && + derivation.linked_by && + derivation.on_resource && + derivation.to_role + ) + .map(derivation => ({ + ...derivation, + resource: createSafeId(derivation.resource), + role: createSafeId(derivation.role), + linked_by: createSafeId(derivation.linked_by), + on_resource: createSafeId(derivation.on_resource), + to_role: createSafeId(derivation.to_role), + resource_id: createSafeId( + `${derivation.resource}_${derivation.role}_${derivation.linked_by}_${derivation.on_resource}_${derivation.to_role}` + ), + dependencies: [] // Initialize empty dependencies array + })); + + // Add dependencies for each derivation + validDerivations.forEach(derivation => { + derivation.dependencies = this.getDependencies(derivation); + }); + + if (validDerivations.length) { + allDerivations.push(...validDerivations); + } + } catch (err) { + this.warningCollector.addWarning( + `Failed to fetch role derivations for resource '${resource.key}': ${err}` + ); + continue; + } + } + + if (!allDerivations.length) { + return ''; + } + + const hcl = this.template({ + derivations: allDerivations.map(derivation => ({ + resource_id: derivation.resource_id, + resource: derivation.resource, + role: derivation.role, + linked_by: derivation.linked_by, + on_resource: derivation.on_resource, + to_role: derivation.to_role, + dependencies: derivation.dependencies + })) + }); + + return '\n# Role Derivations\n' + hcl; + } catch (error) { + this.warningCollector.addWarning( + `Failed to export role derivations: ${error}` + ); + return ''; + } + } +} \ No newline at end of file diff --git a/source/commands/env/export/generators/RoleGenerator.ts b/source/commands/env/export/generators/RoleGenerator.ts index ee188db..4333897 100644 --- a/source/commands/env/export/generators/RoleGenerator.ts +++ b/source/commands/env/export/generators/RoleGenerator.ts @@ -29,22 +29,37 @@ export class RoleGenerator implements HCLGenerator { async generateHCL(): Promise { try { const roles = await this.permit.api.roles.list(); - - if (!roles || roles.length === 0) + if (!roles || roles.length === 0) return ''; - - // Transform roles to format needed for template - const validRoles = roles.map(role => ({ - key: createSafeId(role.key), - name: role.name, - permissions: role.permissions || [] - })); - + + // Transform roles and identify dependencies + const validRoles = roles.map(role => { + const dependencies = this.getDependencies(role.permissions); + return { + key: createSafeId(role.key), + name: role.name, + permissions: role.permissions || [], + dependencies: dependencies + }; + }); + return '\n# Roles\n' + this.template({ roles: validRoles }); - } catch (error) { this.warningCollector.addWarning(`Failed to export roles: ${error}`); return ''; } } + + private getDependencies(permissions: string[]): string[] { + // Extract resource keys from permissions and generate dependency references + const resourceDeps = new Set(); + permissions?.forEach(perm => { + const [resource] = perm.split(':'); + if (resource) { + resourceDeps.add(`permitio_resource.${createSafeId(resource)}`); + } + }); + return Array.from(resourceDeps); + } + } \ No newline at end of file diff --git a/source/commands/env/export/generators/UserSetGenerator.ts b/source/commands/env/export/generators/UserSetGenerator.ts new file mode 100644 index 0000000..8d6abc7 --- /dev/null +++ b/source/commands/env/export/generators/UserSetGenerator.ts @@ -0,0 +1,53 @@ +import { Permit } from 'permitio'; +import { HCLGenerator, WarningCollector } from '../types.js'; +import { createSafeId } from '../utils.js'; +import Handlebars from 'handlebars'; +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export class UserSetGenerator implements HCLGenerator { + name = 'user set'; + private template: HandlebarsTemplateDelegate; + + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/user-set.hcl'), 'utf-8') + ); + } + + async generateHCL(): Promise { + try { + // Get all condition sets using the Permit SDK + const conditionSets = await this.permit.api.conditionSets.list({}); + + // Filter only user sets (not resource sets) + const validSets = conditionSets + .filter(set => set.type === 'userset') + .map(set => ({ + key: createSafeId(set.key), + name: set.name, + description: set.description, + conditions: typeof set.conditions === 'string' + ? set.conditions + : JSON.stringify(set.conditions), + resource: set.resource_id + })); + + if (validSets.length === 0) return ''; + + return '\n# User Sets\n' + this.template({ sets: validSets }); + } catch (error) { + this.warningCollector.addWarning( + `Failed to export user sets: ${error}`, + ); + return ''; + } + } +} \ No newline at end of file diff --git a/source/commands/env/export/templates/relation.hcl b/source/commands/env/export/templates/relation.hcl index c58048f..8331e49 100644 --- a/source/commands/env/export/templates/relation.hcl +++ b/source/commands/env/export/templates/relation.hcl @@ -7,5 +7,10 @@ resource "permitio_relation" "{{key}}" { {{/if}} subject_resource = "{{subject_resource}}" object_resource = "{{object_resource}}" + + depends_on = [ + permitio_resource.{{subject_resource}}, + permitio_resource.{{object_resource}} + ] } {{/each}} \ No newline at end of file diff --git a/source/commands/env/export/templates/resource-set.hcl b/source/commands/env/export/templates/resource-set.hcl new file mode 100644 index 0000000..b44069a --- /dev/null +++ b/source/commands/env/export/templates/resource-set.hcl @@ -0,0 +1,13 @@ +{{#each sets}} +resource "permitio_resource_set" "{{key}}" { + key = "{{key}}" + name = "{{name}}" + {{#if description}} + description = "{{description}}" + {{/if}} + {{#if resource}} + resource = "{{resource}}" + {{/if}} + conditions = "{{conditions}}" +} +{{/each}} \ No newline at end of file diff --git a/source/commands/env/export/templates/role-derivation.hcl b/source/commands/env/export/templates/role-derivation.hcl new file mode 100644 index 0000000..c4ed579 --- /dev/null +++ b/source/commands/env/export/templates/role-derivation.hcl @@ -0,0 +1,12 @@ +{{#each derivations}} +resource "permitio_role_derivation" "{{resource_id}}" { + resource = "{{resource}}" + role = "{{role}}" + linked_by = "{{linked_by}}" + on_resource = "{{on_resource}}" + to_role = "{{to_role}}" + {{#if dependencies}} + depends_on = [{{{json dependencies}}}] + {{/if}} +} +{{/each}} \ No newline at end of file diff --git a/source/commands/env/export/templates/role.hcl b/source/commands/env/export/templates/role.hcl index 795c7f3..cc45527 100644 --- a/source/commands/env/export/templates/role.hcl +++ b/source/commands/env/export/templates/role.hcl @@ -5,5 +5,8 @@ resource "permitio_role" "{{key}}" { {{#if permissions}} permissions = {{{json permissions}}} {{/if}} + {{#if dependencies}} + depends_on = [{{{json dependencies}}}] + {{/if}} } {{/each}} \ No newline at end of file diff --git a/source/commands/env/export/templates/user-set.hcl b/source/commands/env/export/templates/user-set.hcl new file mode 100644 index 0000000..7bf7118 --- /dev/null +++ b/source/commands/env/export/templates/user-set.hcl @@ -0,0 +1,13 @@ +{{#each sets}} +resource "permitio_user_set" "{{key}}" { + key = "{{key}}" + name = "{{name}}" + {{#if description}} + description = "{{description}}" + {{/if}} + {{#if resource}} + resource = "{{resource}}" + {{/if}} + conditions = "{{conditions}}" +} +{{/each}} \ No newline at end of file diff --git a/source/hooks/export/useExport.ts b/source/hooks/export/useExport.ts index 86fd9c9..0614431 100644 --- a/source/hooks/export/useExport.ts +++ b/source/hooks/export/useExport.ts @@ -7,6 +7,9 @@ import { RoleGenerator } from '../../commands/env/export/generators/RoleGenerato import { UserAttributesGenerator } from '../../commands/env/export/generators/UserAttributesGenerator.js'; import { RelationGenerator } from '../../commands/env/export/generators/RelationGenerator.js'; import { ConditionSetGenerator } from '../../commands/env/export/generators/ConditionSetGenerator.js'; +import { ResourceSetGenerator } from '../../commands/env/export/generators/ResourceSetGenerator.js'; +import { UserSetGenerator } from '../../commands/env/export/generators/UserSetGenerator.js'; +import { RoleDerivationGenerator } from '../../commands/env/export/generators/RoleDerivationGenerator.js'; // Define a type for the `scope` parameter interface ExportScope { @@ -41,6 +44,9 @@ ${generateProviderBlock(apiKey)}`; new UserAttributesGenerator(permit, warningCollector), new RelationGenerator(permit, warningCollector), new ConditionSetGenerator(permit, warningCollector), + new ResourceSetGenerator(permit, warningCollector), + new UserSetGenerator(permit, warningCollector), + new RoleDerivationGenerator(permit, warningCollector), ]; for (const generator of generators) { From 8219d7dc53a34c99d7ba3ac7a96dfdb64859a123 Mon Sep 17 00:00:00 2001 From: daveads Date: Sat, 4 Jan 2025 15:30:59 +0100 Subject: [PATCH 19/34] prettier --- .../generators/ConditionSetGenerator.ts | 109 +++---- .../export/generators/RelationGenerator.ts | 278 +++++++++--------- .../export/generators/ResourceGenerator.ts | 67 +++-- .../export/generators/ResourceSetGenerator.ts | 75 ++--- .../generators/RoleDerivationGenerator.ts | 239 +++++++-------- .../env/export/generators/RoleGenerator.ts | 96 +++--- .../generators/UserAttributesGenerator.ts | 63 ++-- .../env/export/generators/UserSetGenerator.ts | 73 +++-- source/components/export/ExportContent.tsx | 6 +- source/components/export/ExportStatus.tsx | 2 +- source/hooks/export/PermitSDK.ts | 9 +- source/hooks/export/useExport.ts | 5 +- 12 files changed, 523 insertions(+), 499 deletions(-) diff --git a/source/commands/env/export/generators/ConditionSetGenerator.ts b/source/commands/env/export/generators/ConditionSetGenerator.ts index 7dc7a40..12327a9 100644 --- a/source/commands/env/export/generators/ConditionSetGenerator.ts +++ b/source/commands/env/export/generators/ConditionSetGenerator.ts @@ -10,61 +10,66 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); export class ConditionSetGenerator implements HCLGenerator { - name = 'condition sets'; - private template: HandlebarsTemplateDelegate; + name = 'condition sets'; + private template: HandlebarsTemplateDelegate; - constructor( - private permit: Permit, - private warningCollector: WarningCollector, - ) { - this.template = Handlebars.compile( - readFileSync(join(__dirname, '../templates/condition-set.hcl'), 'utf-8') - ); - } + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/condition-set.hcl'), 'utf-8'), + ); + } - async generateHCL(): Promise { - try { - const conditionSets = await this.permit.api.conditionSets.list(); - if ( - !conditionSets || - !Array.isArray(conditionSets) || - conditionSets.length === 0 - ) { - return ''; - } + async generateHCL(): Promise { + try { + const conditionSets = await this.permit.api.conditionSets.list(); + if ( + !conditionSets || + !Array.isArray(conditionSets) || + conditionSets.length === 0 + ) { + return ''; + } - const validSets = conditionSets.map(set => { - try { - const isResourceSet = set.type === 'resourceset'; - const resourceType = isResourceSet ? 'resource_set' : 'user_set'; - const conditions = typeof set.conditions === 'string' - ? set.conditions - : JSON.stringify(set.conditions || ''); + const validSets = conditionSets + .map(set => { + try { + const isResourceSet = set.type === 'resourceset'; + const resourceType = isResourceSet ? 'resource_set' : 'user_set'; + const conditions = + typeof set.conditions === 'string' + ? set.conditions + : JSON.stringify(set.conditions || ''); - return { - key: createSafeId(set.key), - name: set.name, - description: set.description, - conditions, - resource: set.resource, - resourceType - }; - } catch (setError) { - this.warningCollector.addWarning( - `Failed to export condition set ${set.key}: ${setError}`, - ); - return null; - } - }).filter(Boolean); // Remove null values from failed conversions + return { + key: createSafeId(set.key), + name: set.name, + description: set.description, + conditions, + resource: set.resource, + resourceType, + }; + } catch (setError) { + this.warningCollector.addWarning( + `Failed to export condition set ${set.key}: ${setError}`, + ); + return null; + } + }) + .filter(Boolean); // Remove null values from failed conversions - if (validSets.length === 0) return ''; + if (validSets.length === 0) return ''; - return '\n# Condition Sets\n' + this.template({ conditionSets: validSets }); - } catch (error) { - this.warningCollector.addWarning( - `Failed to export condition sets: ${error}`, - ); - return ''; - } - } -} \ No newline at end of file + return ( + '\n# Condition Sets\n' + this.template({ conditionSets: validSets }) + ); + } catch (error) { + this.warningCollector.addWarning( + `Failed to export condition sets: ${error}`, + ); + return ''; + } + } +} diff --git a/source/commands/env/export/generators/RelationGenerator.ts b/source/commands/env/export/generators/RelationGenerator.ts index 41f5d1b..59bf993 100644 --- a/source/commands/env/export/generators/RelationGenerator.ts +++ b/source/commands/env/export/generators/RelationGenerator.ts @@ -10,139 +10,151 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); interface RelationData { - key: string; - name: string; - subject_resource: string; - object_resource: string; - description?: string; + key: string; + name: string; + subject_resource: string; + object_resource: string; + description?: string; } export class RelationGenerator implements HCLGenerator { - name = 'relations'; - private template: HandlebarsTemplateDelegate; - private resourceKeys: Set = new Set(); - - constructor( - private permit: Permit, - private warningCollector: WarningCollector, - ) { - this.template = Handlebars.compile( - readFileSync(join(__dirname, '../templates/relation.hcl'), 'utf-8'), - ); - } - - private async loadResourceKeys(): Promise { - try { - const resources = await this.permit.api.resources.list(); - if (resources && Array.isArray(resources)) { - resources.forEach(resource => { - if (resource.key !== '__user') { - this.resourceKeys.add(createSafeId(resource.key)); - } - }); - } - } catch (error) { - this.warningCollector.addWarning(`Failed to load resources: ${error}`); - } - } - - private validateResource(resourceKey: string): boolean { - const safeKey = createSafeId(resourceKey); - if (!this.resourceKeys.has(safeKey)) { - this.warningCollector.addWarning(`Referenced resource "${resourceKey}" does not exist`); - return false; - } - return true; - } - - private validateRelation(relation: any): relation is RelationData { - const requiredFields = ['key', 'name', 'subject_resource', 'object_resource']; - const missingFields = requiredFields.filter(field => !relation[field]); - - if (missingFields.length > 0) { - this.warningCollector.addWarning( - `Relation "${relation.key || 'unknown'}" is missing required fields: ${missingFields.join(', ')}` - ); - return false; - } - - // Validate that referenced resources exist - if (!this.validateResource(relation.subject_resource) || - !this.validateResource(relation.object_resource)) { - return false; - } - - return true; - } - - private formatRelation(relation: RelationData): RelationData { - return { - key: createSafeId(relation.key), - name: relation.name, - subject_resource: createSafeId(relation.subject_resource), - object_resource: createSafeId(relation.object_resource), - ...(relation.description && { description: relation.description }) - }; - } - - async generateHCL(): Promise { - try { - // Load resources first for validation - await this.loadResourceKeys(); - - // Get all resources - const resources = await this.permit.api.resources.list(); - - if (!resources?.length) { - return ''; - } - - // Collect all relations - const allRelations = []; - for (const resource of resources) { - if (resource.key === '__user') { - continue; - } - - try { - const resourceRelations = await this.permit.api.resourceRelations.list({ - resourceKey: resource.key, - }); - - if (resourceRelations?.length) { - allRelations.push(...resourceRelations); - } - } catch (err) { - this.warningCollector.addWarning( - `Failed to fetch relations for resource ${resource.key}: ${err}` - ); - } - } - - if (!allRelations.length) { - return ''; - } - - // Remove duplicates and get unique relations - const uniqueRelations = Array.from( - new Map(allRelations.map(r => [r.key, r])).values() - ); - - // Validate and format relations - const validRelations = uniqueRelations - .filter(this.validateRelation.bind(this)) - .map(this.formatRelation.bind(this)); - - if (!validRelations.length) { - return ''; - } - - // Generate HCL - return '\n# Resource Relations\n' + this.template({ relations: validRelations }); - - } catch (error) { - this.warningCollector.addWarning(`Failed to export relations: ${error}`); - return ''; - } - } -} \ No newline at end of file + name = 'relations'; + private template: HandlebarsTemplateDelegate; + private resourceKeys: Set = new Set(); + + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/relation.hcl'), 'utf-8'), + ); + } + + private async loadResourceKeys(): Promise { + try { + const resources = await this.permit.api.resources.list(); + if (resources && Array.isArray(resources)) { + resources.forEach(resource => { + if (resource.key !== '__user') { + this.resourceKeys.add(createSafeId(resource.key)); + } + }); + } + } catch (error) { + this.warningCollector.addWarning(`Failed to load resources: ${error}`); + } + } + + private validateResource(resourceKey: string): boolean { + const safeKey = createSafeId(resourceKey); + if (!this.resourceKeys.has(safeKey)) { + this.warningCollector.addWarning( + `Referenced resource "${resourceKey}" does not exist`, + ); + return false; + } + return true; + } + + private validateRelation(relation: any): relation is RelationData { + const requiredFields = [ + 'key', + 'name', + 'subject_resource', + 'object_resource', + ]; + const missingFields = requiredFields.filter(field => !relation[field]); + + if (missingFields.length > 0) { + this.warningCollector.addWarning( + `Relation "${relation.key || 'unknown'}" is missing required fields: ${missingFields.join(', ')}`, + ); + return false; + } + + // Validate that referenced resources exist + if ( + !this.validateResource(relation.subject_resource) || + !this.validateResource(relation.object_resource) + ) { + return false; + } + + return true; + } + + private formatRelation(relation: RelationData): RelationData { + return { + key: createSafeId(relation.key), + name: relation.name, + subject_resource: createSafeId(relation.subject_resource), + object_resource: createSafeId(relation.object_resource), + ...(relation.description && { description: relation.description }), + }; + } + + async generateHCL(): Promise { + try { + // Load resources first for validation + await this.loadResourceKeys(); + + // Get all resources + const resources = await this.permit.api.resources.list(); + + if (!resources?.length) { + return ''; + } + + // Collect all relations + const allRelations = []; + for (const resource of resources) { + if (resource.key === '__user') { + continue; + } + + try { + const resourceRelations = + await this.permit.api.resourceRelations.list({ + resourceKey: resource.key, + }); + + if (resourceRelations?.length) { + allRelations.push(...resourceRelations); + } + } catch (err) { + this.warningCollector.addWarning( + `Failed to fetch relations for resource ${resource.key}: ${err}`, + ); + } + } + + if (!allRelations.length) { + return ''; + } + + // Remove duplicates and get unique relations + const uniqueRelations = Array.from( + new Map(allRelations.map(r => [r.key, r])).values(), + ); + + // Validate and format relations + const validRelations = uniqueRelations + .filter(this.validateRelation.bind(this)) + .map(this.formatRelation.bind(this)); + + if (!validRelations.length) { + return ''; + } + + // Generate HCL + return ( + '\n# Resource Relations\n' + + this.template({ relations: validRelations }) + ); + } catch (error) { + this.warningCollector.addWarning(`Failed to export relations: ${error}`); + return ''; + } + } +} diff --git a/source/commands/env/export/generators/ResourceGenerator.ts b/source/commands/env/export/generators/ResourceGenerator.ts index ddfdcb1..f0b0dd2 100644 --- a/source/commands/env/export/generators/ResourceGenerator.ts +++ b/source/commands/env/export/generators/ResourceGenerator.ts @@ -10,44 +10,43 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); interface Attribute { - type: string; - description?: string; + type: string; + description?: string; } export class ResourceGenerator implements HCLGenerator { - name = 'resources'; - private template: HandlebarsTemplateDelegate; + name = 'resources'; + private template: HandlebarsTemplateDelegate; - constructor( - private permit: Permit, - private warningCollector: WarningCollector, - ) { - this.template = Handlebars.compile( - readFileSync(join(__dirname, '../templates/resource.hcl'), 'utf-8') - ); - } + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/resource.hcl'), 'utf-8'), + ); + } - async generateHCL(): Promise { - try { - const resources = await this.permit.api.resources.list(); - const validResources = resources - .filter(resource => resource.key !== '__user') - .map(resource => ({ - key: createSafeId(resource.key), - name: resource.name, - description: resource.description, - urn: resource.urn, - actions: resource.actions || {}, - attributes: resource.attributes - })); + async generateHCL(): Promise { + try { + const resources = await this.permit.api.resources.list(); + const validResources = resources + .filter(resource => resource.key !== '__user') + .map(resource => ({ + key: createSafeId(resource.key), + name: resource.name, + description: resource.description, + urn: resource.urn, + actions: resource.actions || {}, + attributes: resource.attributes, + })); - if (validResources.length === 0) return ''; + if (validResources.length === 0) return ''; - return '\n# Resources\n' + this.template({ resources: validResources }); - - } catch (error) { - this.warningCollector.addWarning(`Failed to export resources: ${error}`); - return ''; - } - } -} \ No newline at end of file + return '\n# Resources\n' + this.template({ resources: validResources }); + } catch (error) { + this.warningCollector.addWarning(`Failed to export resources: ${error}`); + return ''; + } + } +} diff --git a/source/commands/env/export/generators/ResourceSetGenerator.ts b/source/commands/env/export/generators/ResourceSetGenerator.ts index 9beee15..951c23d 100644 --- a/source/commands/env/export/generators/ResourceSetGenerator.ts +++ b/source/commands/env/export/generators/ResourceSetGenerator.ts @@ -11,44 +11,45 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); export class ResourceSetGenerator implements HCLGenerator { - name = 'resource set'; - private template: HandlebarsTemplateDelegate; + name = 'resource set'; + private template: HandlebarsTemplateDelegate; - constructor( - private permit: Permit, - private warningCollector: WarningCollector, - ) { - this.template = Handlebars.compile( - readFileSync(join(__dirname, '../templates/resource-set.hcl'), 'utf-8') - ); - } + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/resource-set.hcl'), 'utf-8'), + ); + } - async generateHCL(): Promise { - try { - // Get all resource sets using the Permit SDK - const resourceSets = await this.permit.api.conditionSets.list({}); - - // Filter only resource sets (not user sets) - const validSets = resourceSets - .filter(set => set.type === 'resourceset') - .map(set => ({ - key: createSafeId(set.key), - name: set.name, - description: set.description, - conditions: typeof set.conditions === 'string' - ? set.conditions - : JSON.stringify(set.conditions), - resource: set.resource_id - })); + async generateHCL(): Promise { + try { + // Get all resource sets using the Permit SDK + const resourceSets = await this.permit.api.conditionSets.list({}); - if (validSets.length === 0) return ''; + // Filter only resource sets (not user sets) + const validSets = resourceSets + .filter(set => set.type === 'resourceset') + .map(set => ({ + key: createSafeId(set.key), + name: set.name, + description: set.description, + conditions: + typeof set.conditions === 'string' + ? set.conditions + : JSON.stringify(set.conditions), + resource: set.resource_id, + })); - return '\n# Resource Sets\n' + this.template({ sets: validSets }); - } catch (error) { - this.warningCollector.addWarning( - `Failed to export resource sets: ${error}`, - ); - return ''; - } - } -} \ No newline at end of file + if (validSets.length === 0) return ''; + + return '\n# Resource Sets\n' + this.template({ sets: validSets }); + } catch (error) { + this.warningCollector.addWarning( + `Failed to export resource sets: ${error}`, + ); + return ''; + } + } +} diff --git a/source/commands/env/export/generators/RoleDerivationGenerator.ts b/source/commands/env/export/generators/RoleDerivationGenerator.ts index 2e7c224..1428d17 100644 --- a/source/commands/env/export/generators/RoleDerivationGenerator.ts +++ b/source/commands/env/export/generators/RoleDerivationGenerator.ts @@ -9,124 +9,129 @@ import { fileURLToPath } from 'url'; const currentFilePath = fileURLToPath(import.meta.url); const currentDirPath = dirname(currentFilePath); - interface RoleDerivation { - resource: string; - role: string; - linked_by: string; - on_resource: string; - to_role: string; - dependencies: string[]; + resource: string; + role: string; + linked_by: string; + on_resource: string; + to_role: string; + dependencies: string[]; } export class RoleDerivationGenerator implements HCLGenerator { - name = 'role derivation'; - private template: HandlebarsTemplateDelegate; - - constructor( - private permit: Permit, - private warningCollector: WarningCollector, - ) { - this.template = Handlebars.compile( - readFileSync(join(currentDirPath, '../templates/role-derivation.hcl'), 'utf-8') - ); - } - - private getDependencies(derivation: RoleDerivation): string[] { - const dependencies = new Set(); - - // Add dependency on the source resource - dependencies.add(`permitio_resource.${createSafeId(derivation.resource)}`); - - // Add dependency on the target resource - dependencies.add(`permitio_resource.${createSafeId(derivation.on_resource)}`); - - // Add dependencies on both roles - dependencies.add(`permitio_role.${createSafeId(derivation.role)}`); - dependencies.add(`permitio_role.${createSafeId(derivation.to_role)}`); - - return Array.from(dependencies); - } - - async generateHCL(): Promise { - try { - const resources = await this.permit.api.resources.list(); - if (!resources?.length) return ''; - - const allDerivations: RoleDerivation[] = []; - - for (const resource of resources) { - try { - if (!resource.key) { - this.warningCollector.addWarning( - `Skipping resource with missing key: ${resource.id}` - ); - continue; - } - - const resourceRoles = await this.permit.api.resourceRoles.list({ - resourceKey: resource.key, - }); - - const validDerivations = resourceRoles - .filter(derivation => - derivation.resource && - derivation.role && - derivation.linked_by && - derivation.on_resource && - derivation.to_role - ) - .map(derivation => ({ - ...derivation, - resource: createSafeId(derivation.resource), - role: createSafeId(derivation.role), - linked_by: createSafeId(derivation.linked_by), - on_resource: createSafeId(derivation.on_resource), - to_role: createSafeId(derivation.to_role), - resource_id: createSafeId( - `${derivation.resource}_${derivation.role}_${derivation.linked_by}_${derivation.on_resource}_${derivation.to_role}` - ), - dependencies: [] // Initialize empty dependencies array - })); - - // Add dependencies for each derivation - validDerivations.forEach(derivation => { - derivation.dependencies = this.getDependencies(derivation); - }); - - if (validDerivations.length) { - allDerivations.push(...validDerivations); - } - } catch (err) { - this.warningCollector.addWarning( - `Failed to fetch role derivations for resource '${resource.key}': ${err}` - ); - continue; - } - } - - if (!allDerivations.length) { - return ''; - } - - const hcl = this.template({ - derivations: allDerivations.map(derivation => ({ - resource_id: derivation.resource_id, - resource: derivation.resource, - role: derivation.role, - linked_by: derivation.linked_by, - on_resource: derivation.on_resource, - to_role: derivation.to_role, - dependencies: derivation.dependencies - })) - }); - - return '\n# Role Derivations\n' + hcl; - } catch (error) { - this.warningCollector.addWarning( - `Failed to export role derivations: ${error}` - ); - return ''; - } - } -} \ No newline at end of file + name = 'role derivation'; + private template: HandlebarsTemplateDelegate; + + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync( + join(currentDirPath, '../templates/role-derivation.hcl'), + 'utf-8', + ), + ); + } + + private getDependencies(derivation: RoleDerivation): string[] { + const dependencies = new Set(); + + // Add dependency on the source resource + dependencies.add(`permitio_resource.${createSafeId(derivation.resource)}`); + + // Add dependency on the target resource + dependencies.add( + `permitio_resource.${createSafeId(derivation.on_resource)}`, + ); + + // Add dependencies on both roles + dependencies.add(`permitio_role.${createSafeId(derivation.role)}`); + dependencies.add(`permitio_role.${createSafeId(derivation.to_role)}`); + + return Array.from(dependencies); + } + + async generateHCL(): Promise { + try { + const resources = await this.permit.api.resources.list(); + if (!resources?.length) return ''; + + const allDerivations: RoleDerivation[] = []; + + for (const resource of resources) { + try { + if (!resource.key) { + this.warningCollector.addWarning( + `Skipping resource with missing key: ${resource.id}`, + ); + continue; + } + + const resourceRoles = await this.permit.api.resourceRoles.list({ + resourceKey: resource.key, + }); + + const validDerivations = resourceRoles + .filter( + derivation => + derivation.resource && + derivation.role && + derivation.linked_by && + derivation.on_resource && + derivation.to_role, + ) + .map(derivation => ({ + ...derivation, + resource: createSafeId(derivation.resource), + role: createSafeId(derivation.role), + linked_by: createSafeId(derivation.linked_by), + on_resource: createSafeId(derivation.on_resource), + to_role: createSafeId(derivation.to_role), + resource_id: createSafeId( + `${derivation.resource}_${derivation.role}_${derivation.linked_by}_${derivation.on_resource}_${derivation.to_role}`, + ), + dependencies: [], // Initialize empty dependencies array + })); + + // Add dependencies for each derivation + validDerivations.forEach(derivation => { + derivation.dependencies = this.getDependencies(derivation); + }); + + if (validDerivations.length) { + allDerivations.push(...validDerivations); + } + } catch (err) { + this.warningCollector.addWarning( + `Failed to fetch role derivations for resource '${resource.key}': ${err}`, + ); + continue; + } + } + + if (!allDerivations.length) { + return ''; + } + + const hcl = this.template({ + derivations: allDerivations.map(derivation => ({ + resource_id: derivation.resource_id, + resource: derivation.resource, + role: derivation.role, + linked_by: derivation.linked_by, + on_resource: derivation.on_resource, + to_role: derivation.to_role, + dependencies: derivation.dependencies, + })), + }); + + return '\n# Role Derivations\n' + hcl; + } catch (error) { + this.warningCollector.addWarning( + `Failed to export role derivations: ${error}`, + ); + return ''; + } + } +} diff --git a/source/commands/env/export/generators/RoleGenerator.ts b/source/commands/env/export/generators/RoleGenerator.ts index 4333897..1dbd844 100644 --- a/source/commands/env/export/generators/RoleGenerator.ts +++ b/source/commands/env/export/generators/RoleGenerator.ts @@ -9,57 +9,55 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -Handlebars.registerHelper('json', function(context) { - return `[${context.map(item => `"${item}"`).join(',')}]`; +Handlebars.registerHelper('json', function (context) { + return `[${context.map(item => `"${item}"`).join(',')}]`; }); export class RoleGenerator implements HCLGenerator { - name = 'roles'; - private template: HandlebarsTemplateDelegate; + name = 'roles'; + private template: HandlebarsTemplateDelegate; - constructor( - private permit: Permit, - private warningCollector: WarningCollector, - ) { - this.template = Handlebars.compile( - readFileSync(join(__dirname, '../templates/role.hcl'), 'utf-8') - ); - } + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/role.hcl'), 'utf-8'), + ); + } - async generateHCL(): Promise { - try { - const roles = await this.permit.api.roles.list(); - if (!roles || roles.length === 0) - return ''; - - // Transform roles and identify dependencies - const validRoles = roles.map(role => { - const dependencies = this.getDependencies(role.permissions); - return { - key: createSafeId(role.key), - name: role.name, - permissions: role.permissions || [], - dependencies: dependencies - }; - }); - - return '\n# Roles\n' + this.template({ roles: validRoles }); - } catch (error) { - this.warningCollector.addWarning(`Failed to export roles: ${error}`); - return ''; - } - } - - private getDependencies(permissions: string[]): string[] { - // Extract resource keys from permissions and generate dependency references - const resourceDeps = new Set(); - permissions?.forEach(perm => { - const [resource] = perm.split(':'); - if (resource) { - resourceDeps.add(`permitio_resource.${createSafeId(resource)}`); - } - }); - return Array.from(resourceDeps); - } - -} \ No newline at end of file + async generateHCL(): Promise { + try { + const roles = await this.permit.api.roles.list(); + if (!roles || roles.length === 0) return ''; + + // Transform roles and identify dependencies + const validRoles = roles.map(role => { + const dependencies = this.getDependencies(role.permissions); + return { + key: createSafeId(role.key), + name: role.name, + permissions: role.permissions || [], + dependencies: dependencies, + }; + }); + + return '\n# Roles\n' + this.template({ roles: validRoles }); + } catch (error) { + this.warningCollector.addWarning(`Failed to export roles: ${error}`); + return ''; + } + } + + private getDependencies(permissions: string[]): string[] { + // Extract resource keys from permissions and generate dependency references + const resourceDeps = new Set(); + permissions?.forEach(perm => { + const [resource] = perm.split(':'); + if (resource) { + resourceDeps.add(`permitio_resource.${createSafeId(resource)}`); + } + }); + return Array.from(resourceDeps); + } +} diff --git a/source/commands/env/export/generators/UserAttributesGenerator.ts b/source/commands/env/export/generators/UserAttributesGenerator.ts index 55c0900..fbbf722 100644 --- a/source/commands/env/export/generators/UserAttributesGenerator.ts +++ b/source/commands/env/export/generators/UserAttributesGenerator.ts @@ -10,39 +10,40 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); export class UserAttributesGenerator implements HCLGenerator { - name = 'user attributes'; - private template: HandlebarsTemplateDelegate; + name = 'user attributes'; + private template: HandlebarsTemplateDelegate; - constructor( - private permit: Permit, - private warningCollector: WarningCollector, - ) { - this.template = Handlebars.compile( - readFileSync(join(__dirname, '../templates/user-attribute.hcl'), 'utf-8') - ); - } + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/user-attribute.hcl'), 'utf-8'), + ); + } - async generateHCL(): Promise { - try { - const userAttributes = await this.permit.api.resourceAttributes.list({ - resourceKey: '__user', - }); + async generateHCL(): Promise { + try { + const userAttributes = await this.permit.api.resourceAttributes.list({ + resourceKey: '__user', + }); - if (!userAttributes || userAttributes.length === 0) - return ''; + if (!userAttributes || userAttributes.length === 0) return ''; - const validAttributes = userAttributes.map(attr => ({ - key: createSafeId(attr.key), - type: attr.type, - description: attr.description, - })); + const validAttributes = userAttributes.map(attr => ({ + key: createSafeId(attr.key), + type: attr.type, + description: attr.description, + })); - return '\n# User Attributes\n' + this.template({ attributes: validAttributes }); - } catch (error) { - this.warningCollector.addWarning( - `Failed to export user attributes: ${error}`, - ); - return ''; - } - } -} \ No newline at end of file + return ( + '\n# User Attributes\n' + this.template({ attributes: validAttributes }) + ); + } catch (error) { + this.warningCollector.addWarning( + `Failed to export user attributes: ${error}`, + ); + return ''; + } + } +} diff --git a/source/commands/env/export/generators/UserSetGenerator.ts b/source/commands/env/export/generators/UserSetGenerator.ts index 8d6abc7..d833c9d 100644 --- a/source/commands/env/export/generators/UserSetGenerator.ts +++ b/source/commands/env/export/generators/UserSetGenerator.ts @@ -10,44 +10,43 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); export class UserSetGenerator implements HCLGenerator { - name = 'user set'; - private template: HandlebarsTemplateDelegate; + name = 'user set'; + private template: HandlebarsTemplateDelegate; - constructor( - private permit: Permit, - private warningCollector: WarningCollector, - ) { - this.template = Handlebars.compile( - readFileSync(join(__dirname, '../templates/user-set.hcl'), 'utf-8') - ); - } + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/user-set.hcl'), 'utf-8'), + ); + } - async generateHCL(): Promise { - try { - // Get all condition sets using the Permit SDK - const conditionSets = await this.permit.api.conditionSets.list({}); - - // Filter only user sets (not resource sets) - const validSets = conditionSets - .filter(set => set.type === 'userset') - .map(set => ({ - key: createSafeId(set.key), - name: set.name, - description: set.description, - conditions: typeof set.conditions === 'string' - ? set.conditions - : JSON.stringify(set.conditions), - resource: set.resource_id - })); + async generateHCL(): Promise { + try { + // Get all condition sets using the Permit SDK + const conditionSets = await this.permit.api.conditionSets.list({}); - if (validSets.length === 0) return ''; + // Filter only user sets (not resource sets) + const validSets = conditionSets + .filter(set => set.type === 'userset') + .map(set => ({ + key: createSafeId(set.key), + name: set.name, + description: set.description, + conditions: + typeof set.conditions === 'string' + ? set.conditions + : JSON.stringify(set.conditions), + resource: set.resource_id, + })); - return '\n# User Sets\n' + this.template({ sets: validSets }); - } catch (error) { - this.warningCollector.addWarning( - `Failed to export user sets: ${error}`, - ); - return ''; - } - } -} \ No newline at end of file + if (validSets.length === 0) return ''; + + return '\n# User Sets\n' + this.template({ sets: validSets }); + } catch (error) { + this.warningCollector.addWarning(`Failed to export user sets: ${error}`); + return ''; + } + } +} diff --git a/source/components/export/ExportContent.tsx b/source/components/export/ExportContent.tsx index c5436f6..9937f9c 100644 --- a/source/components/export/ExportContent.tsx +++ b/source/components/export/ExportContent.tsx @@ -121,9 +121,7 @@ export const ExportContent: FC<{ options: ExportOptions }> = ({ {hclOutput} {/* Wrap HCL output in */} )} - {state.error && ( - {state.error} - )} + {state.error && {state.error}} ); -}; \ No newline at end of file +}; diff --git a/source/components/export/ExportStatus.tsx b/source/components/export/ExportStatus.tsx index df524c0..4e1ea06 100644 --- a/source/components/export/ExportStatus.tsx +++ b/source/components/export/ExportStatus.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Text } from 'ink'; import Spinner from 'ink-spinner'; -import { ExportState } from '../../commands/env/export/types.js' +import { ExportState } from '../../commands/env/export/types.js'; interface ExportStatusProps { state: ExportState; diff --git a/source/hooks/export/PermitSDK.ts b/source/hooks/export/PermitSDK.ts index 800f114..ec4badc 100644 --- a/source/hooks/export/PermitSDK.ts +++ b/source/hooks/export/PermitSDK.ts @@ -1,13 +1,16 @@ import { Permit } from 'permitio'; import React from 'react'; -export const usePermitSDK = (token: string, pdpUrl: string = 'http://localhost:7766') => { +export const usePermitSDK = ( + token: string, + pdpUrl: string = 'http://localhost:7766', +) => { return React.useMemo( () => new Permit({ token, - pdp: pdpUrl, + pdp: pdpUrl, }), [token, pdpUrl], ); -}; \ No newline at end of file +}; diff --git a/source/hooks/export/useExport.ts b/source/hooks/export/useExport.ts index 0614431..f97d500 100644 --- a/source/hooks/export/useExport.ts +++ b/source/hooks/export/useExport.ts @@ -1,7 +1,10 @@ import { useState } from 'react'; import { usePermitSDK } from './PermitSDK.js'; import { ExportState } from '../../commands/env/export/types.js'; -import { createWarningCollector, generateProviderBlock } from '../../commands/env/export/utils.js'; +import { + createWarningCollector, + generateProviderBlock, +} from '../../commands/env/export/utils.js'; import { ResourceGenerator } from '../../commands/env/export/generators/ResourceGenerator.js'; import { RoleGenerator } from '../../commands/env/export/generators/RoleGenerator.js'; import { UserAttributesGenerator } from '../../commands/env/export/generators/UserAttributesGenerator.js'; From bce2cbb5bc4a228b5052975cc3bb0bc084e7ce9c Mon Sep 17 00:00:00 2001 From: daveads Date: Sun, 5 Jan 2025 06:15:22 +0100 Subject: [PATCH 20/34] test --- tests/export/ConditionSetGenerator.test.ts | 122 ++++++------ tests/export/ExportContent.test.tsx | 2 +- tests/export/ExportStatus.test.tsx | 2 +- tests/export/ResourceGenerator.test.tsx | 159 +++++++-------- tests/export/ResourceSetGenerator.test.tsx | 146 ++++++++++++++ tests/export/RoleDerivationGenerator.test.ts | 199 +++++++++++++++++++ tests/export/RoleGenerator.test.tsx | 120 ++++++----- tests/export/UserSetGenerator.test.tsx | 139 +++++++++++++ 8 files changed, 694 insertions(+), 195 deletions(-) create mode 100644 tests/export/ResourceSetGenerator.test.tsx create mode 100644 tests/export/RoleDerivationGenerator.test.ts create mode 100644 tests/export/UserSetGenerator.test.tsx diff --git a/tests/export/ConditionSetGenerator.test.ts b/tests/export/ConditionSetGenerator.test.ts index ae19016..d813657 100644 --- a/tests/export/ConditionSetGenerator.test.ts +++ b/tests/export/ConditionSetGenerator.test.ts @@ -4,67 +4,69 @@ import { createWarningCollector } from '../../source/commands/env/export/utils.j import type { Permit } from 'permitio'; describe('ConditionSetGenerator', () => { - let generator: ConditionSetGenerator; - let mockPermit: { api: any }; - let warningCollector: ReturnType; + let generator: ConditionSetGenerator; + let mockPermit: { api: any }; + let warningCollector: ReturnType; - beforeEach(() => { - mockPermit = { - api: { - conditionSets: { - list: vi.fn().mockResolvedValue([ - { - key: 'us_employees', - name: 'US Employees', - type: 'userset', - description: 'Employees in US', - conditions: { country: 'US' }, - }, - { - key: 'confidential_docs', - name: 'Confidential Documents', - type: 'resourceset', - description: 'Confidential documents', - resource: 'document', - conditions: { classification: 'confidential' }, - }, - ]), - }, - }, - }; + beforeEach(() => { + mockPermit = { + api: { + conditionSets: { + list: vi.fn().mockResolvedValue([ + { + key: 'us_employees', + name: 'US Employees', + type: 'userset', + description: 'Employees in US', + conditions: { country: 'US' }, + }, + { + key: 'confidential_docs', + name: 'Confidential Documents', + type: 'resourceset', + description: 'Confidential documents', + resource: 'document', + conditions: { classification: 'confidential' }, + }, + ]), + }, + }, + }; + warningCollector = createWarningCollector(); + generator = new ConditionSetGenerator( + mockPermit as unknown as Permit, + warningCollector, + ); + }); - warningCollector = createWarningCollector(); - generator = new ConditionSetGenerator( - mockPermit as unknown as Permit, - warningCollector, - ); - }); + it('generates valid HCL for condition sets', async () => { + const hcl = await generator.generateHCL(); + + // Test for specific patterns using escaped quotes + expect(hcl).toContain('conditions = {"country":"US"}'); + expect(hcl).toContain('conditions = {"classification":"confidential"}'); + + // Test structural elements that don't involve escaped JSON + expect(hcl).toContain('resource "permitio_user_set" "us_employees"'); + expect(hcl).toContain('resource "permitio_resource_set" "confidential_docs"'); + expect(hcl).toContain('key = "us_employees"'); + expect(hcl).toContain('resource = "document"'); + }); - it('generates valid HCL for condition sets', async () => { - const hcl = await generator.generateHCL(); + it('handles empty condition sets', async () => { + mockPermit.api.conditionSets.list.mockResolvedValueOnce([]); + const hcl = await generator.generateHCL(); + expect(hcl).toBe(''); + }); - expect(hcl).toContain('resource "permitio_user_set" "us_employees"'); - expect(hcl).toContain( - 'resource "permitio_resource_set" "confidential_docs"', - ); - expect(hcl).toContain('conditions = {"country":"US"}'); - expect(hcl).toContain('conditions = {"classification":"confidential"}'); - }); - - it('handles empty condition sets', async () => { - mockPermit.api.conditionSets.list.mockResolvedValueOnce([]); - const hcl = await generator.generateHCL(); - expect(hcl).toBe(''); - }); - - it('handles errors and adds warnings', async () => { - mockPermit.api.conditionSets.list.mockRejectedValueOnce( - new Error('API Error'), - ); - const hcl = await generator.generateHCL(); - expect(hcl).toBe(''); - expect(warningCollector.getWarnings()[0]).toContain( - 'Failed to export condition sets', - ); - }); -}); + it('handles errors and adds warnings', async () => { + mockPermit.api.conditionSets.list.mockRejectedValueOnce( + new Error('API Error'), + ); + const hcl = await generator.generateHCL(); + expect(hcl).toBe(''); + expect(warningCollector.getWarnings()[0]).toContain( + 'Failed to export condition sets', + ); + }); +}); \ No newline at end of file diff --git a/tests/export/ExportContent.test.tsx b/tests/export/ExportContent.test.tsx index ee5af7c..15ab36d 100644 --- a/tests/export/ExportContent.test.tsx +++ b/tests/export/ExportContent.test.tsx @@ -1,7 +1,7 @@ import { expect, vi, describe, it, beforeEach } from 'vitest'; import React from 'react'; import { render } from 'ink-testing-library'; -import { ExportContent } from '../../source/commands/env/export/components/ExportContent.js'; +import { ExportContent } from '../../source/components/export/ExportContent.js' import { getMockPermit, mockValidateApiKeyScope } from './mocks/permit.js'; import { mockUseAuth } from './mocks/hooks'; diff --git a/tests/export/ExportStatus.test.tsx b/tests/export/ExportStatus.test.tsx index b2447eb..0394d3b 100644 --- a/tests/export/ExportStatus.test.tsx +++ b/tests/export/ExportStatus.test.tsx @@ -1,7 +1,7 @@ import { expect, describe, it } from 'vitest'; import React from 'react'; import { render } from 'ink-testing-library'; -import { ExportStatus } from '../../source/commands/env/export/components/ExportStatus.js'; +import { ExportStatus } from '../../source/components/export/ExportStatus.js'; describe('ExportStatus', () => { it('shows loading state', () => { diff --git a/tests/export/ResourceGenerator.test.tsx b/tests/export/ResourceGenerator.test.tsx index f5eb8f2..d55fec0 100644 --- a/tests/export/ResourceGenerator.test.tsx +++ b/tests/export/ResourceGenerator.test.tsx @@ -4,93 +4,86 @@ import { getMockPermit } from './mocks/permit'; import { createWarningCollector } from '../../source/commands/env/export/utils'; describe('ResourceGenerator', () => { - let generator: ResourceGenerator; + let generator: ResourceGenerator; - beforeEach(() => { - generator = new ResourceGenerator( - getMockPermit(), - createWarningCollector(), - ); - }); + beforeEach(() => { + generator = new ResourceGenerator( + getMockPermit(), + createWarningCollector(), + ); + }); - it('generates empty string when no resources exist', async () => { - const mockPermit = getMockPermit(); - mockPermit.api.resources.list.mockResolvedValueOnce([]); + it('generates empty string when no resources exist', async () => { + const mockPermit = getMockPermit(); + mockPermit.api.resources.list.mockResolvedValueOnce([]); + const generator = new ResourceGenerator( + mockPermit, + createWarningCollector(), + ); + const result = await generator.generateHCL(); + expect(result).toBe(''); + }); - const generator = new ResourceGenerator( - mockPermit, - createWarningCollector(), - ); - const result = await generator.generateHCL(); + it('skips internal user resource', async () => { + const mockPermit = getMockPermit(); + mockPermit.api.resources.list.mockResolvedValueOnce([ + { key: '__user', name: 'User', actions: {}, attributes: {} }, + ]); + const generator = new ResourceGenerator( + mockPermit, + createWarningCollector(), + ); + const result = await generator.generateHCL(); + expect(result).toBe(''); + }); - expect(result).toBe(''); - }); + it('generates valid HCL for resources', async () => { + const mockPermit = getMockPermit(); + mockPermit.api.resources.list.mockResolvedValueOnce([ + { + key: 'document', + name: 'Document', + description: 'A document resource', + actions: { + read: { name: 'Read', description: 'Read the document' }, + }, + attributes: { + owner: { type: 'string', description: 'The owner of the document' }, + }, + }, + ]); + const generator = new ResourceGenerator( + mockPermit, + createWarningCollector(), + ); + const result = await generator.generateHCL(); - it('skips internal user resource', async () => { - const mockPermit = getMockPermit(); - mockPermit.api.resources.list.mockResolvedValueOnce([ - { key: '__user', name: 'User', actions: {}, attributes: {} }, - ]); + expect(result).toContain('\n# Resources\n'); + expect(result).toContain('resource "permitio_resource" "document"'); + expect(result).toContain('key = "document"'); + expect(result).toContain('name = "Document"'); + expect(result).toContain('description = "A document resource"'); + expect(result).toContain('actions = {'); + expect(result).toContain('"read" = {'); + expect(result).toContain('name = "Read"'); + expect(result).toContain('description = "Read the document"'); + expect(result).toContain('attributes = {'); + expect(result).toContain('"owner" = {'); + expect(result).toContain('type = "string"'); + expect(result).toContain('description = "The owner of the document"'); + }); - const generator = new ResourceGenerator( - mockPermit, - createWarningCollector(), - ); - const result = await generator.generateHCL(); - - expect(result).toBe(''); - }); - - it('generates valid HCL for resources', async () => { - const mockPermit = getMockPermit(); - mockPermit.api.resources.list.mockResolvedValueOnce([ - { - key: 'document', - name: 'Document', - description: 'A document resource', - actions: { - read: { name: 'Read', description: 'Read the document' }, - }, - attributes: { - owner: { type: 'string', description: 'The owner of the document' }, - }, - }, - ]); - - const generator = new ResourceGenerator( - mockPermit, - createWarningCollector(), - ); - const result = await generator.generateHCL(); - - expect(result).toContain('\n# Resources\n'); - expect(result).toContain('resource "permitio_resource" "document"'); - expect(result).toContain('key = "document"'); - expect(result).toContain('name = "Document"'); - expect(result).toContain('description = "A document resource"'); - expect(result).toContain('actions = {'); - expect(result).toContain('"read" = {'); - expect(result).toContain('name = "Read"'); - expect(result).toContain('description = "Read the document"'); - expect(result).toContain('attributes = {'); - expect(result).toContain('"owner" = {'); - expect(result).toContain('type = "string"'); - expect(result).toContain('description = "The owner of the document"'); - }); - - it('handles errors when fetching resources', async () => { - const mockPermit = getMockPermit(); - mockPermit.api.resources.list.mockRejectedValueOnce( - new Error('Failed to fetch'), - ); - - const warningCollector = createWarningCollector(); - const generator = new ResourceGenerator(mockPermit, warningCollector); - const result = await generator.generateHCL(); - - expect(result).toBe(''); - expect(warningCollector.getWarnings()).toEqual([ - 'Failed to export resources: Error: Failed to fetch', - ]); - }); + it('handles errors when fetching resources', async () => { + const mockPermit = getMockPermit(); + mockPermit.api.resources.list.mockRejectedValueOnce( + new Error('Failed to fetch'), + ); + const warningCollector = createWarningCollector(); + const generator = new ResourceGenerator(mockPermit, warningCollector); + const result = await generator.generateHCL(); + expect(result).toBe(''); + expect(warningCollector.getWarnings()).toEqual([ + 'Failed to export resources: Error: Failed to fetch', + ]); + }); }); \ No newline at end of file diff --git a/tests/export/ResourceSetGenerator.test.tsx b/tests/export/ResourceSetGenerator.test.tsx new file mode 100644 index 0000000..091143e --- /dev/null +++ b/tests/export/ResourceSetGenerator.test.tsx @@ -0,0 +1,146 @@ +import { describe, it, expect, vi } from 'vitest'; +import { ResourceSetGenerator } from '../../source/commands/env/export/generators/ResourceSetGenerator.js'; +import { createWarningCollector } from '../../source/commands/env/export/utils'; + +describe('ResourceSetGenerator', () => { + const mockPermit = { + api: { + conditionSets: { + list: vi.fn().mockResolvedValue([ + { + key: 'test-set', + name: 'Test Set', + description: 'Test resource set', + type: 'resourceset', + conditions: { attribute: 'value' }, + resource_id: 'document', + }, + { + key: 'user-set', + name: 'User Set', + type: 'userset', // This should be filtered out + conditions: 'user.department == "IT"', + resource_id: 'user', + }, + ]), + }, + }, + }; + + it('generates valid HCL for resource sets', async () => { + const generator = new ResourceSetGenerator( + mockPermit as any, + createWarningCollector(), + ); + + const hcl = await generator.generateHCL(); + + // Basic structure checks + expect(hcl).toContain('# Resource Sets'); + expect(hcl).toContain('resource "permitio_resource_set" "test_set"'); + + // Field checks + expect(hcl).toContain('key = "test_set"'); + expect(hcl).toContain('name = "Test Set"'); + expect(hcl).toContain('description = "Test resource set"'); + expect(hcl).toContain('resource = "document"'); + expect(hcl).toContain('conditions = "{"attribute":"value"}"'); + + // Negative assertions + expect(hcl).not.toContain('user-set'); // Ensure user set is filtered out + expect(hcl).not.toContain('userset'); + + // Verify complete structure + expect(hcl.trim()).toMatchInlineSnapshot(`"# Resource Sets +resource "permitio_resource_set" "test_set" { + key = "test_set" + name = "Test Set" + description = "Test resource set" + resource = "document" + conditions = "{"attribute":"value"}" +}"`); + }); + + it('generates valid HCL for resource sets with string conditions', async () => { + const mockPermitWithStringConditions = { + api: { + conditionSets: { + list: vi.fn().mockResolvedValue([ + { + key: 'string-condition-set', + name: 'String Condition Set', + type: 'resourceset', + conditions: 'resource.owner == user.id', + resource_id: 'document', + }, + ]), + }, + }, + }; + + const generator = new ResourceSetGenerator( + mockPermitWithStringConditions as any, + createWarningCollector(), + ); + + const hcl = await generator.generateHCL(); + + expect(hcl).toContain('conditions = "resource.owner == user.id"'); + + expect(hcl.trim()).toMatchInlineSnapshot(`"# Resource Sets +resource "permitio_resource_set" "string_condition_set" { + key = "string_condition_set" + name = "String Condition Set" + resource = "document" + conditions = "resource.owner == user.id" +}"`); + }); + + it('returns empty string when no resource sets exist', async () => { + const emptyMockPermit = { + api: { + conditionSets: { + list: vi.fn().mockResolvedValue([ + { + key: 'user-set', + name: 'User Set', + type: 'userset', + conditions: 'user.department == "IT"', + }, + ]), + }, + }, + }; + + const generator = new ResourceSetGenerator( + emptyMockPermit as any, + createWarningCollector(), + ); + + const hcl = await generator.generateHCL(); + expect(hcl).toBe(''); + }); + + it('handles API errors gracefully', async () => { + const errorMockPermit = { + api: { + conditionSets: { + list: vi.fn().mockRejectedValue(new Error('API Error')), + }, + }, + }; + + const warningCollector = createWarningCollector(); + const generator = new ResourceSetGenerator( + errorMockPermit as any, + warningCollector, + ); + + const hcl = await generator.generateHCL(); + + expect(hcl).toBe(''); + expect(warningCollector.getWarnings()).toContain( + 'Failed to export resource sets: Error: API Error', + ); + }); +}); \ No newline at end of file diff --git a/tests/export/RoleDerivationGenerator.test.ts b/tests/export/RoleDerivationGenerator.test.ts new file mode 100644 index 0000000..7591dea --- /dev/null +++ b/tests/export/RoleDerivationGenerator.test.ts @@ -0,0 +1,199 @@ +import { describe, it, expect, vi } from 'vitest'; +import { RoleDerivationGenerator } from '../../source/commands/env/export/generators/RoleDerivationGenerator.js'; +import { createWarningCollector } from '../../source/commands/env/export/utils'; +import Handlebars from 'handlebars'; + +// Mock the file system operations +vi.mock('fs', () => ({ + readFileSync: vi.fn().mockReturnValue(`{{#each derivations}} +resource "permitio_role_derivation" "{{resource_id}}" { + resource = "{{resource}}" + role = "{{role}}" + linked_by = "{{linked_by}}" + on_resource = "{{on_resource}}" + to_role = "{{to_role}}" + {{#if dependencies}} + depends_on = [{{#each dependencies}}"{{this}}"{{#unless @last}},{{/unless}}{{/each}}] + {{/if}} +} +{{/each}}`), +})); + +// Register the json helper for Handlebars +Handlebars.registerHelper('json', function(context) { + return JSON.stringify(context); +}); + +describe('RoleDerivationGenerator', () => { + const mockPermit = { + api: { + resources: { + list: vi.fn().mockResolvedValue([ + { + id: 'res1', + key: 'document', + }, + { + id: 'res2', + key: 'folder', + }, + ]), + }, + resourceRoles: { + list: vi.fn().mockImplementation(({ resourceKey }) => { + if (resourceKey === 'document') { + return Promise.resolve([ + { + resource: 'document', + role: 'editor', + linked_by: 'parent', + on_resource: 'folder', + to_role: 'viewer', + }, + ]); + } + return Promise.resolve([]); + }), + }, + }, + }; + + it('generates valid HCL for role derivations', async () => { + const generator = new RoleDerivationGenerator( + mockPermit as any, + createWarningCollector(), + ); + + const hcl = await generator.generateHCL(); + + // Basic structure checks + expect(hcl).toContain('# Role Derivations'); + expect(hcl).toContain('resource "permitio_role_derivation"'); + + // Field checks - ensure we check exact formatting + expect(hcl).toContain('resource = "document"'); + expect(hcl).toContain('role = "editor"'); + expect(hcl).toContain('linked_by = "parent"'); + expect(hcl).toContain('on_resource = "folder"'); + expect(hcl).toContain('to_role = "viewer"'); + + // Dependencies check + expect(hcl).toContain('depends_on'); + expect(hcl).toContain('permitio_resource.document'); + expect(hcl).toContain('permitio_resource.folder'); + expect(hcl).toContain('permitio_role.editor'); + expect(hcl).toContain('permitio_role.viewer'); + + // Snapshot test with exact formatting + expect(hcl.trim()).toMatchInlineSnapshot(` + "# Role Derivations + resource "permitio_role_derivation" "document_editor_parent_folder_viewer" { + resource = "document" + role = "editor" + linked_by = "parent" + on_resource = "folder" + to_role = "viewer" + depends_on = ["permitio_resource.document","permitio_resource.folder","permitio_role.editor","permitio_role.viewer"] + }" + `); + }); + + it('handles resources with missing keys', async () => { + const mockPermitWithInvalidResource = { + api: { + resources: { + list: vi.fn().mockResolvedValue([ + { + id: 'res1', + // key is missing + }, + ]), + }, + resourceRoles: { + list: vi.fn().mockResolvedValue([]), + }, + }, + }; + + const warningCollector = createWarningCollector(); + const generator = new RoleDerivationGenerator( + mockPermitWithInvalidResource as any, + warningCollector, + ); + + const hcl = await generator.generateHCL(); + expect(hcl).toBe(''); + expect(warningCollector.getWarnings()).toContain( + 'Skipping resource with missing key: res1', + ); + }); + + it('handles API errors gracefully', async () => { + const errorMockPermit = { + api: { + resources: { + list: vi.fn().mockRejectedValue(new Error('API Error')), + }, + }, + }; + + const warningCollector = createWarningCollector(); + const generator = new RoleDerivationGenerator( + errorMockPermit as any, + warningCollector, + ); + + const hcl = await generator.generateHCL(); + expect(hcl).toBe(''); + expect(warningCollector.getWarnings()).toContain( + 'Failed to export role derivations: Error: API Error', + ); + }); + + it('handles empty resources list', async () => { + const emptyMockPermit = { + api: { + resources: { + list: vi.fn().mockResolvedValue([]), + }, + }, + }; + + const generator = new RoleDerivationGenerator( + emptyMockPermit as any, + createWarningCollector(), + ); + + const hcl = await generator.generateHCL(); + expect(hcl).toBe(''); + }); + + it('handles role derivation list errors', async () => { + const mockPermitWithDerivationError = { + api: { + resources: { + list: vi.fn().mockResolvedValue([ + { + id: 'res1', + key: 'document', + }, + ]), + }, + resourceRoles: { + list: vi.fn().mockRejectedValue(new Error('Derivation API Error')), + }, + }, + }; + + const warningCollector = createWarningCollector(); + const generator = new RoleDerivationGenerator( + mockPermitWithDerivationError as any, + warningCollector, + ); + + const hcl = await generator.generateHCL(); + expect(warningCollector.getWarnings()).toContain( + "Failed to fetch role derivations for resource 'document': Error: Derivation API Error", + ); + }); +}); \ No newline at end of file diff --git a/tests/export/RoleGenerator.test.tsx b/tests/export/RoleGenerator.test.tsx index e55fd88..078b3ed 100644 --- a/tests/export/RoleGenerator.test.tsx +++ b/tests/export/RoleGenerator.test.tsx @@ -3,54 +3,74 @@ import { RoleGenerator } from '../../source/commands/env/export/generators/RoleG import { createWarningCollector } from '../../source/commands/env/export/utils'; describe('RoleGenerator', () => { - const mockPermit = { - api: { - roles: { - list: vi.fn().mockResolvedValue([ - { - key: 'admin', - name: 'Administrator', - description: 'Admin role', - permissions: ['document:read', 'document:write'], - extends: ['viewer'], // This will be ignored in the HCL output - }, - ]), - }, - }, - }; - - it('generates valid HCL for roles', async () => { - const generator = new RoleGenerator( - mockPermit as any, - createWarningCollector(), - ); - const hcl = await generator.generateHCL(); - - expect(hcl).toContain('resource "permitio_role" "admin"'); - expect(hcl).toContain(' key = "admin"'); - expect(hcl).toContain(' name = "Administrator"'); - expect(hcl).toContain(' description = "Admin role"'); - expect(hcl).toContain('["document:read","document:write"]'); - expect(hcl).not.toContain('["viewer"]'); // Ensure "viewer" is not included - }); - - it('handles API errors gracefully', async () => { - const errorMockPermit = { - api: { - roles: { - list: vi.fn().mockRejectedValue(new Error('API Error')), - }, - }, - }; - const warningCollector = createWarningCollector(); - const generator = new RoleGenerator( - errorMockPermit as any, - warningCollector, - ); - const hcl = await generator.generateHCL(); - expect(hcl).toBe(''); - expect(warningCollector.getWarnings()).toContain( - 'Failed to export roles: Error: API Error', - ); - }); + const mockPermit = { + api: { + roles: { + list: vi.fn().mockResolvedValue([ + { + key: 'admin', + name: 'Administrator', + description: 'Admin role', + permissions: ['document:read', 'document:write'], + extends: ['viewer'], + }, + ]), + }, + }, + }; + + it('generates valid HCL for roles', async () => { + const generator = new RoleGenerator( + mockPermit as any, + createWarningCollector(), + ); + + const hcl = await generator.generateHCL(); + + // Basic structure checks + expect(hcl).toContain('# Roles'); + expect(hcl).toContain('resource "permitio_role" "admin"'); + + // Field checks + expect(hcl).toContain(' key = "admin"'); + expect(hcl).toContain(' name = "Administrator"'); + expect(hcl).toContain(' permissions = ["document:read","document:write"]'); + + // Dependency check + expect(hcl).toContain(' depends_on = [["permitio_resource.document"]]'); + + // Negative assertions + expect(hcl).not.toContain('["viewer"]'); // Ensure "viewer" is not included + + // Updated snapshot to match actual format + expect(hcl.trim()).toMatchInlineSnapshot(`"# Roles +resource "permitio_role" "admin" { + key = "admin" + name = "Administrator" + permissions = ["document:read","document:write"] + depends_on = [["permitio_resource.document"]] +}"`); + }); + + it('handles API errors gracefully', async () => { + const errorMockPermit = { + api: { + roles: { + list: vi.fn().mockRejectedValue(new Error('API Error')), + }, + }, + }; + + const warningCollector = createWarningCollector(); + const generator = new RoleGenerator( + errorMockPermit as any, + warningCollector, + ); + + const hcl = await generator.generateHCL(); + expect(hcl).toBe(''); + expect(warningCollector.getWarnings()).toContain( + 'Failed to export roles: Error: API Error', + ); + }); }); \ No newline at end of file diff --git a/tests/export/UserSetGenerator.test.tsx b/tests/export/UserSetGenerator.test.tsx new file mode 100644 index 0000000..68f6b33 --- /dev/null +++ b/tests/export/UserSetGenerator.test.tsx @@ -0,0 +1,139 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { UserSetGenerator } from '../../source/commands/env/export/generators/UserSetGenerator'; +import type { WarningCollector } from '../../source/commands/env/export/types'; +import { readFileSync } from 'fs'; +import { join } from 'path'; + +// Mock fs and path modules +vi.mock('fs', () => ({ + readFileSync: vi.fn().mockReturnValue(` + {{#each sets}} + resource "permitio_user_set" "{{key}}" { + key = "{{key}}" + name = "{{name}}" + {{#if description}} + description = "{{description}}" + {{/if}} + {{#if resource}} + resource = "{{resource}}" + {{/if}} + conditions = "{{conditions}}" + } + {{/each}} + `) +})); + +describe('UserSetGenerator', () => { + let warningCollector: WarningCollector; + let mockPermit: any; + + beforeEach(() => { + warningCollector = { + addWarning: vi.fn(), + getWarnings: vi.fn().mockReturnValue([]), + }; + mockPermit = { + api: { + conditionSets: { + list: vi.fn(), + }, + }, + }; + }); + + it('generates empty string when no condition sets exist', async () => { + mockPermit.api.conditionSets.list.mockResolvedValue([]); + const generator = new UserSetGenerator(mockPermit, warningCollector); + const result = await generator.generateHCL(); + expect(result).toBe(''); + }); + + it('generates valid HCL for user sets', async () => { + const mockConditionSets = [ + { + key: 'us_based', + name: 'US Based Users', + description: 'Users from United States', + type: 'userset', + conditions: { location: 'US' }, + resource_id: null, + }, + { + key: 'premium_users', + name: 'Premium Users', + type: 'userset', + conditions: 'user.subscription == "premium"', + resource_id: 'subscription', + }, + { + key: 'resource_set', + name: 'Resource Set', + type: 'resourceset', // This should be filtered out + conditions: {}, + }, + ]; + + mockPermit.api.conditionSets.list.mockResolvedValue(mockConditionSets); + const generator = new UserSetGenerator(mockPermit, warningCollector); + const result = await generator.generateHCL(); + + // Check for header + expect(result).toContain('# User Sets'); + + // Check first user set + expect(result).toContain('resource "permitio_user_set" "us_based"'); + expect(result).toContain('key = "us_based"'); + expect(result).toContain('name = "US Based Users"'); + expect(result).toContain('description = "Users from United States"'); + expect(result).toContain('conditions = "{"location":"US"}"'); + + // Check second user set + expect(result).toContain('resource "permitio_user_set" "premium_users"'); + expect(result).toContain('key = "premium_users"'); + expect(result).toContain('name = "Premium Users"'); + expect(result).toContain('resource = "subscription"'); + expect(result).toContain('conditions = "user.subscription == "premium""'); + + // Ensure resource set is not included + expect(result).not.toContain('resource_set'); + }); + + it('handles errors when fetching condition sets', async () => { + mockPermit.api.conditionSets.list.mockRejectedValue(new Error('API error')); + const generator = new UserSetGenerator(mockPermit, warningCollector); + const result = await generator.generateHCL(); + + expect(result).toBe(''); + expect(warningCollector.addWarning).toHaveBeenCalledWith( + expect.stringContaining('Failed to export user sets') + ); + }); + + it('handles empty or invalid conditions', async () => { + const mockConditionSets = [ + { + key: 'empty_conditions', + name: 'Empty Conditions', + type: 'userset', + conditions: '', + resource_id: null, + }, + { + key: 'null_conditions', + name: 'Null Conditions', + type: 'userset', + conditions: null, + resource_id: null, + }, + ]; + + mockPermit.api.conditionSets.list.mockResolvedValue(mockConditionSets); + const generator = new UserSetGenerator(mockPermit, warningCollector); + const result = await generator.generateHCL(); + + expect(result).toContain('resource "permitio_user_set" "empty_conditions"'); + expect(result).toContain('conditions = ""'); + expect(result).toContain('resource "permitio_user_set" "null_conditions"'); + expect(result).toContain('conditions = "null"'); + }); +}); \ No newline at end of file From e1231c486f7b568cabb33e77808d4e772bce20f9 Mon Sep 17 00:00:00 2001 From: daveads Date: Sun, 5 Jan 2025 06:39:02 +0100 Subject: [PATCH 21/34] typescript errors --- .../export/generators/ResourceGenerator.ts | 5 -- .../generators/RoleDerivationGenerator.ts | 80 +++++++++---------- .../env/export/generators/RoleGenerator.ts | 6 +- 3 files changed, 42 insertions(+), 49 deletions(-) diff --git a/source/commands/env/export/generators/ResourceGenerator.ts b/source/commands/env/export/generators/ResourceGenerator.ts index f0b0dd2..f4351f1 100644 --- a/source/commands/env/export/generators/ResourceGenerator.ts +++ b/source/commands/env/export/generators/ResourceGenerator.ts @@ -9,11 +9,6 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -interface Attribute { - type: string; - description?: string; -} - export class ResourceGenerator implements HCLGenerator { name = 'resources'; private template: HandlebarsTemplateDelegate; diff --git a/source/commands/env/export/generators/RoleDerivationGenerator.ts b/source/commands/env/export/generators/RoleDerivationGenerator.ts index 1428d17..c5caead 100644 --- a/source/commands/env/export/generators/RoleDerivationGenerator.ts +++ b/source/commands/env/export/generators/RoleDerivationGenerator.ts @@ -5,11 +5,13 @@ import Handlebars from 'handlebars'; import { readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; +import { ResourceRoleRead } from 'permitio/build/main/openapi/types'; const currentFilePath = fileURLToPath(import.meta.url); const currentDirPath = dirname(currentFilePath); -interface RoleDerivation { +interface RoleDerivationData { + resource_id: string; resource: string; role: string; linked_by: string; @@ -34,7 +36,7 @@ export class RoleDerivationGenerator implements HCLGenerator { ); } - private getDependencies(derivation: RoleDerivation): string[] { + private getDependencies(derivation: RoleDerivationData): string[] { const dependencies = new Set(); // Add dependency on the source resource @@ -52,12 +54,39 @@ export class RoleDerivationGenerator implements HCLGenerator { return Array.from(dependencies); } + private convertToDerivationData( + role: ResourceRoleRead, + ): RoleDerivationData | null { + // Check that all required properties exist and are non-empty + if (!role.resource || !role.name || !role.id) { + return null; + } + + // Create unique ID based on available properties + const resource_id = createSafeId( + `${role.resource}_${role.name}_${role.id}`, + ); + + // Create derivation data structure + const derivation: RoleDerivationData = { + resource_id, + resource: createSafeId(role.resource), + role: createSafeId(role.name), + linked_by: role.id, // Using ID as linked_by + on_resource: createSafeId(role.resource), // Using same resource as on_resource + to_role: createSafeId(role.name), + dependencies: [], + }; + + return derivation; + } + async generateHCL(): Promise { try { const resources = await this.permit.api.resources.list(); if (!resources?.length) return ''; - const allDerivations: RoleDerivation[] = []; + const allDerivations: RoleDerivationData[] = []; for (const resource of resources) { try { @@ -72,35 +101,12 @@ export class RoleDerivationGenerator implements HCLGenerator { resourceKey: resource.key, }); - const validDerivations = resourceRoles - .filter( - derivation => - derivation.resource && - derivation.role && - derivation.linked_by && - derivation.on_resource && - derivation.to_role, - ) - .map(derivation => ({ - ...derivation, - resource: createSafeId(derivation.resource), - role: createSafeId(derivation.role), - linked_by: createSafeId(derivation.linked_by), - on_resource: createSafeId(derivation.on_resource), - to_role: createSafeId(derivation.to_role), - resource_id: createSafeId( - `${derivation.resource}_${derivation.role}_${derivation.linked_by}_${derivation.on_resource}_${derivation.to_role}`, - ), - dependencies: [], // Initialize empty dependencies array - })); - - // Add dependencies for each derivation - validDerivations.forEach(derivation => { - derivation.dependencies = this.getDependencies(derivation); - }); - - if (validDerivations.length) { - allDerivations.push(...validDerivations); + for (const role of resourceRoles) { + const derivation = this.convertToDerivationData(role); + if (derivation) { + derivation.dependencies = this.getDependencies(derivation); + allDerivations.push(derivation); + } } } catch (err) { this.warningCollector.addWarning( @@ -115,15 +121,7 @@ export class RoleDerivationGenerator implements HCLGenerator { } const hcl = this.template({ - derivations: allDerivations.map(derivation => ({ - resource_id: derivation.resource_id, - resource: derivation.resource, - role: derivation.role, - linked_by: derivation.linked_by, - on_resource: derivation.on_resource, - to_role: derivation.to_role, - dependencies: derivation.dependencies, - })), + derivations: allDerivations, }); return '\n# Role Derivations\n' + hcl; diff --git a/source/commands/env/export/generators/RoleGenerator.ts b/source/commands/env/export/generators/RoleGenerator.ts index 1dbd844..a9d8614 100644 --- a/source/commands/env/export/generators/RoleGenerator.ts +++ b/source/commands/env/export/generators/RoleGenerator.ts @@ -9,8 +9,8 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -Handlebars.registerHelper('json', function (context) { - return `[${context.map(item => `"${item}"`).join(',')}]`; +Handlebars.registerHelper('json', function (context: string[]) { + return `[${context.map((item: string) => `"${item}"`).join(',')}]`; }); export class RoleGenerator implements HCLGenerator { @@ -33,7 +33,7 @@ export class RoleGenerator implements HCLGenerator { // Transform roles and identify dependencies const validRoles = roles.map(role => { - const dependencies = this.getDependencies(role.permissions); + const dependencies = this.getDependencies(role.permissions || []); // Fix: Provide default value return { key: createSafeId(role.key), name: role.name, From 508c3a3b4d437de008de852a63b613034b78b9e5 Mon Sep 17 00:00:00 2001 From: daveads Date: Sun, 5 Jan 2025 10:31:12 +0100 Subject: [PATCH 22/34] lint.. --- .../generators/ConditionSetGenerator.ts | 27 ++++++++++++++++--- .../export/generators/RelationGenerator.ts | 22 +++++++++++---- .../export/generators/ResourceGenerator.ts | 14 ++++++++-- .../export/generators/ResourceSetGenerator.ts | 18 +++++++++---- .../generators/RoleDerivationGenerator.ts | 4 +-- .../env/export/generators/RoleGenerator.ts | 13 +++++++-- .../generators/UserAttributesGenerator.ts | 11 ++++++-- .../env/export/generators/UserSetGenerator.ts | 19 +++++++++---- 8 files changed, 101 insertions(+), 27 deletions(-) diff --git a/source/commands/env/export/generators/ConditionSetGenerator.ts b/source/commands/env/export/generators/ConditionSetGenerator.ts index 12327a9..6753733 100644 --- a/source/commands/env/export/generators/ConditionSetGenerator.ts +++ b/source/commands/env/export/generators/ConditionSetGenerator.ts @@ -1,17 +1,36 @@ import { Permit } from 'permitio'; import { HCLGenerator, WarningCollector } from '../types.js'; import { createSafeId } from '../utils.js'; -import Handlebars from 'handlebars'; +import Handlebars, { TemplateDelegate } from 'handlebars'; import { readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; +import { ResourceRead } from 'permitio/build/main/openapi/types'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +interface ConditionSetData { + key: string; + name: string; + description?: string; + conditions: string; + resource?: ResourceRead; + resourceType: string; +} + +type ValidConditionSet = { + key: string; + name: string; + description?: string; + conditions: string; + resource?: ResourceRead; + resourceType: string; +} | null; + export class ConditionSetGenerator implements HCLGenerator { name = 'condition sets'; - private template: HandlebarsTemplateDelegate; + private template: TemplateDelegate<{ conditionSets: ConditionSetData[] }>; constructor( private permit: Permit, @@ -34,7 +53,7 @@ export class ConditionSetGenerator implements HCLGenerator { } const validSets = conditionSets - .map(set => { + .map(set => { try { const isResourceSet = set.type === 'resourceset'; const resourceType = isResourceSet ? 'resource_set' : 'user_set'; @@ -58,7 +77,7 @@ export class ConditionSetGenerator implements HCLGenerator { return null; } }) - .filter(Boolean); // Remove null values from failed conversions + .filter((set): set is ConditionSetData => set !== null); if (validSets.length === 0) return ''; diff --git a/source/commands/env/export/generators/RelationGenerator.ts b/source/commands/env/export/generators/RelationGenerator.ts index 59bf993..ad6e794 100644 --- a/source/commands/env/export/generators/RelationGenerator.ts +++ b/source/commands/env/export/generators/RelationGenerator.ts @@ -1,7 +1,7 @@ import { Permit } from 'permitio'; import { HCLGenerator, WarningCollector } from '../types.js'; import { createSafeId } from '../utils.js'; -import Handlebars from 'handlebars'; +import Handlebars, { TemplateDelegate } from 'handlebars'; import { readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; @@ -15,11 +15,21 @@ interface RelationData { subject_resource: string; object_resource: string; description?: string; + [key: string]: unknown; +} + +interface RawRelation { + key: string; + name: string; + subject_resource: string; + object_resource: string; + description?: string; + [key: string]: unknown; } export class RelationGenerator implements HCLGenerator { name = 'relations'; - private template: HandlebarsTemplateDelegate; + private template: TemplateDelegate; private resourceKeys: Set = new Set(); constructor( @@ -57,7 +67,7 @@ export class RelationGenerator implements HCLGenerator { return true; } - private validateRelation(relation: any): relation is RelationData { + private validateRelation(relation: RawRelation): relation is RelationData { const requiredFields = [ 'key', 'name', @@ -107,7 +117,7 @@ export class RelationGenerator implements HCLGenerator { } // Collect all relations - const allRelations = []; + const allRelations: RawRelation[] = []; for (const resource of resources) { if (resource.key === '__user') { continue; @@ -120,7 +130,9 @@ export class RelationGenerator implements HCLGenerator { }); if (resourceRelations?.length) { - allRelations.push(...resourceRelations); + allRelations.push( + ...(resourceRelations as unknown as RawRelation[]), + ); } } catch (err) { this.warningCollector.addWarning( diff --git a/source/commands/env/export/generators/ResourceGenerator.ts b/source/commands/env/export/generators/ResourceGenerator.ts index f4351f1..b89e22b 100644 --- a/source/commands/env/export/generators/ResourceGenerator.ts +++ b/source/commands/env/export/generators/ResourceGenerator.ts @@ -1,7 +1,7 @@ import { Permit } from 'permitio'; import { HCLGenerator, WarningCollector } from '../types.js'; import { createSafeId } from '../utils.js'; -import Handlebars from 'handlebars'; +import Handlebars, { TemplateDelegate } from 'handlebars'; import { readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; @@ -9,9 +9,19 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +// Define a proper type for the resource object +interface ResourceData { + key: string; + name: string; + description?: string; + urn?: string; + actions: Record; + attributes?: Record; +} + export class ResourceGenerator implements HCLGenerator { name = 'resources'; - private template: HandlebarsTemplateDelegate; + private template: TemplateDelegate<{ resources: ResourceData[] }>; constructor( private permit: Permit, diff --git a/source/commands/env/export/generators/ResourceSetGenerator.ts b/source/commands/env/export/generators/ResourceSetGenerator.ts index 951c23d..d533299 100644 --- a/source/commands/env/export/generators/ResourceSetGenerator.ts +++ b/source/commands/env/export/generators/ResourceSetGenerator.ts @@ -1,8 +1,7 @@ -// ResourceSetGenerator.ts import { Permit } from 'permitio'; import { HCLGenerator, WarningCollector } from '../types.js'; import { createSafeId } from '../utils.js'; -import Handlebars from 'handlebars'; +import Handlebars, { TemplateDelegate } from 'handlebars'; import { readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; @@ -10,9 +9,18 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +// Define a proper type for the resource set object +interface ResourceSetData { + key: string; + name: string; + description?: string; + conditions: string; + resource: string; +} + export class ResourceSetGenerator implements HCLGenerator { name = 'resource set'; - private template: HandlebarsTemplateDelegate; + private template: TemplateDelegate<{ sets: ResourceSetData[] }>; constructor( private permit: Permit, @@ -28,7 +36,7 @@ export class ResourceSetGenerator implements HCLGenerator { // Get all resource sets using the Permit SDK const resourceSets = await this.permit.api.conditionSets.list({}); - // Filter only resource sets (not user sets) + // Filter only resource sets (not user sets) and ensure `resource_id` is defined const validSets = resourceSets .filter(set => set.type === 'resourceset') .map(set => ({ @@ -39,7 +47,7 @@ export class ResourceSetGenerator implements HCLGenerator { typeof set.conditions === 'string' ? set.conditions : JSON.stringify(set.conditions), - resource: set.resource_id, + resource: set.resource_id?.toString() || '', })); if (validSets.length === 0) return ''; diff --git a/source/commands/env/export/generators/RoleDerivationGenerator.ts b/source/commands/env/export/generators/RoleDerivationGenerator.ts index c5caead..75b7ffd 100644 --- a/source/commands/env/export/generators/RoleDerivationGenerator.ts +++ b/source/commands/env/export/generators/RoleDerivationGenerator.ts @@ -1,7 +1,7 @@ import { Permit } from 'permitio'; import { HCLGenerator, WarningCollector } from '../types.js'; import { createSafeId } from '../utils.js'; -import Handlebars from 'handlebars'; +import Handlebars, { TemplateDelegate } from 'handlebars'; import { readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; @@ -22,7 +22,7 @@ interface RoleDerivationData { export class RoleDerivationGenerator implements HCLGenerator { name = 'role derivation'; - private template: HandlebarsTemplateDelegate; + private template: TemplateDelegate<{ derivations: RoleDerivationData[] }>; constructor( private permit: Permit, diff --git a/source/commands/env/export/generators/RoleGenerator.ts b/source/commands/env/export/generators/RoleGenerator.ts index a9d8614..ea55c2c 100644 --- a/source/commands/env/export/generators/RoleGenerator.ts +++ b/source/commands/env/export/generators/RoleGenerator.ts @@ -1,7 +1,7 @@ import { Permit } from 'permitio'; import { HCLGenerator, WarningCollector } from '../types.js'; import { createSafeId } from '../utils.js'; -import Handlebars from 'handlebars'; +import Handlebars, { TemplateDelegate } from 'handlebars'; import { readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; @@ -9,13 +9,22 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +// Define a proper type for the role object +interface RoleData { + key: string; + name: string; + permissions: string[]; + dependencies: string[]; +} + +// Register Handlebars helper Handlebars.registerHelper('json', function (context: string[]) { return `[${context.map((item: string) => `"${item}"`).join(',')}]`; }); export class RoleGenerator implements HCLGenerator { name = 'roles'; - private template: HandlebarsTemplateDelegate; + private template: TemplateDelegate<{ roles: RoleData[] }>; constructor( private permit: Permit, diff --git a/source/commands/env/export/generators/UserAttributesGenerator.ts b/source/commands/env/export/generators/UserAttributesGenerator.ts index fbbf722..b06bc60 100644 --- a/source/commands/env/export/generators/UserAttributesGenerator.ts +++ b/source/commands/env/export/generators/UserAttributesGenerator.ts @@ -1,7 +1,7 @@ import { Permit } from 'permitio'; import { HCLGenerator, WarningCollector } from '../types.js'; import { createSafeId } from '../utils.js'; -import Handlebars from 'handlebars'; +import Handlebars, { TemplateDelegate } from 'handlebars'; import { readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; @@ -9,9 +9,16 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +// Define a proper type for the user attribute object +interface UserAttributeData { + key: string; + type: string; + description?: string; +} + export class UserAttributesGenerator implements HCLGenerator { name = 'user attributes'; - private template: HandlebarsTemplateDelegate; + private template: TemplateDelegate<{ attributes: UserAttributeData[] }>; constructor( private permit: Permit, diff --git a/source/commands/env/export/generators/UserSetGenerator.ts b/source/commands/env/export/generators/UserSetGenerator.ts index d833c9d..d23aeae 100644 --- a/source/commands/env/export/generators/UserSetGenerator.ts +++ b/source/commands/env/export/generators/UserSetGenerator.ts @@ -1,7 +1,7 @@ import { Permit } from 'permitio'; import { HCLGenerator, WarningCollector } from '../types.js'; import { createSafeId } from '../utils.js'; -import Handlebars from 'handlebars'; +import Handlebars, { TemplateDelegate } from 'handlebars'; import { readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; @@ -9,9 +9,18 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +// Define a proper type for the user set object +interface UserSetData { + key: string; + name: string; + description?: string; + conditions: string; + resource: string; // Ensure `resource` is always a string +} + export class UserSetGenerator implements HCLGenerator { name = 'user set'; - private template: HandlebarsTemplateDelegate; + private template: TemplateDelegate<{ sets: UserSetData[] }>; constructor( private permit: Permit, @@ -27,9 +36,9 @@ export class UserSetGenerator implements HCLGenerator { // Get all condition sets using the Permit SDK const conditionSets = await this.permit.api.conditionSets.list({}); - // Filter only user sets (not resource sets) + // Filter only user sets (not resource sets) and ensure `resource_id` is defined const validSets = conditionSets - .filter(set => set.type === 'userset') + .filter(set => set.type === 'userset' && set.resource_id !== undefined) // Ensure `resource_id` is defined .map(set => ({ key: createSafeId(set.key), name: set.name, @@ -38,7 +47,7 @@ export class UserSetGenerator implements HCLGenerator { typeof set.conditions === 'string' ? set.conditions : JSON.stringify(set.conditions), - resource: set.resource_id, + resource: set.resource_id, // `resource_id` is now guaranteed to be defined })); if (validSets.length === 0) return ''; From 39d159b4eac09986e498012f92a91ea214d3dcac Mon Sep 17 00:00:00 2001 From: daveads Date: Sun, 5 Jan 2025 10:41:34 +0100 Subject: [PATCH 23/34] lint.. --- .../export/generators/ResourceGenerator.ts | 132 ++++++++++++------ .../env/export/generators/UserSetGenerator.ts | 88 ++++++------ 2 files changed, 133 insertions(+), 87 deletions(-) diff --git a/source/commands/env/export/generators/ResourceGenerator.ts b/source/commands/env/export/generators/ResourceGenerator.ts index b89e22b..3d5372c 100644 --- a/source/commands/env/export/generators/ResourceGenerator.ts +++ b/source/commands/env/export/generators/ResourceGenerator.ts @@ -9,49 +9,97 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -// Define a proper type for the resource object interface ResourceData { - key: string; - name: string; - description?: string; - urn?: string; - actions: Record; - attributes?: Record; + key: string; + name: string; + description?: string; + urn?: string; + actions: Record; + attributes?: Record; } -export class ResourceGenerator implements HCLGenerator { - name = 'resources'; - private template: TemplateDelegate<{ resources: ResourceData[] }>; - - constructor( - private permit: Permit, - private warningCollector: WarningCollector, - ) { - this.template = Handlebars.compile( - readFileSync(join(__dirname, '../templates/resource.hcl'), 'utf-8'), - ); - } - - async generateHCL(): Promise { - try { - const resources = await this.permit.api.resources.list(); - const validResources = resources - .filter(resource => resource.key !== '__user') - .map(resource => ({ - key: createSafeId(resource.key), - name: resource.name, - description: resource.description, - urn: resource.urn, - actions: resource.actions || {}, - attributes: resource.attributes, - })); - - if (validResources.length === 0) return ''; - - return '\n# Resources\n' + this.template({ resources: validResources }); - } catch (error) { - this.warningCollector.addWarning(`Failed to export resources: ${error}`); - return ''; - } - } +interface ActionData { + name: string; + description?: string; +} + +// Define a type for attributes +interface AttributeData { + type: string; + required?: boolean; +} + +interface ActionBlockRead { + name?: string; + description?: string; +} + +interface AttributeBlockRead { + type?: string; + required?: boolean; } + +export class ResourceGenerator implements HCLGenerator { + name = 'resources'; + private template: TemplateDelegate<{ resources: ResourceData[] }>; + + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/resource.hcl'), 'utf-8'), + ); + } + + async generateHCL(): Promise { + try { + const resources = await this.permit.api.resources.list(); + const validResources = resources + .filter((resource) => resource.key !== '__user') + .map((resource) => ({ + key: createSafeId(resource.key), + name: resource.name, + description: resource.description, + urn: resource.urn, + actions: this.transformActions(resource.actions || {}), + attributes: this.transformAttributes(resource.attributes), + })); + + if (validResources.length === 0) return ''; + + return '\n# Resources\n' + this.template({ resources: validResources }); + } catch (error) { + this.warningCollector.addWarning(`Failed to export resources: ${error}`); + return ''; + } + } + + // Helper function to transform actions + private transformActions(actions: Record): Record { + const transformedActions: Record = {}; + for (const [key, action] of Object.entries(actions)) { + transformedActions[key] = { + name: action.name || key, + description: action.description, + }; + } + return transformedActions; + } + + // Helper function to transform attributes + private transformAttributes( + attributes: Record | undefined, + ): Record | undefined { + if (!attributes) return undefined; + + const transformedAttributes: Record = {}; + for (const [key, attribute] of Object.entries(attributes)) { + transformedAttributes[key] = { + type: attribute.type || 'string', + required: attribute.required || false, + }; + } + return transformedAttributes; + } +} \ No newline at end of file diff --git a/source/commands/env/export/generators/UserSetGenerator.ts b/source/commands/env/export/generators/UserSetGenerator.ts index d23aeae..0721388 100644 --- a/source/commands/env/export/generators/UserSetGenerator.ts +++ b/source/commands/env/export/generators/UserSetGenerator.ts @@ -9,53 +9,51 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -// Define a proper type for the user set object + interface UserSetData { - key: string; - name: string; - description?: string; - conditions: string; - resource: string; // Ensure `resource` is always a string + key: string; + name: string; + description?: string; + conditions: string; + resource: string; } export class UserSetGenerator implements HCLGenerator { - name = 'user set'; - private template: TemplateDelegate<{ sets: UserSetData[] }>; - - constructor( - private permit: Permit, - private warningCollector: WarningCollector, - ) { - this.template = Handlebars.compile( - readFileSync(join(__dirname, '../templates/user-set.hcl'), 'utf-8'), - ); - } - - async generateHCL(): Promise { - try { - // Get all condition sets using the Permit SDK - const conditionSets = await this.permit.api.conditionSets.list({}); - - // Filter only user sets (not resource sets) and ensure `resource_id` is defined - const validSets = conditionSets - .filter(set => set.type === 'userset' && set.resource_id !== undefined) // Ensure `resource_id` is defined - .map(set => ({ - key: createSafeId(set.key), - name: set.name, - description: set.description, - conditions: - typeof set.conditions === 'string' - ? set.conditions - : JSON.stringify(set.conditions), - resource: set.resource_id, // `resource_id` is now guaranteed to be defined - })); - - if (validSets.length === 0) return ''; - - return '\n# User Sets\n' + this.template({ sets: validSets }); - } catch (error) { - this.warningCollector.addWarning(`Failed to export user sets: ${error}`); - return ''; - } - } + name = 'user set'; + private template: TemplateDelegate<{ sets: UserSetData[] }>; + + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/user-set.hcl'), 'utf-8'), + ); + } + + async generateHCL(): Promise { + try { + const conditionSets = await this.permit.api.conditionSets.list({}); + + const validSets = conditionSets + .filter((set) => set.type === 'userset') + .map((set) => ({ + key: createSafeId(set.key), + name: set.name, + description: set.description, + conditions: + typeof set.conditions === 'string' + ? set.conditions + : JSON.stringify(set.conditions), + resource: set.resource_id?.toString() || '', + })); + + if (validSets.length === 0) return ''; + + return '\n# User Sets\n' + this.template({ sets: validSets }); + } catch (error) { + this.warningCollector.addWarning(`Failed to export user sets: ${error}`); + return ''; + } + } } From b2a591c740ec251737594c27645da36300282931 Mon Sep 17 00:00:00 2001 From: daveads Date: Sun, 5 Jan 2025 10:42:37 +0100 Subject: [PATCH 24/34] format --- .../export/generators/ResourceGenerator.ts | 142 +++++++++--------- .../env/export/generators/UserSetGenerator.ts | 85 ++++++----- 2 files changed, 114 insertions(+), 113 deletions(-) diff --git a/source/commands/env/export/generators/ResourceGenerator.ts b/source/commands/env/export/generators/ResourceGenerator.ts index 3d5372c..2c4bf54 100644 --- a/source/commands/env/export/generators/ResourceGenerator.ts +++ b/source/commands/env/export/generators/ResourceGenerator.ts @@ -10,96 +10,98 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); interface ResourceData { - key: string; - name: string; - description?: string; - urn?: string; - actions: Record; - attributes?: Record; + key: string; + name: string; + description?: string; + urn?: string; + actions: Record; + attributes?: Record; } interface ActionData { - name: string; - description?: string; + name: string; + description?: string; } // Define a type for attributes interface AttributeData { - type: string; - required?: boolean; + type: string; + required?: boolean; } interface ActionBlockRead { - name?: string; - description?: string; + name?: string; + description?: string; } interface AttributeBlockRead { - type?: string; - required?: boolean; + type?: string; + required?: boolean; } export class ResourceGenerator implements HCLGenerator { - name = 'resources'; - private template: TemplateDelegate<{ resources: ResourceData[] }>; + name = 'resources'; + private template: TemplateDelegate<{ resources: ResourceData[] }>; - constructor( - private permit: Permit, - private warningCollector: WarningCollector, - ) { - this.template = Handlebars.compile( - readFileSync(join(__dirname, '../templates/resource.hcl'), 'utf-8'), - ); - } + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/resource.hcl'), 'utf-8'), + ); + } - async generateHCL(): Promise { - try { - const resources = await this.permit.api.resources.list(); - const validResources = resources - .filter((resource) => resource.key !== '__user') - .map((resource) => ({ - key: createSafeId(resource.key), - name: resource.name, - description: resource.description, - urn: resource.urn, - actions: this.transformActions(resource.actions || {}), - attributes: this.transformAttributes(resource.attributes), - })); + async generateHCL(): Promise { + try { + const resources = await this.permit.api.resources.list(); + const validResources = resources + .filter(resource => resource.key !== '__user') + .map(resource => ({ + key: createSafeId(resource.key), + name: resource.name, + description: resource.description, + urn: resource.urn, + actions: this.transformActions(resource.actions || {}), + attributes: this.transformAttributes(resource.attributes), + })); - if (validResources.length === 0) return ''; + if (validResources.length === 0) return ''; - return '\n# Resources\n' + this.template({ resources: validResources }); - } catch (error) { - this.warningCollector.addWarning(`Failed to export resources: ${error}`); - return ''; - } - } + return '\n# Resources\n' + this.template({ resources: validResources }); + } catch (error) { + this.warningCollector.addWarning(`Failed to export resources: ${error}`); + return ''; + } + } - // Helper function to transform actions - private transformActions(actions: Record): Record { - const transformedActions: Record = {}; - for (const [key, action] of Object.entries(actions)) { - transformedActions[key] = { - name: action.name || key, - description: action.description, - }; - } - return transformedActions; - } + // Helper function to transform actions + private transformActions( + actions: Record, + ): Record { + const transformedActions: Record = {}; + for (const [key, action] of Object.entries(actions)) { + transformedActions[key] = { + name: action.name || key, + description: action.description, + }; + } + return transformedActions; + } - // Helper function to transform attributes - private transformAttributes( - attributes: Record | undefined, - ): Record | undefined { - if (!attributes) return undefined; + // Helper function to transform attributes + private transformAttributes( + attributes: Record | undefined, + ): Record | undefined { + if (!attributes) return undefined; - const transformedAttributes: Record = {}; - for (const [key, attribute] of Object.entries(attributes)) { - transformedAttributes[key] = { - type: attribute.type || 'string', - required: attribute.required || false, - }; - } - return transformedAttributes; - } -} \ No newline at end of file + const transformedAttributes: Record = {}; + for (const [key, attribute] of Object.entries(attributes)) { + transformedAttributes[key] = { + type: attribute.type || 'string', + required: attribute.required || false, + }; + } + return transformedAttributes; + } +} diff --git a/source/commands/env/export/generators/UserSetGenerator.ts b/source/commands/env/export/generators/UserSetGenerator.ts index 0721388..2ccbd82 100644 --- a/source/commands/env/export/generators/UserSetGenerator.ts +++ b/source/commands/env/export/generators/UserSetGenerator.ts @@ -9,51 +9,50 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); - interface UserSetData { - key: string; - name: string; - description?: string; - conditions: string; - resource: string; + key: string; + name: string; + description?: string; + conditions: string; + resource: string; } export class UserSetGenerator implements HCLGenerator { - name = 'user set'; - private template: TemplateDelegate<{ sets: UserSetData[] }>; - - constructor( - private permit: Permit, - private warningCollector: WarningCollector, - ) { - this.template = Handlebars.compile( - readFileSync(join(__dirname, '../templates/user-set.hcl'), 'utf-8'), - ); - } - - async generateHCL(): Promise { - try { - const conditionSets = await this.permit.api.conditionSets.list({}); - - const validSets = conditionSets - .filter((set) => set.type === 'userset') - .map((set) => ({ - key: createSafeId(set.key), - name: set.name, - description: set.description, - conditions: - typeof set.conditions === 'string' - ? set.conditions - : JSON.stringify(set.conditions), - resource: set.resource_id?.toString() || '', - })); - - if (validSets.length === 0) return ''; - - return '\n# User Sets\n' + this.template({ sets: validSets }); - } catch (error) { - this.warningCollector.addWarning(`Failed to export user sets: ${error}`); - return ''; - } - } + name = 'user set'; + private template: TemplateDelegate<{ sets: UserSetData[] }>; + + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/user-set.hcl'), 'utf-8'), + ); + } + + async generateHCL(): Promise { + try { + const conditionSets = await this.permit.api.conditionSets.list({}); + + const validSets = conditionSets + .filter(set => set.type === 'userset') + .map(set => ({ + key: createSafeId(set.key), + name: set.name, + description: set.description, + conditions: + typeof set.conditions === 'string' + ? set.conditions + : JSON.stringify(set.conditions), + resource: set.resource_id?.toString() || '', + })); + + if (validSets.length === 0) return ''; + + return '\n# User Sets\n' + this.template({ sets: validSets }); + } catch (error) { + this.warningCollector.addWarning(`Failed to export user sets: ${error}`); + return ''; + } + } } From ebd81721bdd3df671f1b6ffc2ab929a6ae6cfc33 Mon Sep 17 00:00:00 2001 From: daveads Date: Sun, 5 Jan 2025 10:47:39 +0100 Subject: [PATCH 25/34] actiondata --- .../export/generators/ResourceGenerator.ts | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/source/commands/env/export/generators/ResourceGenerator.ts b/source/commands/env/export/generators/ResourceGenerator.ts index 2c4bf54..116fbd8 100644 --- a/source/commands/env/export/generators/ResourceGenerator.ts +++ b/source/commands/env/export/generators/ResourceGenerator.ts @@ -9,26 +9,25 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -interface ResourceData { - key: string; - name: string; - description?: string; - urn?: string; - actions: Record; - attributes?: Record; -} - interface ActionData { name: string; description?: string; } -// Define a type for attributes interface AttributeData { type: string; required?: boolean; } +interface ResourceData { + key: string; + name: string; + description?: string; + urn?: string; + actions: Record; + attributes?: Record; +} + interface ActionBlockRead { name?: string; description?: string; @@ -62,8 +61,8 @@ export class ResourceGenerator implements HCLGenerator { name: resource.name, description: resource.description, urn: resource.urn, - actions: this.transformActions(resource.actions || {}), - attributes: this.transformAttributes(resource.attributes), + actions: this.transformActions(resource.actions || {}), // Transform actions + attributes: this.transformAttributes(resource.attributes), // Transform attributes })); if (validResources.length === 0) return ''; @@ -82,7 +81,7 @@ export class ResourceGenerator implements HCLGenerator { const transformedActions: Record = {}; for (const [key, action] of Object.entries(actions)) { transformedActions[key] = { - name: action.name || key, + name: action.name || key, // Use the key as a fallback if `name` is undefined description: action.description, }; } From 8b466ff4b6ac817d22a437f7c6e49fada4a25ad8 Mon Sep 17 00:00:00 2001 From: daveads Date: Sun, 5 Jan 2025 13:55:55 +0100 Subject: [PATCH 26/34] test --- tests/export/ResourceGenerator.test.tsx | 1 - tests/export/RoleDerivationGenerator.test.ts | 49 ++++++-------------- 2 files changed, 15 insertions(+), 35 deletions(-) diff --git a/tests/export/ResourceGenerator.test.tsx b/tests/export/ResourceGenerator.test.tsx index d55fec0..ba74c7c 100644 --- a/tests/export/ResourceGenerator.test.tsx +++ b/tests/export/ResourceGenerator.test.tsx @@ -70,7 +70,6 @@ describe('ResourceGenerator', () => { expect(result).toContain('attributes = {'); expect(result).toContain('"owner" = {'); expect(result).toContain('type = "string"'); - expect(result).toContain('description = "The owner of the document"'); }); it('handles errors when fetching resources', async () => { diff --git a/tests/export/RoleDerivationGenerator.test.ts b/tests/export/RoleDerivationGenerator.test.ts index 7591dea..66b4473 100644 --- a/tests/export/RoleDerivationGenerator.test.ts +++ b/tests/export/RoleDerivationGenerator.test.ts @@ -13,7 +13,7 @@ resource "permitio_role_derivation" "{{resource_id}}" { on_resource = "{{on_resource}}" to_role = "{{to_role}}" {{#if dependencies}} - depends_on = [{{#each dependencies}}"{{this}}"{{#unless @last}},{{/unless}}{{/each}}] + depends_on = {{{json dependencies}}} {{/if}} } {{/each}}`), @@ -33,26 +33,17 @@ describe('RoleDerivationGenerator', () => { id: 'res1', key: 'document', }, - { - id: 'res2', - key: 'folder', - }, ]), }, resourceRoles: { - list: vi.fn().mockImplementation(({ resourceKey }) => { - if (resourceKey === 'document') { - return Promise.resolve([ - { - resource: 'document', - role: 'editor', - linked_by: 'parent', - on_resource: 'folder', - to_role: 'viewer', - }, - ]); - } - return Promise.resolve([]); + list: vi.fn().mockImplementation(() => { + return Promise.resolve([ + { + id: 'parent', + resource: 'document', + name: 'editor', + }, + ]); }), }, }, @@ -66,34 +57,25 @@ describe('RoleDerivationGenerator', () => { const hcl = await generator.generateHCL(); - // Basic structure checks expect(hcl).toContain('# Role Derivations'); expect(hcl).toContain('resource "permitio_role_derivation"'); - - // Field checks - ensure we check exact formatting expect(hcl).toContain('resource = "document"'); expect(hcl).toContain('role = "editor"'); expect(hcl).toContain('linked_by = "parent"'); - expect(hcl).toContain('on_resource = "folder"'); - expect(hcl).toContain('to_role = "viewer"'); - - // Dependencies check + expect(hcl).toContain('on_resource = "document"'); + expect(hcl).toContain('to_role = "editor"'); expect(hcl).toContain('depends_on'); - expect(hcl).toContain('permitio_resource.document'); - expect(hcl).toContain('permitio_resource.folder'); - expect(hcl).toContain('permitio_role.editor'); - expect(hcl).toContain('permitio_role.viewer'); // Snapshot test with exact formatting expect(hcl.trim()).toMatchInlineSnapshot(` "# Role Derivations - resource "permitio_role_derivation" "document_editor_parent_folder_viewer" { + resource "permitio_role_derivation" "document_editor_parent" { resource = "document" role = "editor" linked_by = "parent" - on_resource = "folder" - to_role = "viewer" - depends_on = ["permitio_resource.document","permitio_resource.folder","permitio_role.editor","permitio_role.viewer"] + on_resource = "document" + to_role = "editor" + depends_on = ["permitio_resource.document","permitio_role.editor"] }" `); }); @@ -105,7 +87,6 @@ describe('RoleDerivationGenerator', () => { list: vi.fn().mockResolvedValue([ { id: 'res1', - // key is missing }, ]), }, From a5dc8fe9105e78132be452ce36eb31106beb97ad Mon Sep 17 00:00:00 2001 From: daveads Date: Sun, 5 Jan 2025 14:05:01 +0100 Subject: [PATCH 27/34] .. --- eslint.config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/eslint.config.js b/eslint.config.js index 3478a29..f92e870 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -35,7 +35,6 @@ export default [ RequestInit: 'readonly', fetch: 'readonly', process: 'readonly', - console: true, }, }, }, From 009c4c6637edeb1fb32cffe972fc93bdc3879eaa Mon Sep 17 00:00:00 2001 From: daveads Date: Sun, 5 Jan 2025 14:45:56 +0100 Subject: [PATCH 28/34] test --- eslint.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/eslint.config.js b/eslint.config.js index f92e870..f34b954 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -35,6 +35,7 @@ export default [ RequestInit: 'readonly', fetch: 'readonly', process: 'readonly', + console: 'readonly', }, }, }, From dda9be6e96cf8db43c84a33f02a694451c377117 Mon Sep 17 00:00:00 2001 From: daveads Date: Sun, 5 Jan 2025 15:17:37 +0100 Subject: [PATCH 29/34] .. --- tests/export/index.test.tsx | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 tests/export/index.test.tsx diff --git a/tests/export/index.test.tsx b/tests/export/index.test.tsx deleted file mode 100644 index d761278..0000000 --- a/tests/export/index.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { expect, vi, describe, it } from 'vitest'; -import React from 'react'; -import { render } from 'ink-testing-library'; -import Export from '../../source/commands/env/export/index.js'; -import { AuthProvider } from '../../source/components/AuthProvider.js'; - -// Test the main Export component -describe('Export Command', () => { - it('renders with AuthProvider', () => { - const { lastFrame } = render(); - expect(lastFrame()).toBeTruthy(); - }); - - it('passes options correctly', () => { - const options = { key: 'test-key', file: 'output.tf' }; - const { lastFrame } = render(); - expect(lastFrame()).toBeTruthy(); - }); -}); From 307dfeaabbb647be19147bc1a266d80695853350 Mon Sep 17 00:00:00 2001 From: daveads Date: Tue, 7 Jan 2025 10:19:24 +0100 Subject: [PATCH 30/34] include .hcl files in build output --- package-lock.json | 2279 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 + 2 files changed, 2251 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7cb2b45..5325964 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "@vitest/coverage-v8": "^2.1.8", "@vitest/ui": "^2.1.8", "chalk": "^5.2.0", + "cpx": "^1.5.0", "delay": "^6.0.0", "eslint": "^9.13.0", "eslint-config-prettier": "^9.1.0", @@ -3986,6 +3987,80 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "license": "ISC", + "dependencies": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + } + }, + "node_modules/anymatch/node_modules/braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha512-xU7bpz2ytJl1bH9cgIurjpg/n8Gohy9GTw81heDYLJQ4RU60dlyJsa+atVF2pI0yMMvKxI9HkKwjePCj5XI1hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/anymatch/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/anymatch/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/anymatch/node_modules/micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha512-LnU2XFEk9xxSJ6rfgAry/ty5qwUTyHYOBU0g4R6tIw5ljwgGIBmiKhRWLw5NpMOnrgUNcDJ4WMp8rl3sYVHLNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -4010,6 +4085,39 @@ "deep-equal": "^2.0.5" } }, + "node_modules/arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha512-dtXTVMkh6VkEEA7OhXnN1Ecb8aAGFdZ1LFxtOCoqj4qkyOJMt7+qs6Ahdy6p/NQCPYsRSXXivhSB/J5E9jmYKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", @@ -4058,6 +4166,16 @@ "node": ">=8" } }, + "node_modules/array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha512-G2n5bG5fSUCpnsXz4+8FUkYsGPkNfLn9YvS66U5qbTIXI2Ynnlo4Bi42bWv+omKUCqz+ejzfClwne0alJWJPhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array.prototype.findlast": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", @@ -4197,6 +4315,16 @@ "node": ">=12" } }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -4213,12 +4341,38 @@ "node": ">=4" } }, + "node_modules/async-each": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz", + "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -4339,12 +4493,59 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "node_modules/babel-runtime/node_modules/regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true, + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -4365,6 +4566,27 @@ ], "license": "MIT" }, + "node_modules/binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -4541,6 +4763,37 @@ "node": ">=8" } }, + "node_modules/cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cache-base/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -4647,6 +4900,79 @@ "node": ">= 16" } }, + "node_modules/chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha512-mk8fAWcRUOxY7btlLtitj3A45jOwSAxH4tOFOoEGbVsl6cL6pPMWUy7dwZ/canfj3QEdP6FHSnf/l1c6/WkzVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + }, + "optionalDependencies": { + "fsevents": "^1.0.0" + } + }, + "node_modules/chokidar/node_modules/fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "deprecated": "Upgrade to fsevents v2 to mitigate potential security issues", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha512-JDYOvfxio/t42HKdxkAYaCiBN7oYiuxykOxKxdaUW5Qn0zaYN3gRQWolrwdnf0shM9/EP0ebuuTmyoXNr1cC5w==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^2.0.0" + } + }, + "node_modules/chokidar/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chokidar/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -4659,6 +4985,59 @@ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "license": "MIT" }, + "node_modules/class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/class-utils/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/cli-boxes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", @@ -4759,6 +5138,20 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4804,6 +5197,16 @@ "node": ">=18" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4827,6 +5230,25 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "dev": true, + "hasInstallScript": true, + "license": "MIT" + }, "node_modules/core-js-compat": { "version": "3.39.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz", @@ -4841,17 +5263,114 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true, "license": "MIT" }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/cpx": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/cpx/-/cpx-1.5.0.tgz", + "integrity": "sha512-jHTjZhsbg9xWgsP2vuNW2jnnzBX+p4T+vNI9Lbjzs1n4KhOfa22bQppiFYLsWQKd8TzmL5aSP/Me3yfsCwXbDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-runtime": "^6.9.2", + "chokidar": "^1.6.0", + "duplexer": "^0.1.1", + "glob": "^7.0.5", + "glob2base": "^0.0.12", + "minimatch": "^3.0.2", + "mkdirp": "^0.5.1", + "resolve": "^1.1.7", + "safe-buffer": "^5.0.1", + "shell-quote": "^1.6.1", + "subarg": "^1.0.0" + }, + "bin": { + "cpx": "bin/index.js" + } + }, + "node_modules/cpx/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cpx/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cpx/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/cpx/node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -5008,6 +5527,16 @@ "optional": true, "peer": true }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -5246,6 +5775,13 @@ "node": ">=0.10.0" } }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -6861,6 +7397,62 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha512-hxx03P2dJxss6ceIeri9cmYOT4SRs3Zk3afZwWpOsRqLqprhTR8u++SlC+sFGsQr7WGFPdMF7Gjc1njDLDK6UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-posix-bracket": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha512-AFASGfIlnIbkKPQwX1yHaDjFvh/1gyKJODme52V6IORh69uEYgZp0o9C+qsIGNVEiuuhQU0CSSl++Rlegg1qvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-range/node_modules/fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-range/node_modules/is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -6880,6 +7472,56 @@ "node": ">=12.0.0" } }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend-shallow/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha512-1FOj1LOwn42TMrruOHGt18HemVnbwAmAak7krWk+wa93KXxGbK+2jpezm+ytJYDaBX0/SPLZFHKM7m+tKobWGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fast-copy": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", @@ -7016,6 +7658,24 @@ "node": ">=16.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha512-BTCqyBaWBTsauvnHiE8i562+EdJj+oUpkqWp2R1iCoR8f6oo8STRu3of7WJJ0TqWtxN50a5YFpzYK4Jj9esYfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -7029,6 +7689,13 @@ "node": ">=8" } }, + "node_modules/find-index": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-index/-/find-index-0.1.1.tgz", + "integrity": "sha512-uJ5vWrfBKMcE6y2Z8834dwEZj9mNGxYa3t3I53OwFeuZ8D9oc2E5zcsrkuhX6h4iYrjhiv0T3szQmxlAV9uxDg==", + "dev": true, + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -7109,6 +7776,29 @@ "is-callable": "^1.1.3" } }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -7153,6 +7843,19 @@ "node": ">= 6" } }, + "node_modules/fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "map-cache": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -7306,6 +8009,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -7332,6 +8045,53 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha512-ab1S1g1EbO7YzauaJLkgLp7DZVAqj9M/dvKlTt8DkXA2tiOIcSMrlVI2J1RZyB5iJVccEscjGn+kpOG9788MHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-base/node_modules/glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha512-JDYOvfxio/t42HKdxkAYaCiBN7oYiuxykOxKxdaUW5Qn0zaYN3gRQWolrwdnf0shM9/EP0ebuuTmyoXNr1cC5w==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^2.0.0" + } + }, + "node_modules/glob-base/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-base/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -7357,6 +8117,18 @@ "node": ">=10" } }, + "node_modules/glob2base": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/glob2base/-/glob2base-0.0.12.tgz", + "integrity": "sha512-ZyqlgowMbfj2NPjxaZZ/EtsXlOch28FRXgMd64vqZWk1bT9+wvSRLYD1om9M7QfQru51zJPAT17qXm4/zd+9QA==", + "dev": true, + "dependencies": { + "find-index": "^0.1.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/globals": { "version": "15.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz", @@ -7421,6 +8193,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/gradient-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/gradient-string/-/gradient-string-2.0.2.tgz", @@ -7579,30 +8358,108 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/help-me": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz", - "integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==", + "node_modules/has-value/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, "license": "MIT", - "dependencies": { - "glob": "^8.0.0", - "readable-stream": "^3.6.0" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/help-me/node_modules/readable-stream": { - "version": "3.6.2", + "node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/help-me": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz", + "integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==", + "license": "MIT", + "dependencies": { + "glob": "^8.0.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/help-me/node_modules/readable-stream": { + "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", @@ -8385,6 +9242,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -8434,9 +9304,9 @@ } }, "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -8520,6 +9390,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha512-9YclgOGtN/f8zx0Pr4FQYMdibBiTaH3sn52vjYip4ZSf6C4/6RfTEZ+MR4GvKhCxdPh21Bg42/WL55f6KSnKpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha512-0EygVC5qPvIyb+gSz7zdD5/AAoS6Qrx1e//6N4yv4oNm30kqvdmG66oZFWVlQHUWe5OjP08FuTw2IdT0EOTcYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-primitive": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -8668,6 +9571,39 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha512-Yu68oeXJ7LeWNmZ3Zov/xg/oDBnBK2RNxwYY1ilNJX+tKKZqgPK+qOn/Gs9jEu66KDY9Netf5XLKNGzas/vPfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -8677,6 +9613,16 @@ "optional": true, "peer": true }, + "node_modules/is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha512-N3w1tFaRfk3UrPfqeRyD+GYDASU3W5VinKhlORy8EWVf/sIdDL9GAcew85XmktCfH+ngG7SRXEVDoO18WMdB/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -8838,6 +9784,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-wsl": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", @@ -8881,6 +9837,26 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isobject/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -9467,6 +10443,36 @@ "dev": true, "license": "ISC" }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/math-random": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", + "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", + "dev": true, + "license": "MIT" + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -9602,6 +10608,46 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-deep/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -9630,6 +10676,14 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -9649,6 +10703,83 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nanomatch/node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nanomatch/node_modules/array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nanomatch/node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nanomatch/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nanomatch/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", @@ -9707,6 +10838,19 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -9752,10 +10896,52 @@ "node": ">=0.10.0" } }, - "node_modules/object-inspect": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", - "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "node_modules/object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", "dev": true, "license": "MIT", "engines": { @@ -9792,6 +10978,29 @@ "node": ">= 0.4" } }, + "node_modules/object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-visit/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object.assign": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", @@ -9860,6 +11069,43 @@ "node": ">= 0.4" } }, + "node_modules/object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha512-UiAM5mhmIuKLsOvrL+B0U2d1hXHF3bFYWIuH1LMpuV2EJEHG1Ntz06PgLEHjm6VFd87NpH8rastvPoyv6UW2fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object.values": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", @@ -9999,6 +11245,45 @@ "node": ">=6" } }, + "node_modules/parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha512-FC5TeK0AwXzq3tUBFtH74naWkPQCEWs4K+xMxWZBlKDWu0bVHXGZa+KKqxKidd7xwhdZ19ZNuF2uO1M/r196HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-glob/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-glob/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/parse-json": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.1.0.tgz", @@ -10043,6 +11328,16 @@ "node": "0.4-0.9" } }, + "node_modules/pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pastel": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pastel/-/pastel-3.0.0.tgz", @@ -10084,6 +11379,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -10271,6 +11576,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -10346,6 +11661,16 @@ "node": ">= 0.8.0" } }, + "node_modules/preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha512-s/46sYeylUfHNjI+sA/78FAHlmIuKqI9wNnzEOGehAlUUYeObv5C2mOinXBjyUyWmJ2SfcS2/ydApH4hTF4WXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prettier": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", @@ -10384,6 +11709,13 @@ "node": ">= 0.6.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, "node_modules/process-warning": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz", @@ -10454,6 +11786,41 @@ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, + "node_modules/randomatic": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", + "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/randomatic/node_modules/is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/randomatic/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -10564,6 +11931,366 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/readdirp/node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp/node_modules/array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/readdirp/node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp/node_modules/expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp/node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp/node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp/node_modules/expand-brackets/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/readdirp/node_modules/extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp/node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp/node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp/node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "license": "MIT", + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/readdirp/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", @@ -10645,6 +12372,33 @@ "@babel/runtime": "^7.8.4" } }, + "node_modules/regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-equal-shallow": "^0.1.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "license": "MIT", + "dependencies": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/regexp-ast-analysis": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.7.1.tgz", @@ -10716,6 +12470,33 @@ "regjsparser": "bin/parser" } }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true, + "license": "ISC" + }, + "node_modules/repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/require-in-the-middle": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz", @@ -10775,6 +12556,14 @@ "node": ">=4" } }, + "node_modules/resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "deprecated": "https://github.com/lydell/resolve-url#deprecated", + "dev": true, + "license": "MIT" + }, "node_modules/restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -10791,6 +12580,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -10925,6 +12724,16 @@ ], "license": "MIT" }, + "node_modules/safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ret": "~0.1.10" + } + }, "node_modules/safe-regex-test": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", @@ -11052,6 +12861,35 @@ "node": ">= 0.4" } }, + "node_modules/set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -11073,6 +12911,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", + "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -11206,6 +13057,131 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/snapdragon/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/snapdragon/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sonic-boom": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", @@ -11234,6 +13210,29 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "license": "MIT", + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "node_modules/source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated", + "dev": true, + "license": "MIT" + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -11266,6 +13265,19 @@ "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", "license": "CC0-1.0" }, + "node_modules/split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -11303,6 +13315,47 @@ "dev": true, "license": "MIT" }, + "node_modules/static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/std-env": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", @@ -11623,6 +13676,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/subarg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha512-RIrIdRY0X1xojthNcVtgT9sjpOGagEUKpZdgBUi054OEPFo282yg+zE+t1Rj3+RqKq2xStL7uUHhY+AjbC4BXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.1.0" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -11901,6 +13964,35 @@ "optional": true, "peer": true }, + "node_modules/to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -11914,6 +14006,30 @@ "node": ">=8.0" } }, + "node_modules/to-regex/node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/to-rotated": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/to-rotated/-/to-rotated-1.0.0.tgz", @@ -12313,6 +14429,91 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unset-value/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", @@ -12354,6 +14555,24 @@ "punycode": "^2.1.0" } }, + "node_modules/urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "deprecated": "Please see https://github.com/lydell/urix#deprecated", + "dev": true, + "license": "MIT" + }, + "node_modules/use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 328e3be..5080c5a 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "scripts": { "build": "tsc", + "postbuild": "cpx 'source/commands/env/export/templates/*.hcl' 'dist/commands/env/export/templates'", "dev": " NODE_NO_WARNINGS=1 tsc --watch", "lint": "eslint \"source/**/*.{js,ts,tsx}\"", "lint:fix": "eslint \"source/**/*.{js,ts,tsx}\" --fix", @@ -49,6 +50,7 @@ "@vitest/coverage-v8": "^2.1.8", "@vitest/ui": "^2.1.8", "chalk": "^5.2.0", + "cpx": "^1.5.0", "delay": "^6.0.0", "eslint": "^9.13.0", "eslint-config-prettier": "^9.1.0", From e8f9eb3b5a1a2ecf3c42c9d13aa3214a4d14f762 Mon Sep 17 00:00:00 2001 From: daveads Date: Fri, 10 Jan 2025 04:59:42 +0100 Subject: [PATCH 31/34] npm build and npm dev --- package-lock.json | 415 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 6 +- 2 files changed, 419 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5325964..386b405 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "@vitest/coverage-v8": "^2.1.8", "@vitest/ui": "^2.1.8", "chalk": "^5.2.0", + "concurrently": "^9.1.2", "cpx": "^1.5.0", "delay": "^6.0.0", "eslint": "^9.13.0", @@ -54,6 +55,7 @@ "ink-testing-library": "^4.0.0", "parser": "^0.1.4", "prettier": "^3.3.3", + "rimraf": "^6.0.1", "ts-node": "^10.9.1", "typescript": "^5.6.3", "typescript-eslint": "^8.11.0", @@ -5126,6 +5128,110 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/code-excerpt": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", @@ -5214,6 +5320,78 @@ "dev": true, "license": "MIT" }, + "node_modules/concurrently": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", + "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -7947,6 +8125,16 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-east-asian-width": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", @@ -12497,6 +12685,16 @@ "node": ">=0.10" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-in-the-middle": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz", @@ -12601,6 +12799,109 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/jackspeak": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", + "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", @@ -12685,6 +12986,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-array-concat": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", @@ -14091,6 +14402,16 @@ "node": ">=18" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/ts-api-utils": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.0.tgz", @@ -15204,6 +15525,16 @@ "optional": true, "peer": true }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -15211,6 +15542,90 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 5080c5a..d1d17d2 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,9 @@ }, "scripts": { "build": "tsc", - "postbuild": "cpx 'source/commands/env/export/templates/*.hcl' 'dist/commands/env/export/templates'", - "dev": " NODE_NO_WARNINGS=1 tsc --watch", + "postbuild": "rimraf dist/commands/env/export/templates && cpx 'source/commands/env/export/templates/*.hcl' 'dist/commands/env/export/templates'", + "predev": "cpx 'source/commands/env/export/templates/*.hcl' 'dist/commands/env/export/templates'", + "dev": "NODE_NO_WARNINGS=1 tsc --watch", "lint": "eslint \"source/**/*.{js,ts,tsx}\"", "lint:fix": "eslint \"source/**/*.{js,ts,tsx}\" --fix", "test": "prettier --check ./source && vitest run --coverage", @@ -64,6 +65,7 @@ "ink-testing-library": "^4.0.0", "parser": "^0.1.4", "prettier": "^3.3.3", + "rimraf": "^6.0.1", "ts-node": "^10.9.1", "typescript": "^5.6.3", "typescript-eslint": "^8.11.0", From e7ddd4c17048f97a035ec4f609472d7eafe8da91 Mon Sep 17 00:00:00 2001 From: daveads Date: Fri, 10 Jan 2025 06:21:08 +0100 Subject: [PATCH 32/34] test --- tests/export/RelationGenerator.test.ts | 104 +++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 tests/export/RelationGenerator.test.ts diff --git a/tests/export/RelationGenerator.test.ts b/tests/export/RelationGenerator.test.ts new file mode 100644 index 0000000..b162e95 --- /dev/null +++ b/tests/export/RelationGenerator.test.ts @@ -0,0 +1,104 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { RelationGenerator } from '../../source/commands/env/export/generators/RelationGenerator'; +import type { WarningCollector } from '../../source/commands/env/export/types'; + +describe('RelationGenerator', () => { + let warningCollector: WarningCollector; + let mockPermit: any; + + beforeEach(() => { + warningCollector = { + addWarning: vi.fn(), + getWarnings: vi.fn().mockReturnValue([]), + }; + mockPermit = { + api: { + resources: { + list: vi.fn(), + }, + resourceRelations: { + list: vi.fn(), + }, + }, + }; + }); + + it('generates empty string when no resources exist', async () => { + mockPermit.api.resources.list.mockResolvedValue([]); + const generator = new RelationGenerator(mockPermit, warningCollector); + const result = await generator.generateHCL(); + expect(result).toBe(''); + }); + + it('skips internal user resource', async () => { + mockPermit.api.resources.list + .mockResolvedValueOnce([{ key: '__user' }, { key: 'document' }]) + .mockResolvedValueOnce([{ key: '__user' }, { key: 'document' }]); + mockPermit.api.resourceRelations.list.mockResolvedValue([]); + + const generator = new RelationGenerator(mockPermit, warningCollector); + await generator.generateHCL(); + expect(mockPermit.api.resourceRelations.list).toHaveBeenCalledTimes(1); + expect(mockPermit.api.resourceRelations.list).toHaveBeenCalledWith({ + resourceKey: 'document', + }); + }); + + it('generates valid HCL for relations', async () => { + const mockResources = [{ key: 'document' }, { key: 'user' }]; + const mockRelations = [ + { + key: 'owner', + name: 'Owner', + description: 'Document owner', + subject_resource: 'user', + object_resource: 'document', + }, + ]; + + mockPermit.api.resources.list + .mockResolvedValueOnce(mockResources) + .mockResolvedValueOnce(mockResources); + mockPermit.api.resourceRelations.list.mockResolvedValue(mockRelations); + + const generator = new RelationGenerator(mockPermit, warningCollector); + const result = await generator.generateHCL(); + + expect(result).toContain('resource "permitio_relation" "owner"'); + expect(result).toContain('key = "owner"'); + expect(result).toContain('name = "Owner"'); + expect(result).toContain('description = "Document owner"'); + expect(result).toContain('subject_resource = "user"'); + expect(result).toContain('object_resource = "document"'); + }); + + it('handles errors when fetching relations', async () => { + mockPermit.api.resources.list + .mockResolvedValueOnce([{ key: 'document' }]) + .mockResolvedValueOnce([{ key: 'document' }]); + mockPermit.api.resourceRelations.list.mockRejectedValue(new Error('API error')); + + const generator = new RelationGenerator(mockPermit, warningCollector); + await generator.generateHCL(); + expect(warningCollector.addWarning).toHaveBeenCalledWith( + expect.stringContaining('Failed to fetch relations for resource document'), + ); + }); + + it('skips invalid relations', async () => { + mockPermit.api.resources.list + .mockResolvedValueOnce([{ key: 'document' }]) + .mockResolvedValueOnce([{ key: 'document' }]); + mockPermit.api.resourceRelations.list.mockResolvedValue([ + { key: 'invalid' }, + ]); + + const generator = new RelationGenerator(mockPermit, warningCollector); + const result = await generator.generateHCL(); + + expect(warningCollector.addWarning).toHaveBeenCalledWith( + 'Relation "invalid" is missing required fields: name, subject_resource, object_resource' + ); + expect(result).not.toContain('resource "permitio_relation" "invalid"'); + }); +}); \ No newline at end of file From 187ebb016ac3ca82468bb9ba1271985c6d7539ea Mon Sep 17 00:00:00 2001 From: daveads Date: Mon, 20 Jan 2025 14:07:48 +0100 Subject: [PATCH 33/34] resource set --- .../export/generators/ResourceSetGenerator.ts | 128 +++++++++++------- .../env/export/templates/resource-set.hcl | 16 ++- 2 files changed, 90 insertions(+), 54 deletions(-) diff --git a/source/commands/env/export/generators/ResourceSetGenerator.ts b/source/commands/env/export/generators/ResourceSetGenerator.ts index d533299..53cee83 100644 --- a/source/commands/env/export/generators/ResourceSetGenerator.ts +++ b/source/commands/env/export/generators/ResourceSetGenerator.ts @@ -9,55 +9,87 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -// Define a proper type for the resource set object interface ResourceSetData { - key: string; - name: string; - description?: string; - conditions: string; - resource: string; + key: string; + name: string; + description?: string; + conditions: string; + resource: string; + depends_on: string[]; } export class ResourceSetGenerator implements HCLGenerator { - name = 'resource set'; - private template: TemplateDelegate<{ sets: ResourceSetData[] }>; - - constructor( - private permit: Permit, - private warningCollector: WarningCollector, - ) { - this.template = Handlebars.compile( - readFileSync(join(__dirname, '../templates/resource-set.hcl'), 'utf-8'), - ); - } - - async generateHCL(): Promise { - try { - // Get all resource sets using the Permit SDK - const resourceSets = await this.permit.api.conditionSets.list({}); - - // Filter only resource sets (not user sets) and ensure `resource_id` is defined - const validSets = resourceSets - .filter(set => set.type === 'resourceset') - .map(set => ({ - key: createSafeId(set.key), - name: set.name, - description: set.description, - conditions: - typeof set.conditions === 'string' - ? set.conditions - : JSON.stringify(set.conditions), - resource: set.resource_id?.toString() || '', - })); - - if (validSets.length === 0) return ''; - - return '\n# Resource Sets\n' + this.template({ sets: validSets }); - } catch (error) { - this.warningCollector.addWarning( - `Failed to export resource sets: ${error}`, - ); - return ''; - } - } -} + name = 'resource sets'; + private template: TemplateDelegate<{ sets: ResourceSetData[] }>; + private resourceKeyMap: Map = new Map(); + + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/resource-set.hcl'), 'utf-8'), + ); + } + + async generateHCL(): Promise { + try { + // First, build resource key map + await this.buildResourceKeyMap(); + + const resourceSets = await this.permit.api.conditionSets.list({}); + const validSets = resourceSets + .filter(set => set.type === 'resourceset' && set.resource_id) + .map(set => ({ + key: createSafeId(set.key), + name: set.name, + description: set.description, + conditions: this.formatConditions(set.conditions), + // Use resource key instead of ID + resource: this.resourceKeyMap.get(set.resource_id!) || set.resource_id!, + depends_on: [`permitio_resource.${this.resourceKeyMap.get(set.resource_id!) || set.resource_id!}`] + })); + + if (validSets.length === 0) return ''; + + return '\n# Resource Sets\n' + this.template({ sets: validSets }); + } catch (error) { + this.warningCollector.addWarning( + `Failed to export resource sets: ${error}`, + ); + return ''; + } + } + + private async buildResourceKeyMap() { + try { + const resources = await this.permit.api.resources.list(); + resources.forEach(resource => { + this.resourceKeyMap.set(resource.id, resource.key); + }); + } catch (error) { + this.warningCollector.addWarning( + `Failed to build resource key map: ${error}`, + ); + } + } + + private formatConditions(conditions: any): string { + if (typeof conditions === 'string') { + try { + conditions = JSON.parse(conditions); + } catch { + return conditions; + } + } + + try { + return `jsonencode(${JSON.stringify(conditions, null, 2)})`; + } catch (error) { + this.warningCollector.addWarning( + `Failed to format conditions: ${error}. Using stringified version.`, + ); + return JSON.stringify(conditions); + } + } +} \ No newline at end of file diff --git a/source/commands/env/export/templates/resource-set.hcl b/source/commands/env/export/templates/resource-set.hcl index b44069a..69b9734 100644 --- a/source/commands/env/export/templates/resource-set.hcl +++ b/source/commands/env/export/templates/resource-set.hcl @@ -1,13 +1,17 @@ {{#each sets}} resource "permitio_resource_set" "{{key}}" { - key = "{{key}}" - name = "{{name}}" + name = "{{name}}" + key = "{{key}}" {{#if description}} description = "{{description}}" {{/if}} - {{#if resource}} - resource = "{{resource}}" - {{/if}} - conditions = "{{conditions}}" + resource = permitio_resource.{{resource}}.key + conditions = {{{conditions}}} + + depends_on = [ + {{#each depends_on}} + {{this}}{{#unless @last}},{{/unless}} + {{/each}} + ] } {{/each}} \ No newline at end of file From 7aabb4b5031a156111d24336a298ac8f217a8980 Mon Sep 17 00:00:00 2001 From: daveads Date: Tue, 21 Jan 2025 14:01:09 +0100 Subject: [PATCH 34/34] fixing exported output --- .../generators/ConditionSetGenerator.ts | 157 ++++++++++-------- .../export/generators/ResourceGenerator.ts | 114 +++++++------ .../env/export/templates/condition-set.hcl | 15 +- .../env/export/templates/resource.hcl | 33 ++-- source/commands/env/export/utils.ts | 2 +- 5 files changed, 173 insertions(+), 148 deletions(-) diff --git a/source/commands/env/export/generators/ConditionSetGenerator.ts b/source/commands/env/export/generators/ConditionSetGenerator.ts index 6753733..f20f8ad 100644 --- a/source/commands/env/export/generators/ConditionSetGenerator.ts +++ b/source/commands/env/export/generators/ConditionSetGenerator.ts @@ -11,84 +11,97 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); interface ConditionSetData { - key: string; - name: string; - description?: string; - conditions: string; - resource?: ResourceRead; - resourceType: string; + key: string; + name: string; + description?: string; + conditions: string; + resource?: ResourceRead; } -type ValidConditionSet = { - key: string; - name: string; - description?: string; - conditions: string; - resource?: ResourceRead; - resourceType: string; -} | null; - export class ConditionSetGenerator implements HCLGenerator { - name = 'condition sets'; - private template: TemplateDelegate<{ conditionSets: ConditionSetData[] }>; + name = 'user sets'; // Changed to clarify this handles user sets only + private template: TemplateDelegate<{ sets: ConditionSetData[] }>; + private resourceKeyMap: Map = new Map(); - constructor( - private permit: Permit, - private warningCollector: WarningCollector, - ) { - this.template = Handlebars.compile( - readFileSync(join(__dirname, '../templates/condition-set.hcl'), 'utf-8'), - ); - } + constructor( + private permit: Permit, + private warningCollector: WarningCollector, + ) { + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/user-set.hcl'), 'utf-8'), + ); + } - async generateHCL(): Promise { - try { - const conditionSets = await this.permit.api.conditionSets.list(); - if ( - !conditionSets || - !Array.isArray(conditionSets) || - conditionSets.length === 0 - ) { - return ''; - } + async generateHCL(): Promise { + try { + await this.buildResourceKeyMap(); + const conditionSets = await this.permit.api.conditionSets.list(); + + if (!conditionSets || !Array.isArray(conditionSets) || conditionSets.length === 0) { + return ''; + } - const validSets = conditionSets - .map(set => { - try { - const isResourceSet = set.type === 'resourceset'; - const resourceType = isResourceSet ? 'resource_set' : 'user_set'; - const conditions = - typeof set.conditions === 'string' - ? set.conditions - : JSON.stringify(set.conditions || ''); + // Only process user sets, since resource sets are handled by ResourceSetGenerator + const validSets = conditionSets + .filter(set => set.type === 'userset') + .map(set => { + try { + return { + key: createSafeId(set.key), + name: set.name, + description: set.description, + conditions: this.formatConditions(set.conditions), + resource: set.resource + }; + } catch (setError) { + this.warningCollector.addWarning( + `Failed to export user set ${set.key}: ${setError}`, + ); + return null; + } + }) + .filter((set): set is ConditionSetData => set !== null); - return { - key: createSafeId(set.key), - name: set.name, - description: set.description, - conditions, - resource: set.resource, - resourceType, - }; - } catch (setError) { - this.warningCollector.addWarning( - `Failed to export condition set ${set.key}: ${setError}`, - ); - return null; - } - }) - .filter((set): set is ConditionSetData => set !== null); + if (validSets.length === 0) return ''; - if (validSets.length === 0) return ''; + return '\n# User Sets\n' + this.template({ sets: validSets }); + } catch (error) { + this.warningCollector.addWarning( + `Failed to export user sets: ${error}`, + ); + return ''; + } + } - return ( - '\n# Condition Sets\n' + this.template({ conditionSets: validSets }) - ); - } catch (error) { - this.warningCollector.addWarning( - `Failed to export condition sets: ${error}`, - ); - return ''; - } - } -} + private async buildResourceKeyMap() { + try { + const resources = await this.permit.api.resources.list(); + resources.forEach(resource => { + this.resourceKeyMap.set(resource.id, resource.key); + }); + } catch (error) { + this.warningCollector.addWarning( + `Failed to build resource key map: ${error}`, + ); + } + } + + private formatConditions(conditions: any): string { + if (typeof conditions === 'string') { + try { + conditions = JSON.parse(conditions); + } catch { + return conditions; + } + } + + try { + return `jsonencode(${JSON.stringify(conditions, null, 2)})`; + } catch (error) { + this.warningCollector.addWarning( + `Failed to format conditions: ${error}. Using stringified version.`, + ); + return JSON.stringify(conditions); + } + } +} \ No newline at end of file diff --git a/source/commands/env/export/generators/ResourceGenerator.ts b/source/commands/env/export/generators/ResourceGenerator.ts index 116fbd8..d6792de 100644 --- a/source/commands/env/export/generators/ResourceGenerator.ts +++ b/source/commands/env/export/generators/ResourceGenerator.ts @@ -41,66 +41,78 @@ interface AttributeBlockRead { export class ResourceGenerator implements HCLGenerator { name = 'resources'; private template: TemplateDelegate<{ resources: ResourceData[] }>; - + constructor( - private permit: Permit, - private warningCollector: WarningCollector, + private permit: Permit, + private warningCollector: WarningCollector, ) { - this.template = Handlebars.compile( - readFileSync(join(__dirname, '../templates/resource.hcl'), 'utf-8'), - ); + this.template = Handlebars.compile( + readFileSync(join(__dirname, '../templates/resource.hcl'), 'utf-8'), + ); } - + async generateHCL(): Promise { - try { - const resources = await this.permit.api.resources.list(); - const validResources = resources - .filter(resource => resource.key !== '__user') - .map(resource => ({ - key: createSafeId(resource.key), - name: resource.name, - description: resource.description, - urn: resource.urn, - actions: this.transformActions(resource.actions || {}), // Transform actions - attributes: this.transformAttributes(resource.attributes), // Transform attributes - })); - - if (validResources.length === 0) return ''; - - return '\n# Resources\n' + this.template({ resources: validResources }); - } catch (error) { - this.warningCollector.addWarning(`Failed to export resources: ${error}`); - return ''; - } + try { + const resources = await this.permit.api.resources.list(); + const validResources = resources + .filter(resource => resource.key !== '__user') + .map(resource => ({ + key: createSafeId(resource.key), + name: resource.name, + description: resource.description, + urn: resource.urn, + actions: this.transformActions(resource.actions || {}), + attributes: this.transformAttributes(resource.attributes), + depends_on: [] + })); + + if (validResources.length === 0) return ''; + return '\n# Resources\n' + this.template({ resources: validResources }); + } catch (error) { + this.warningCollector.addWarning(`Failed to export resources: ${error}`); + return ''; + } } - - // Helper function to transform actions + + // Add the missing transformActions method private transformActions( - actions: Record, + actions: Record, ): Record { - const transformedActions: Record = {}; - for (const [key, action] of Object.entries(actions)) { - transformedActions[key] = { - name: action.name || key, // Use the key as a fallback if `name` is undefined - description: action.description, - }; - } - return transformedActions; + const transformedActions: Record = {}; + + for (const [key, action] of Object.entries(actions)) { + transformedActions[key] = { + name: action.name || key, + description: action.description, + }; + } + + return transformedActions; } - - // Helper function to transform attributes + private transformAttributes( - attributes: Record | undefined, + attributes: Record | undefined, ): Record | undefined { - if (!attributes) return undefined; - - const transformedAttributes: Record = {}; - for (const [key, attribute] of Object.entries(attributes)) { - transformedAttributes[key] = { - type: attribute.type || 'string', - required: attribute.required || false, - }; - } - return transformedAttributes; + if (!attributes) return undefined; + const transformedAttributes: Record = {}; + + for (const [key, attribute] of Object.entries(attributes)) { + transformedAttributes[key] = { + name: key.charAt(0).toUpperCase() + key.slice(1), + type: this.normalizeAttributeType(attribute.type || 'string'), + }; + } + + return transformedAttributes; + } + + private normalizeAttributeType(type: string): string { + const typeMap: Record = { + 'boolean': 'bool', + 'array': 'array', + 'string': 'string', + // Add other type mappings as needed + }; + return typeMap[type.toLowerCase()] || type; } } diff --git a/source/commands/env/export/templates/condition-set.hcl b/source/commands/env/export/templates/condition-set.hcl index 7c01b37..c64ca96 100644 --- a/source/commands/env/export/templates/condition-set.hcl +++ b/source/commands/env/export/templates/condition-set.hcl @@ -1,13 +1,16 @@ -{{#each conditionSets}} -resource "permitio_{{resourceType}}" "{{key}}" { - key = "{{key}}" - name = "{{name}}" +{{#each sets}} +resource "permitio_user_set" "{{key}}" { + name = "{{name}}" + key = "{{key}}" {{#if description}} description = "{{description}}" {{/if}} - conditions = {{conditions}} + conditions = {{{conditions}}} {{#if resource}} - resource = "{{resource}}" + resource = permitio_resource.{{resource}}.key + depends_on = [ + permitio_resource.{{resource}} + ] {{/if}} } {{/each}} \ No newline at end of file diff --git a/source/commands/env/export/templates/resource.hcl b/source/commands/env/export/templates/resource.hcl index 96f6875..25d1499 100644 --- a/source/commands/env/export/templates/resource.hcl +++ b/source/commands/env/export/templates/resource.hcl @@ -1,38 +1,35 @@ {{#each resources}} resource "permitio_resource" "{{key}}" { - key = "{{key}}" name = "{{name}}" - {{#if description}} description = "{{description}}" - {{/if}} - {{#if urn}} - urn = "{{urn}}" - {{/if}} - - {{#if actions}} + key = "{{key}}" + actions = { {{#each actions}} "{{@key}}" = { name = "{{name}}" - {{#if description}} - description = "{{description}}" - {{/if}} - } + {{#if description}}description = "{{description}}"{{/if}} + }{{#unless @last}},{{/unless}} {{/each}} } - {{/if}} {{#if attributes}} attributes = { {{#each attributes}} - "{{@key}}" = { + {{@key}} = { + name = "{{name}}" type = "{{type}}" - {{#if description}} - description = "{{description}}" - {{/if}} - } + }{{#unless @last}},{{/unless}} {{/each}} } {{/if}} + + {{#if depends_on}} + depends_on = [ + {{#each depends_on}} + "{{this}}"{{#unless @last}},{{/unless}} + {{/each}} + ] + {{/if}} } {{/each}} \ No newline at end of file diff --git a/source/commands/env/export/utils.ts b/source/commands/env/export/utils.ts index f0df56e..d7da01d 100644 --- a/source/commands/env/export/utils.ts +++ b/source/commands/env/export/utils.ts @@ -25,7 +25,7 @@ terraform { required_providers { permitio = { source = "permitio/permit-io" - version = "~> 0.1.0" + version = "~> 0.0.12" } } }