project_addressing.hpp 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. // v2.3 — project addressing.
  2. //
  3. // Collections live inside a "project" namespace. On the wire, an
  4. // unqualified name like "users" resolves to ("default", "users"); an
  5. // explicit name like "my_app:users" resolves to ("my_app", "users").
  6. // The default project is auto-created on first boot and is the v2.2
  7. // back-compat path: every existing client that doesn't set a project
  8. // addresses "default" and observes the same data it always did.
  9. //
  10. // Storage maps one project → one LmdbEnv at <dataDir>/projects/<name>/env/.
  11. // gRPC handlers parse the input here and dispatch the rest of the call
  12. // against the resolved project store.
  13. //
  14. // Constraints (mirrors the collection-name rules so the joined wire form
  15. // stays unambiguous):
  16. // - project name: ^[a-zA-Z_][a-zA-Z0-9_-]{0,62}$ (1..63 chars)
  17. // - collection name: same rules as v2.2 (already enforced upstream)
  18. // - separator is a single ':' (configurable below if we ever need to)
  19. //
  20. // Reserved project names:
  21. // - "default" is always present and addressable; recreating it via
  22. // CreateProject is a no-op (idempotent).
  23. // - Names starting with '_' are reserved for system use, same as
  24. // collection sub-db names.
  25. #pragma once
  26. #include <stdexcept>
  27. #include <string>
  28. #include <string_view>
  29. namespace smartbotic::database {
  30. constexpr const char* kDefaultProject = "default";
  31. constexpr char kProjectSeparator = ':';
  32. constexpr size_t kMaxProjectNameLen = 63;
  33. struct ProjectCollection {
  34. std::string project;
  35. std::string collection;
  36. };
  37. // Edge-case semantics (chosen to fail loud rather than silently coerce):
  38. // "" -> std::invalid_argument
  39. // "users" -> ("default", "users")
  40. // "my_app:users" -> ("my_app", "users")
  41. // ":users" -> std::invalid_argument (empty project before ':' is a
  42. // user error — they typed the separator intentionally)
  43. // "my_app:" -> std::invalid_argument (empty collection)
  44. // "a:b:c" -> std::invalid_argument (multiple separators)
  45. //
  46. // The returned `project` is guaranteed non-empty and the `collection` is
  47. // guaranteed non-empty. Project-name validation is NOT done here — the
  48. // caller calls isValidProjectName() at create-time, so reads of an
  49. // invalid-but-existing project name still succeed via the parser (the
  50. // invalid name would have failed validation at create time).
  51. inline ProjectCollection parseProjectCollection(std::string_view input) {
  52. if (input.empty()) {
  53. throw std::invalid_argument("project-collection: empty input");
  54. }
  55. const size_t pos = input.find(kProjectSeparator);
  56. if (pos == std::string_view::npos) {
  57. return {kDefaultProject, std::string(input)};
  58. }
  59. if (pos == 0) {
  60. throw std::invalid_argument(
  61. "project-collection: ':' separator at position 0 (empty project) — "
  62. "use an unqualified name to address the default project");
  63. }
  64. if (pos == input.size() - 1) {
  65. throw std::invalid_argument(
  66. "project-collection: empty collection after ':' separator");
  67. }
  68. // Reject multiple separators — ambiguous.
  69. if (input.find(kProjectSeparator, pos + 1) != std::string_view::npos) {
  70. throw std::invalid_argument(
  71. "project-collection: only one ':' separator allowed");
  72. }
  73. return {std::string(input.substr(0, pos)),
  74. std::string(input.substr(pos + 1))};
  75. }
  76. // One-call helper for gRPC handlers. Parses + builds the qualified form
  77. // in one go. The qualified form is what MemoryStore uses as its
  78. // collection key in v2.3+ (the bare collection name is what each
  79. // per-project LMDB DocumentStore sees).
  80. //
  81. // ResolvedCollection rc = resolveCollection("my_app:users");
  82. // // rc.project = "my_app"
  83. // // rc.collection = "users"
  84. // // rc.qualified = "my_app:users"
  85. //
  86. // ResolvedCollection rc = resolveCollection("users");
  87. // // rc.project = "default"
  88. // // rc.collection = "users"
  89. // // rc.qualified = "default:users"
  90. struct ResolvedCollection {
  91. std::string project;
  92. std::string collection;
  93. std::string qualified;
  94. };
  95. inline ResolvedCollection resolveCollection(std::string_view input) {
  96. auto pc = parseProjectCollection(input);
  97. std::string qualified;
  98. qualified.reserve(pc.project.size() + 1 + pc.collection.size());
  99. qualified.append(pc.project);
  100. qualified.push_back(kProjectSeparator);
  101. qualified.append(pc.collection);
  102. return {std::move(pc.project), std::move(pc.collection), std::move(qualified)};
  103. }
  104. // True iff `name` matches the project-name regex AND is not a reserved
  105. // system name. Used by CreateProject + the lazy first-write path.
  106. inline bool isValidProjectName(std::string_view name) {
  107. if (name.empty() || name.size() > kMaxProjectNameLen) return false;
  108. if (name[0] == '_') return false; // reserved
  109. auto valid_first = [](char c) {
  110. return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
  111. };
  112. auto valid_rest = [](char c) {
  113. return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
  114. || (c >= '0' && c <= '9') || c == '_' || c == '-';
  115. };
  116. if (!valid_first(name[0])) return false;
  117. for (size_t i = 1; i < name.size(); ++i) {
  118. if (!valid_rest(name[i])) return false;
  119. }
  120. return true;
  121. }
  122. } // namespace smartbotic::database