archsight 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/docs/computed_annotations.md +7 -5
  4. data/docs/licenses.md +1 -2
  5. data/docs/modeling.md +5 -1
  6. data/docs/pages.md +63 -6
  7. data/docs/search.md +17 -0
  8. data/lib/archsight/annotations/annotation.rb +5 -4
  9. data/lib/archsight/annotations/computed.rb +5 -1
  10. data/lib/archsight/annotations/relation_resolver.rb +96 -79
  11. data/lib/archsight/cli.rb +3 -2
  12. data/lib/archsight/database.rb +2 -1
  13. data/lib/archsight/documentation.rb +2 -1
  14. data/lib/archsight/export/confluence/exporter.rb +11 -2
  15. data/lib/archsight/export/confluence/storage.rb +99 -8
  16. data/lib/archsight/export/confluence/tables.rb +78 -0
  17. data/lib/archsight/helpers/fenced_blocks.rb +61 -0
  18. data/lib/archsight/helpers/requirements_blocks.rb +90 -0
  19. data/lib/archsight/helpers/view_blocks.rb +108 -0
  20. data/lib/archsight/helpers/wiki_links.rb +39 -5
  21. data/lib/archsight/helpers.rb +3 -0
  22. data/lib/archsight/import/handlers/go_grapher.rb +4 -1
  23. data/lib/archsight/import/handlers/go_module_parser.rb +4 -1
  24. data/lib/archsight/linter.rb +25 -3
  25. data/lib/archsight/mcp/base.rb +38 -0
  26. data/lib/archsight/requirements.rb +70 -0
  27. data/lib/archsight/resources/analysis.rb +2 -1
  28. data/lib/archsight/resources/application_component.rb +5 -3
  29. data/lib/archsight/resources/application_interface.rb +4 -2
  30. data/lib/archsight/resources/application_service.rb +4 -3
  31. data/lib/archsight/resources/base.rb +27 -12
  32. data/lib/archsight/resources/business_actor.rb +5 -3
  33. data/lib/archsight/resources/business_product.rb +4 -3
  34. data/lib/archsight/resources/business_requirement.rb +4 -3
  35. data/lib/archsight/resources/compliance_evidence.rb +4 -2
  36. data/lib/archsight/resources/data_object.rb +2 -1
  37. data/lib/archsight/resources/import.rb +5 -2
  38. data/lib/archsight/resources/page.rb +4 -2
  39. data/lib/archsight/resources/technology_artifact.rb +4 -3
  40. data/lib/archsight/resources/technology_node.rb +1 -1
  41. data/lib/archsight/resources/technology_service.rb +4 -0
  42. data/lib/archsight/resources/technology_system_software.rb +4 -0
  43. data/lib/archsight/resources/view.rb +2 -1
  44. data/lib/archsight/version.rb +1 -1
  45. data/lib/archsight/view_table.rb +102 -0
  46. data/lib/archsight/web/api/json_helpers.rb +7 -5
  47. data/lib/archsight/web/api/openapi/spec.yaml +135 -2
  48. data/lib/archsight/web/api/requirements_helpers.rb +26 -0
  49. data/lib/archsight/web/api/routes.rb +19 -0
  50. data/lib/archsight/web/application.rb +6 -1
  51. data/lib/archsight/web/public/vue/ApiDocsPage-D-cPRZCT.js +1 -0
  52. data/lib/archsight/web/public/vue/ApiDocsPage-DZ0fa5-h.css +1 -0
  53. data/lib/archsight/web/public/vue/{DocPage-uaT8CdFm.js → DocPage-DK6vNDbF.js} +1 -1
  54. data/lib/archsight/web/public/vue/EditorPage-BoJpQaVw.js +35 -0
  55. data/lib/archsight/web/public/vue/EditorPage-CbG4mc9T.css +1 -0
  56. data/lib/archsight/web/public/vue/ErrorPage-Ck9izEUS.css +1 -0
  57. data/lib/archsight/web/public/vue/ErrorPage-PGyjdtEf.js +2 -0
  58. data/lib/archsight/web/public/vue/GraphView-BLiKR4zP.js +1 -0
  59. data/lib/archsight/web/public/vue/GraphView-BvWAbUAl.css +1 -0
  60. data/lib/archsight/web/public/vue/HomePage-C0lR8i2C.js +2 -0
  61. data/lib/archsight/web/public/vue/InstanceRouter-60Tt3ZNM.css +1 -0
  62. data/lib/archsight/web/public/vue/InstanceRouter-D3W2jJHV.js +1 -0
  63. data/lib/archsight/web/public/vue/KindList-BlsaRBNO.js +1 -0
  64. data/lib/archsight/web/public/vue/PageView-9MgHtrgl.js +1 -0
  65. data/lib/archsight/web/public/vue/QueryError-3EqjpbyV.css +1 -0
  66. data/lib/archsight/web/public/vue/QueryError-D1FL1xgA.js +1 -0
  67. data/lib/archsight/web/public/vue/ResourceList-B0FLaFR3.css +1 -0
  68. data/lib/archsight/web/public/vue/ResourceList-vkgFyeOY.js +2 -0
  69. data/lib/archsight/web/public/vue/SearchResults-Cl3O_OEr.js +1 -0
  70. data/lib/archsight/web/public/vue/SearchResults-DiW5XVYW.css +1 -0
  71. data/lib/archsight/web/public/vue/WikiPage-C-8SG67a.css +1 -0
  72. data/lib/archsight/web/public/vue/WikiPage-CeCQBTDS.js +13 -0
  73. data/lib/archsight/web/public/vue/architecture-7GRP2DOG-BfA1TPxQ.js +1 -0
  74. data/lib/archsight/web/public/vue/cynefin-OW5HDTMX-BmgdUKVD.js +1 -0
  75. data/lib/archsight/web/public/vue/eventmodeling-NTZA5JFV-DbXDvAWF.js +1 -0
  76. data/lib/archsight/web/public/vue/gitGraph-4MIJSDKK-BkVhrKS1.js +1 -0
  77. data/lib/archsight/web/public/vue/index-D7m61Ahx.js +3 -0
  78. data/lib/archsight/web/public/vue/index-Dbx3MXWG.css +1 -0
  79. data/lib/archsight/web/public/vue/info-A6RAGUB7-BIlCVuUY.js +1 -0
  80. data/lib/archsight/web/public/vue/mermaid-BjEi5URd.js +3334 -0
  81. data/lib/archsight/web/public/vue/packet-AYTQ26CC-eoQTjY3j.js +1 -0
  82. data/lib/archsight/web/public/vue/pie-WAS4IAKB-DEOotGhh.js +1 -0
  83. data/lib/archsight/web/public/vue/radar-RG4KPBEZ-C2yucuUN.js +1 -0
  84. data/lib/archsight/web/public/vue/railroad-74A4TZTK-CzONK5VY.js +1 -0
  85. data/lib/archsight/web/public/vue/railroad-abnf-HS5TGJTU-9zRPsDsx.js +1 -0
  86. data/lib/archsight/web/public/vue/railroad-ebnf-LZEXJU2U-BJ4dkz9l.js +1 -0
  87. data/lib/archsight/web/public/vue/railroad-peg-WCYAUIDC-DFy_rqAj.js +1 -0
  88. data/lib/archsight/web/public/vue/treeView-Q6P3EWNA-2cq5CyD2.js +1 -0
  89. data/lib/archsight/web/public/vue/treemap-WGGIJYW6-DK-e9Ez4.js +1 -0
  90. data/lib/archsight/web/public/vue/{useGraphviz-C71SdG-N.js → useGraphviz-DweKV7Kg.js} +1 -1
  91. data/lib/archsight/web/public/vue/wardley-WFR3VGLG-BQwqUqfB.js +1 -0
  92. data/lib/archsight/web/public/vue.html +3 -3
  93. data/lib/archsight.rb +2 -0
  94. metadata +49 -40
  95. data/lib/archsight/web/public/vue/ApiDocsPage-C0y953v0.css +0 -1
  96. data/lib/archsight/web/public/vue/ApiDocsPage-C_4tAWis.js +0 -1
  97. data/lib/archsight/web/public/vue/EditorPage-C557BJC-.js +0 -35
  98. data/lib/archsight/web/public/vue/EditorPage-df5N-p21.css +0 -1
  99. data/lib/archsight/web/public/vue/ErrorPage-Vdebmife.js +0 -2
  100. data/lib/archsight/web/public/vue/ErrorPage-uMDnfY5_.css +0 -1
  101. data/lib/archsight/web/public/vue/GraphView-BduUql2N.js +0 -1
  102. data/lib/archsight/web/public/vue/GraphView-Cj2V2stN.css +0 -1
  103. data/lib/archsight/web/public/vue/HomePage-BHUTg8Ap.js +0 -2
  104. data/lib/archsight/web/public/vue/InstanceRouter-1lSigA58.js +0 -1
  105. data/lib/archsight/web/public/vue/InstanceRouter-Di7f3Rya.css +0 -1
  106. data/lib/archsight/web/public/vue/KindList-BDZs0j6d.js +0 -1
  107. data/lib/archsight/web/public/vue/PageView-BYzJDwof.js +0 -1
  108. data/lib/archsight/web/public/vue/ResourceList-CnbhU9wm.js +0 -1
  109. data/lib/archsight/web/public/vue/ResourceList-xyBwu7fh.css +0 -1
  110. data/lib/archsight/web/public/vue/SearchResults-CNhf6VOx.js +0 -1
  111. data/lib/archsight/web/public/vue/SearchResults-DOHzqAy3.css +0 -1
  112. data/lib/archsight/web/public/vue/WikiPage-DbmWkM7W.js +0 -13
  113. data/lib/archsight/web/public/vue/WikiPage-GV67QmNJ.css +0 -1
  114. data/lib/archsight/web/public/vue/architecture-TIHT7OUA-Bf-MWFmn.js +0 -1
  115. data/lib/archsight/web/public/vue/cynefin-VYW2F7L2-CkKt8qqs.js +0 -1
  116. data/lib/archsight/web/public/vue/eventmodeling-45OFAUF4-NwSTYPHj.js +0 -1
  117. data/lib/archsight/web/public/vue/gitGraph-TEB2WS4Q-DLRegrRG.js +0 -1
  118. data/lib/archsight/web/public/vue/index-CyVWObLU.js +0 -3
  119. data/lib/archsight/web/public/vue/index-DtKeHT3S.css +0 -1
  120. data/lib/archsight/web/public/vue/info-DKCQHKI2-EV39NzoN.js +0 -1
  121. data/lib/archsight/web/public/vue/mermaid-BMkGfnhm.js +0 -3279
  122. data/lib/archsight/web/public/vue/packet-7NZHBO7P-USljV3MZ.js +0 -1
  123. data/lib/archsight/web/public/vue/pie-RZYD4A2V-DChciBW2.js +0 -1
  124. data/lib/archsight/web/public/vue/radar-I7S5WNFK-CgGJdW1t.js +0 -1
  125. data/lib/archsight/web/public/vue/railroad-3IZDKUUU-Dy43vF5c.js +0 -1
  126. data/lib/archsight/web/public/vue/railroad-abnf-AHOZXSZD-BhsTH-sE.js +0 -1
  127. data/lib/archsight/web/public/vue/railroad-ebnf-EBAXGLYW-BBTd_u0u.js +0 -1
  128. data/lib/archsight/web/public/vue/railroad-peg-LSFZ7HO6-BiNTEzf0.js +0 -1
  129. data/lib/archsight/web/public/vue/treeView-QDETBFTQ-BfnCcf-q.js +0 -1
  130. data/lib/archsight/web/public/vue/treemap-6X3UGDF4-BsAORhvk.js +0 -1
  131. data/lib/archsight/web/public/vue/wardley-OPB4EBWU-DQvnEhRj.js +0 -1
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Archsight
4
+ # The result of a view as a plain table (columns and rows of text), for places that cannot run the frontend, such
5
+ # as the Confluence export. It follows what the web UI shows for a View (`ViewResults.vue`, `ResourceList.vue`):
6
+ # a Name column, a Kind column unless the view is `list:name`, one column per `view/fields` entry that is not
7
+ # `name` or `kind`, rows sorted by `view/sort`.
8
+ module ViewTable
9
+ # Rows beyond this are cut (the table says how many); a Confluence page is no place for thousands of rows
10
+ LIMIT = 200
11
+
12
+ # @param as [Symbol] :text, :status or :priority (a requirement's, shown as a lozenge) or :markdown
13
+ # @param resources [Array] resources the cell names (shown one per line, linked where a target exists)
14
+ Cell = Struct.new(:text, :resources, :as, keyword_init: true) do
15
+ def initialize(text: "", resources: [], as: :text) = super
16
+ end
17
+
18
+ # @param title [String] shown above the table, may be empty
19
+ # @param columns [Array<String>] header texts
20
+ # @param rows [Array<Array<Cell>>] at most LIMIT
21
+ # @param total [Integer] number of rows before cutting
22
+ Table = Struct.new(:title, :columns, :rows, :total, keyword_init: true) do
23
+ def cut = total - rows.length
24
+ end
25
+
26
+ IDENTITY_FIELDS = %w[name kind].freeze
27
+
28
+ module_function
29
+
30
+ # @raise [Archsight::Query::QueryError] if the query does not parse
31
+ def build(database, query:, fields: [], sort: [], show_kind: true, title: "", limit: LIMIT)
32
+ resources = sort_resources(Archsight::Query.parse(query).filter(database), sort)
33
+ columns = fields.reject { |f| IDENTITY_FIELDS.include?(f) }
34
+ header = ["Name", (show_kind ? "Kind" : nil), *columns.map { |f| column_title(f) }].compact
35
+ rows = resources.first(limit).map do |resource|
36
+ [Cell.new(text: resource.name, resources: [resource]),
37
+ (Cell.new(text: kind_of(resource)) if show_kind),
38
+ *columns.map { |f| Cell.new(text: resource.annotations[f].to_s) }].compact
39
+ end
40
+ Table.new(title: title, columns: header, rows: rows, total: resources.length)
41
+ end
42
+
43
+ # The table of a ```view block (see Helpers::ViewBlocks)
44
+ # @raise [Helpers::ViewBlocks::Error, Archsight::Query::QueryError]
45
+ def from_block(database, source)
46
+ spec = Helpers::ViewBlocks.parse(source)
47
+ build(database, query: spec[:query], fields: spec[:fields], sort: spec[:sort], show_kind: spec[:type] == "list:name+kind", title: spec[:title])
48
+ end
49
+
50
+ # The table of a View resource
51
+ # @raise [Helpers::ViewBlocks::Error, Archsight::Query::QueryError]
52
+ def from_view(database, view)
53
+ annotations = view.annotations
54
+ query = annotations["view/query"].to_s.strip
55
+ raise Helpers::ViewBlocks::Error, "view #{view.name} has no query" if query.empty?
56
+
57
+ build(database, query: query, fields: list(annotations["view/fields"]), sort: list(annotations["view/sort"]),
58
+ show_kind: (annotations["view/type"] || "list:name+kind") == "list:name+kind", title: view.name)
59
+ end
60
+
61
+ # "scc/language/Go/loc" -> "Go loc", "activity/createdAt" -> "Activity created At" (as ResourceList.vue titles columns)
62
+ def column_title(key)
63
+ segments = key.split("/")
64
+ title = segments.length >= 2 ? "#{segments[-2]} #{segments[-1]}" : segments.last.to_s
65
+ title.gsub(/([a-z])([A-Z])/, '\1 \2').sub(/\A./, &:upcase)
66
+ end
67
+
68
+ def kind_of(resource) = resource.class.name.split("::").last
69
+
70
+ def list(value) = value.to_s.split(",").map(&:strip).reject(&:empty?)
71
+
72
+ # `view/sort` fields: name, kind or an annotation, "-" for descending; numbers inside text compare as numbers.
73
+ # Without a sort the order is by name, like the search API.
74
+ def sort_resources(resources, sort)
75
+ return resources.sort_by(&:name) if sort.empty?
76
+
77
+ resources.sort { |left, right| compare(left, right, sort) }
78
+ end
79
+
80
+ def compare(left, right, sort)
81
+ sort.each do |field|
82
+ key = field.delete_prefix("-")
83
+ order = natural(value_of(left, key)) <=> natural(value_of(right, key))
84
+ return field.start_with?("-") ? -order : order unless order.zero?
85
+ end
86
+ 0
87
+ end
88
+
89
+ def value_of(resource, key)
90
+ case key
91
+ when "name" then resource.name
92
+ when "kind" then kind_of(resource)
93
+ else resource.annotations[key].to_s
94
+ end
95
+ end
96
+
97
+ # A key for sorting text with numbers in it the way people expect (item2 < item10, case-insensitive first)
98
+ def natural(text)
99
+ [text.scan(/\d+|\D+/).map { |part| part.match?(/\A\d/) ? [0, part.to_i] : [1, part.downcase] }, text]
100
+ end
101
+ end
102
+ end
@@ -130,13 +130,11 @@ module Archsight::Web::API::JsonHelpers
130
130
  end
131
131
 
132
132
  def build_count_response(query, results, query_time_ms)
133
- by_kind = results.group_by { |r| r.class.to_s.split("::").last }
134
- .transform_values(&:length)
135
133
  {
136
134
  query: query,
137
135
  total: results.length,
138
136
  query_time_ms: query_time_ms,
139
- by_kind: by_kind
137
+ by_kind: Archsight::MCP.count_by_kind(results)
140
138
  }
141
139
  end
142
140
 
@@ -144,10 +142,13 @@ module Archsight::Web::API::JsonHelpers
144
142
  limit, offset = parse_pagination_params
145
143
  output = parse_output_param
146
144
  sorted = results.sort_by(&:name)
147
- pagination = paginate(sorted, limit: limit, offset: offset)
145
+ # `kind` narrows the hits to one kind; by_kind always counts all hits so the facets stay complete
146
+ kind = params[:kind].to_s
147
+ visible = kind.empty? ? sorted : sorted.select { |r| Archsight::MCP.kind_of(r) == kind }
148
+ pagination = paginate(visible, limit: limit, offset: offset)
148
149
 
149
150
  instances = pagination[:items].map do |r|
150
- resource_summary(r, output: output, omit_kind: false)
151
+ resource_summary(r, output: output, omit_kind: false).merge(highlights: Archsight::MCP.highlights(r))
151
152
  end
152
153
 
153
154
  {
@@ -157,6 +158,7 @@ module Archsight::Web::API::JsonHelpers
157
158
  offset: pagination[:offset],
158
159
  count: instances.length,
159
160
  query_time_ms: query_time_ms,
161
+ by_kind: Archsight::MCP.count_by_kind(results),
160
162
  instances: instances
161
163
  }
162
164
  end
@@ -650,7 +650,12 @@ paths:
650
650
  See the API description for full query language documentation.
651
651
 
652
652
  Response includes: kind, name, metadata, spec (no relations/references).
653
- Additional fields: query, query_time_ms, by_kind (count mode).
653
+ Additional fields: query, query_time_ms, by_kind.
654
+
655
+ Every hit carries `highlights`: the values of the kind's summary attributes (annotations marked
656
+ `summary: true` in the kind definition, at most 3 per kind, computed ones included) that are set.
657
+ `by_kind` counts all hits of the query per kind, also when `kind` narrows the list, so a client can show
658
+ facets and let the user pick one.
654
659
  operationId: search
655
660
  tags:
656
661
  - Search
@@ -674,6 +679,13 @@ paths:
674
679
  orphan:
675
680
  value: '<- none'
676
681
  summary: Find orphan resources
682
+ - name: kind
683
+ in: query
684
+ required: false
685
+ description: Only return hits of this kind (`total` and paging follow; `by_kind` still counts all hits)
686
+ schema:
687
+ type: string
688
+ example: TechnologyArtifact
677
689
  - $ref: '#/components/parameters/limit'
678
690
  - $ref: '#/components/parameters/offset'
679
691
  - $ref: '#/components/parameters/output'
@@ -691,6 +703,88 @@ paths:
691
703
  schema:
692
704
  $ref: '#/components/schemas/QueryError'
693
705
 
706
+ /api/v1/requirements:
707
+ get:
708
+ summary: Business requirements of a selection of resources
709
+ description: |
710
+ The business requirements that the resources selected by `of` realize (`implemented`), partially realize
711
+ (`partial`) or plan (`planned`), merged over all of them: one entry per requirement with the best status and
712
+ the resources it comes from (`by`). Sorted by priority (must, should, may, none), then name. `story` is
713
+ rendered HTML.
714
+ operationId: getRequirements
715
+ tags:
716
+ - Search
717
+ parameters:
718
+ - name: of
719
+ in: query
720
+ required: true
721
+ description: Query selecting the resources whose requirements are listed
722
+ schema:
723
+ type: string
724
+ example: 'ApplicationService: name =~ "Backup"'
725
+ - name: priority
726
+ in: query
727
+ required: false
728
+ description: Comma separated list, only requirements with one of these priorities
729
+ schema:
730
+ type: string
731
+ example: must,should
732
+ - name: status
733
+ in: query
734
+ required: false
735
+ description: Comma separated list, only requirements with one of these statuses
736
+ schema:
737
+ type: string
738
+ example: implemented,partial
739
+ responses:
740
+ '200':
741
+ description: The requirements
742
+ content:
743
+ application/json:
744
+ schema:
745
+ type: object
746
+ properties:
747
+ query:
748
+ type: string
749
+ total:
750
+ type: integer
751
+ query_time_ms:
752
+ type: number
753
+ requirements:
754
+ type: array
755
+ items:
756
+ type: object
757
+ properties:
758
+ name:
759
+ type: string
760
+ status:
761
+ type: string
762
+ enum: [implemented, partial, planned]
763
+ priority:
764
+ type: string
765
+ nullable: true
766
+ story:
767
+ type: string
768
+ nullable: true
769
+ by:
770
+ type: array
771
+ items:
772
+ type: object
773
+ properties:
774
+ kind:
775
+ type: string
776
+ name:
777
+ type: string
778
+ status:
779
+ type: string
780
+ enum: [implemented, partial, planned]
781
+ '400':
782
+ description: Invalid query, or an unknown priority or status
783
+ content:
784
+ application/json:
785
+ schema:
786
+ $ref: '#/components/schemas/QueryError'
787
+
694
788
  /dot:
695
789
  get:
696
790
  summary: Get full architecture graph as DOT
@@ -1225,6 +1319,40 @@ components:
1225
1319
  items:
1226
1320
  $ref: '#/components/schemas/Kind'
1227
1321
 
1322
+ Highlight:
1323
+ type: object
1324
+ required:
1325
+ - key
1326
+ - title
1327
+ - value
1328
+ properties:
1329
+ key:
1330
+ type: string
1331
+ description: Annotation key, exactly as the query language takes it
1332
+ example: activity/status
1333
+ title:
1334
+ type: string
1335
+ description: Display title of the annotation
1336
+ example: Activity Status
1337
+ value:
1338
+ description: The value; an array for list annotations, a number for Integer/Float typed ones
1339
+ oneOf:
1340
+ - type: string
1341
+ - type: number
1342
+ - type: array
1343
+ items:
1344
+ type: string
1345
+ format:
1346
+ type: string
1347
+ nullable: true
1348
+ description: Rendering hint of the annotation (tag_word, tag_list, markdown, link, ...)
1349
+ example: tag_word
1350
+ type:
1351
+ type: string
1352
+ nullable: true
1353
+ description: Short name of the value type when the annotation declares one (Integer, Float, Time, Person, ...)
1354
+ example: Integer
1355
+
1228
1356
  Instance:
1229
1357
  type: object
1230
1358
  required:
@@ -1247,6 +1375,11 @@ components:
1247
1375
  type: object
1248
1376
  additionalProperties: true
1249
1377
  description: Instance specification
1378
+ highlights:
1379
+ type: array
1380
+ description: Values of the kind's summary attributes that are set (search results only)
1381
+ items:
1382
+ $ref: '#/components/schemas/Highlight'
1250
1383
  relations:
1251
1384
  type: object
1252
1385
  additionalProperties:
@@ -1307,7 +1440,7 @@ components:
1307
1440
  type: object
1308
1441
  additionalProperties:
1309
1442
  type: integer
1310
- description: Result counts by kind (count mode only)
1443
+ description: Result counts by kind (search and count mode; all hits of the query, not only the current page)
1311
1444
  instances:
1312
1445
  type: array
1313
1446
  items:
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Archsight; end
4
+ module Archsight::Web; end
5
+ module Archsight::Web::API; end
6
+
7
+ # Parameters and JSON of `GET /api/v1/requirements` (see Archsight::Requirements)
8
+ module Archsight::Web::API::RequirementsHelpers
9
+ # @param name [Symbol] a comma separated list parameter, every value of which must be one of `allowed`
10
+ def csv_param(name, allowed)
11
+ values = params[name].to_s.split(",").map(&:strip).reject(&:empty?)
12
+ invalid = values - allowed
13
+ json_error("Parameter '#{name}' must be #{allowed.join(", ")}, not '#{invalid.first}'", status: 400, error_type: "BadRequest") unless invalid.empty?
14
+
15
+ values
16
+ end
17
+
18
+ def build_requirements_response(query, requirements, query_time_ms)
19
+ {
20
+ query: query,
21
+ total: requirements.length,
22
+ query_time_ms: query_time_ms,
23
+ requirements: requirements.map { |r| r.merge(story: r[:story] && markdown(r[:story])) }
24
+ }
25
+ end
26
+ end
@@ -4,6 +4,7 @@ require "sinatra/base"
4
4
  require "sinatra/extension"
5
5
  require_relative "json_helpers"
6
6
  require_relative "page_helpers"
7
+ require_relative "requirements_helpers"
7
8
  require_relative "../../assets"
8
9
 
9
10
  module Archsight; end
@@ -16,6 +17,7 @@ module Archsight::Web::API::Routes
16
17
 
17
18
  helpers Archsight::Web::API::JsonHelpers
18
19
  helpers Archsight::Web::API::PageHelpers
20
+ helpers Archsight::Web::API::RequirementsHelpers
19
21
 
20
22
  # Rendering is CPU work, previews are typed, not pasted
21
23
  MAX_DIAGRAM_SOURCE = 100_000
@@ -110,6 +112,23 @@ module Archsight::Web::API::Routes
110
112
  end
111
113
  end
112
114
 
115
+ # GET /api/v1/requirements - The business requirements of the resources a query selects (see Archsight::Requirements)
116
+ get "/api/v1/requirements" do
117
+ query = params[:of]
118
+ json_error("Query parameter 'of' is required", status: 400, error_type: "BadRequest") unless query
119
+
120
+ priority = csv_param(:priority, Archsight::Requirements::PRIORITIES)
121
+ status = csv_param(:status, Archsight::Requirements::STATUSES.values)
122
+ start_time = Time.now
123
+
124
+ begin
125
+ requirements = Archsight::Requirements.collect(db, of: query, priority: priority, status: status)
126
+ json_response(build_requirements_response(query, requirements, ((Time.now - start_time) * 1000).round(2)))
127
+ rescue Archsight::Query::QueryError => e
128
+ json_error(e.message, status: 400, error_type: "QueryError", query: query)
129
+ end
130
+ end
131
+
113
132
  # GET /api/v1/assets/*path - Images and draw.io diagrams that markdown embeds. The path is relative to the
114
133
  # resources directory; only files of the allowed types inside it are ever served, never anything outside of
115
134
  # it and never resource definitions (see Archsight::Assets). Outside, missing and not-served-type all
@@ -140,6 +140,9 @@ class Archsight::Web::Application < Sinatra::Base
140
140
  html = Archsight::Helpers::AssetImages.rewrite(html, base_dir: base, resources_dir: Archsight.resources_dir) if base
141
141
  # ```asd blocks become placeholders until the text passes below are done (see DiagramBlocks)
142
142
  html, diagrams = Archsight::Helpers::DiagramBlocks.extract(html, resolver: Archsight::Helpers::ResourceResolver.new(db))
143
+ # so are ```view and ```requirements blocks (see ViewBlocks, RequirementsBlocks)
144
+ html, views = Archsight::Helpers::ViewBlocks.extract(html)
145
+ html, requirements = Archsight::Helpers::RequirementsBlocks.extract(html)
143
146
 
144
147
  # Resolve relative URLs if we have a git URL (for repository READMEs)
145
148
  if git_url && (base_url = github_raw_base_url(git_url))
@@ -157,7 +160,9 @@ class Archsight::Web::Application < Sinatra::Base
157
160
  # Convert [[Target]] / [[Target|label]] wiki-style links to page and resource links
158
161
  html = Archsight::Helpers::Embeds.new(db).render(html)
159
162
  html = Archsight::Helpers::WikiLinks.new(db).render(Archsight::Helpers::Macros.render(html, context: Archsight::Helpers::Macros::Context.new(db, page)))
160
- Archsight::Helpers::DiagramBlocks.restore(html, diagrams)
163
+ html = Archsight::Helpers::DiagramBlocks.restore(html, diagrams)
164
+ html = Archsight::Helpers::ViewBlocks.restore(html, views)
165
+ Archsight::Helpers::RequirementsBlocks.restore(html, requirements)
161
166
  end
162
167
 
163
168
  # Generate asset path with cache-busting query string based on file mtime
@@ -0,0 +1 @@
1
+ import{$ as e,B as t,H as n,M as r,T as i,X as a,Y as o,j as s,k as c,t as l,x as u}from"./index-D7m61Ahx.js";var d={class:`api-docs-page`},f={key:0,class:`pico-color-red-500`},p=l({__name:`ApiDocsPage`,setup(l){let p=u(),m=o(null),h=o(null);function g(e){return new Promise((t,n)=>{if(window.Redoc)return t();let r=document.createElement(`script`);r.src=e,r.onload=t,r.onerror=()=>n(Error(`Failed to load ReDoc`)),document.head.appendChild(r)})}return t(async()=>{try{await g(`https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js`),window.Redoc.init(`/api/v1/openapi.yaml`,{},m.value)}catch(e){h.value=e.message}}),(t,o)=>(n(),r(`div`,d,[c(`a`,{href:`#`,class:`back-link`,onClick:o[0]||=i(e=>a(p).back(),[`prevent`])},`← Back`),h.value?(n(),r(`div`,f,e(h.value),1)):s(``,!0),c(`div`,{ref_key:`container`,ref:m},null,512)]))}},[[`__scopeId`,`data-v-4823b89b`]]);export{p as default};
@@ -0,0 +1 @@
1
+ .back-link[data-v-4823b89b]{z-index:100;font-size:var(--fs-sm);background:#fff;border:1px solid #ddd;border-radius:4px;padding:6px 14px;text-decoration:none;position:fixed;top:12px;right:12px;box-shadow:0 1px 4px #0000001a}
@@ -1 +1 @@
1
- import{G as e,J as t,O as n,V as r,j as i,k as a,s as o}from"./index-CyVWObLU.js";import{t as s}from"./WikiPage-DbmWkM7W.js";var c={key:1},l={__name:`DocPage`,props:{filename:String},setup(l){let u=l,d=t(null);function f(e,t){let n=new DOMParser().parseFromString(e,`text/html`).body,r=n.querySelector(`:scope > article`)||n,i=r.querySelector(`h1`),a=i?.textContent.trim()||t.split(`/`).pop().replace(/[-_]/g,` `);i?.remove();let o=[...r.querySelectorAll(`h2[id], h3[id], h4[id]`)].map(e=>({level:Number(e.tagName[1]),id:e.id,text:e.textContent.trim()})),s=[{title:`Documentation`}];return t.startsWith(`resources/`)&&s.push({title:`Resource types`}),{title:a,html:r.innerHTML,toc:o.length>=2?o:[],breadcrumb:s}}async function p(){let e=await o(u.filename);d.value=e?f(e,u.filename):{title:`Not found`,html:`<p>Documentation not found.</p>`,toc:[],breadcrumb:[{title:`Documentation`}]},document.title=`${d.value.title} - Archsight`}return e(()=>u.filename,p,{immediate:!0}),(e,t)=>d.value?(r(),a(s,{key:0,title:d.value.title,html:d.value.html,breadcrumb:d.value.breadcrumb,toc:d.value.toc},null,8,[`title`,`html`,`breadcrumb`,`toc`])):(r(),i(`article`,c,[...t[0]||=[n(`p`,null,`Loading...`,-1)]]))}};export{l as default};
1
+ import{A as e,H as t,K as n,M as r,Y as i,k as a,s as o}from"./index-D7m61Ahx.js";import{t as s}from"./WikiPage-CeCQBTDS.js";var c={key:1},l={__name:`DocPage`,props:{filename:String},setup(l){let u=l,d=i(null);function f(e,t){let n=new DOMParser().parseFromString(e,`text/html`).body,r=n.querySelector(`:scope > article`)||n,i=r.querySelector(`h1`),a=i?.textContent.trim()||t.split(`/`).pop().replace(/[-_]/g,` `);i?.remove();let o=[...r.querySelectorAll(`h2[id], h3[id], h4[id]`)].map(e=>({level:Number(e.tagName[1]),id:e.id,text:e.textContent.trim()})),s=[{title:`Documentation`}];return t.startsWith(`resources/`)&&s.push({title:`Resource types`}),{title:a,html:r.innerHTML,toc:o.length>=2?o:[],breadcrumb:s}}async function p(){let e=await o(u.filename);d.value=e?f(e,u.filename):{title:`Not found`,html:`<p>Documentation not found.</p>`,toc:[],breadcrumb:[{title:`Documentation`}]},document.title=`${d.value.title} - Archsight`}return n(()=>u.filename,p,{immediate:!0}),(n,i)=>d.value?(t(),e(s,{key:0,title:d.value.title,html:d.value.html,breadcrumb:d.value.breadcrumb,toc:d.value.toc},null,8,[`title`,`html`,`breadcrumb`,`toc`])):(t(),r(`article`,c,[...i[0]||=[a(`p`,null,`Loading...`,-1)]]))}};export{l as default};