| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132 |
- // v2.3 — project addressing.
- //
- // Collections live inside a "project" namespace. On the wire, an
- // unqualified name like "users" resolves to ("default", "users"); an
- // explicit name like "my_app:users" resolves to ("my_app", "users").
- // The default project is auto-created on first boot and is the v2.2
- // back-compat path: every existing client that doesn't set a project
- // addresses "default" and observes the same data it always did.
- //
- // Storage maps one project → one LmdbEnv at <dataDir>/projects/<name>/env/.
- // gRPC handlers parse the input here and dispatch the rest of the call
- // against the resolved project store.
- //
- // Constraints (mirrors the collection-name rules so the joined wire form
- // stays unambiguous):
- // - project name: ^[a-zA-Z_][a-zA-Z0-9_-]{0,62}$ (1..63 chars)
- // - collection name: same rules as v2.2 (already enforced upstream)
- // - separator is a single ':' (configurable below if we ever need to)
- //
- // Reserved project names:
- // - "default" is always present and addressable; recreating it via
- // CreateProject is a no-op (idempotent).
- // - Names starting with '_' are reserved for system use, same as
- // collection sub-db names.
- #pragma once
- #include <stdexcept>
- #include <string>
- #include <string_view>
- namespace smartbotic::database {
- constexpr const char* kDefaultProject = "default";
- constexpr char kProjectSeparator = ':';
- constexpr size_t kMaxProjectNameLen = 63;
- struct ProjectCollection {
- std::string project;
- std::string collection;
- };
- // Edge-case semantics (chosen to fail loud rather than silently coerce):
- // "" -> std::invalid_argument
- // "users" -> ("default", "users")
- // "my_app:users" -> ("my_app", "users")
- // ":users" -> std::invalid_argument (empty project before ':' is a
- // user error — they typed the separator intentionally)
- // "my_app:" -> std::invalid_argument (empty collection)
- // "a:b:c" -> std::invalid_argument (multiple separators)
- //
- // The returned `project` is guaranteed non-empty and the `collection` is
- // guaranteed non-empty. Project-name validation is NOT done here — the
- // caller calls isValidProjectName() at create-time, so reads of an
- // invalid-but-existing project name still succeed via the parser (the
- // invalid name would have failed validation at create time).
- inline ProjectCollection parseProjectCollection(std::string_view input) {
- if (input.empty()) {
- throw std::invalid_argument("project-collection: empty input");
- }
- const size_t pos = input.find(kProjectSeparator);
- if (pos == std::string_view::npos) {
- return {kDefaultProject, std::string(input)};
- }
- if (pos == 0) {
- throw std::invalid_argument(
- "project-collection: ':' separator at position 0 (empty project) — "
- "use an unqualified name to address the default project");
- }
- if (pos == input.size() - 1) {
- throw std::invalid_argument(
- "project-collection: empty collection after ':' separator");
- }
- // Reject multiple separators — ambiguous.
- if (input.find(kProjectSeparator, pos + 1) != std::string_view::npos) {
- throw std::invalid_argument(
- "project-collection: only one ':' separator allowed");
- }
- return {std::string(input.substr(0, pos)),
- std::string(input.substr(pos + 1))};
- }
- // One-call helper for gRPC handlers. Parses + builds the qualified form
- // in one go. The qualified form is what MemoryStore uses as its
- // collection key in v2.3+ (the bare collection name is what each
- // per-project LMDB DocumentStore sees).
- //
- // ResolvedCollection rc = resolveCollection("my_app:users");
- // // rc.project = "my_app"
- // // rc.collection = "users"
- // // rc.qualified = "my_app:users"
- //
- // ResolvedCollection rc = resolveCollection("users");
- // // rc.project = "default"
- // // rc.collection = "users"
- // // rc.qualified = "default:users"
- struct ResolvedCollection {
- std::string project;
- std::string collection;
- std::string qualified;
- };
- inline ResolvedCollection resolveCollection(std::string_view input) {
- auto pc = parseProjectCollection(input);
- std::string qualified;
- qualified.reserve(pc.project.size() + 1 + pc.collection.size());
- qualified.append(pc.project);
- qualified.push_back(kProjectSeparator);
- qualified.append(pc.collection);
- return {std::move(pc.project), std::move(pc.collection), std::move(qualified)};
- }
- // True iff `name` matches the project-name regex AND is not a reserved
- // system name. Used by CreateProject + the lazy first-write path.
- inline bool isValidProjectName(std::string_view name) {
- if (name.empty() || name.size() > kMaxProjectNameLen) return false;
- if (name[0] == '_') return false; // reserved
- auto valid_first = [](char c) {
- return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
- };
- auto valid_rest = [](char c) {
- return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
- || (c >= '0' && c <= '9') || c == '_' || c == '-';
- };
- if (!valid_first(name[0])) return false;
- for (size_t i = 1; i < name.size(); ++i) {
- if (!valid_rest(name[i])) return false;
- }
- return true;
- }
- } // namespace smartbotic::database
|