private static string GenerateComponentCollection(TypeDescription type) { var collectionType = $"global::Improbable.Stdlib.ComponentCollection<{type.Fqn()}>"; return($@"public static {collectionType} CreateComponentCollection() {{ return new {collectionType}(ComponentId, Create, ApplyUpdate); }}"); }
public string Generate(TypeDescription type) { if (!HasAnnotation(type, WellKnownAnnotations.CreateTableAttribute)) { return(string.Empty); } var setupCommands = type.Annotations.GetAnnotationStrings(WellKnownAnnotations.CreateTableAttribute, 0); var primaryKeyFields = type.Fields.WithAnnotation(WellKnownAnnotations.PrimaryKeyAttribute).ToList(); if (primaryKeyFields.Count == 0) { throw new Exception($"{type.QualifiedName} is exposed to the database, but no fields are marked with [{WellKnownAnnotations.PrimaryKeyAttribute}]"); } var columnCreator = CreateColumns(type.QualifiedName, type.Fields); var primaryKeyColumnNames = string.Join(", ", primaryKeyFields.Select(f => $@"{f.Name}")); var primaryKey = $"PRIMARY KEY ({primaryKeyColumnNames})"; var selectClause = string.Join(", ", type.Fields.Select(f => $@"{f.Name}{PostgresTypeConversion(f)}")); var ordinal = 0; var indexFields = type.Fields.WithAnnotation(WellKnownAnnotations.IndexAttribute); var typeName = $"{type.Fqn()}"; return($@"public static string CreateTypeTable(string tableName) {{ return $@""CREATE TABLE {{tableName}} ( {Indent(2, columnCreator.TrimEnd())} {Indent(2, primaryKey)} );""; }} public static {typeName} FromQuery(global::Npgsql.NpgsqlDataReader reader) {{ return new {typeName} ( {Indent(2, CreateReader(type, type.Fields)).TrimEnd()} ); }} public const string SelectClause = ""{selectClause}""; {string.Join(Environment.NewLine, type.Fields.Select(f => $"public const int {SnakeCaseToPascalCase(f.Name)}Ordinal = {ordinal++};"))} public struct DatabaseChangeNotification {{ public {type.Fqn()}? Old {{ get; set; }} public {type.Fqn()} New {{ get; set; }} }} public static string InitializeDatabase(string tableName) {{ return $@"" {string.Join(Environment.NewLine, setupCommands)} {{CreateTypeTable(tableName)}} {string.Join(Environment.NewLine, indexFields.Select(f => f.Annotations.GetAnnotationString(WellKnownAnnotations.IndexAttribute, 0).Replace("{fieldName}", f.Name)))} -- Setup change notifications. This maps to the DatabaseChangeNotification class. CREATE OR REPLACE FUNCTION notify_{{tableName}}() RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'UPDATE' THEN PERFORM pg_notify( '{{tableName}}'::text, json_build_object( 'old', row_to_json(OLD), 'new', row_to_json(NEW) )::text); ELSE PERFORM pg_notify( '{{tableName}}'::text, json_build_object( 'new', row_to_json(NEW) )::text); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER notify_{{tableName}}_tgr AFTER INSERT OR UPDATE on {{tableName}} FOR EACH ROW EXECUTE PROCEDURE notify_{{tableName}}();""; }} "); }
public string Generate(TypeDescription type) { var exposedFields = type.Fields.Where(Types.IsFieldExposedToDatabase).ToList(); if (!exposedFields.Any()) { return(string.Empty); } var profileIdFields = type.Fields.Where(f => Annotations.HasAnnotations(f, ProfileIdAnnotation)).ToArray(); if (!profileIdFields.Any()) { throw new InvalidOperationException($"{type.QualifiedName} is missing a string field annotated with {ProfileIdAnnotation}"); } if (profileIdFields.Length != 1) { throw new InvalidOperationException($"{type.QualifiedName} has multiple fields annotated with {ProfileIdAnnotation}. Only one is allowed."); } var profileIdField = profileIdFields[0]; if (!profileIdField.IsSingular() || !profileIdField.HasPrimitive(PrimitiveType.String)) { throw new InvalidOperationException($"{profileIdField.Name} is annotated with {ProfileIdAnnotation}, which requires it to be a string type."); } var profileFieldName = SnakeCaseToPascalCase(profileIdField.Name); var sb = new StringBuilder(); // Provide shortcuts foreach (var field in exposedFields) { sb.AppendLine($"public const string {DatabaseSyncFieldName(field)} = \"{type.QualifiedName}.{field.Name}\";"); sb.AppendLine($"public const string {PathFieldName(field)} = \"{type.ComponentId}_{field.FieldId}\";"); } // Use the static Linq methods below to avoid needing to import System.Linq. const string enumerable = "global::System.Linq.Enumerable"; const string isImmediateChild = "global::Improbable.DatabaseSync.DatabaseSync.IsImmediateChild"; const string insertStatement = "insert into {databaseName} (path, name, count)"; var valueFields = exposedFields.WithAnnotation(ValueAnnotation).ToList(); var listFields = exposedFields.WithAnnotation(ValueListAnnotation).ToList(); sb.AppendLine($@"[global::Improbable.DatabaseSync.Hydrate({type.ComponentId})] public static {SchemaComponentUpdate} Hydrate({GenericIEnumerable}<{DatabaseSyncItemClass}> items, string profilePath) {{ var update = new Update(); foreach(var item in items) {{ // Update count and list fields. switch(item.Name) {{ {string.Join("\n", valueFields.Select(field => Indent(3, $"case {DatabaseSyncFieldName(field)}: update.Set{FieldName(field)}(item.Count); break;")))} {string.Join("\n", listFields.Select(field => Indent(3, $@"case {DatabaseSyncFieldName(field)}: update.Set{FieldName(field)}({enumerable}.ToArray({enumerable}.Where(items, i => {isImmediateChild}(i.Path, item.Path)))); break;")))} }} }} return update.ToSchemaUpdate(); }} [global::Improbable.DatabaseSync.ProfileIdFromSchemaData] public static string GetProfileIdFromComponentData(global::Improbable.Worker.CInterop.SchemaObject fields) {{ return fields.GetString({profileIdField.FieldId}); }} public static string ComponentToDatabase(string databaseName, in {type.Fqn()} item) {{ if (string.IsNullOrEmpty(item.{profileFieldName})) {{ throw new global::System.ArgumentNullException(nameof(item.{profileFieldName})); }} return $@""{string.Join("\n", valueFields.Select(field => $"{insertStatement} values('{{item.{profileFieldName}}}.{{{PathFieldName(field)}}}', '{{{DatabaseSyncFieldName(field)}}}', {{item.{FieldName(field)}}});"))} {string.Join("\n", listFields.Select(field => $"{insertStatement} values('{{item.{profileFieldName}}}.{{{PathFieldName(field)}}}', '{{{DatabaseSyncFieldName(field)}}}', 0);"))}""; }} [global::Improbable.DatabaseSync.ProfileId] public string DatabaseSyncProfileId => {profileFieldName}; { string.Join("\n", exposedFields.Select(field => $@"public string {FieldName(field)}Path() {{ return {profileFieldName} + ""."" + {PathFieldName(field)}; }} ")) }"); return(sb.ToString().TrimEnd()); }
private static string GenerateCommands(TypeDescription type, IEnumerable <ComponentDefinition.CommandDefinition> commands) { var text = new StringBuilder(); var bindingMethods = new StringBuilder(); var commandIndices = new StringBuilder(); foreach (var cmd in commands) { var(request, response) = cmd.InnerFqns(); var cmdName = cmd.Name(); commandIndices.AppendLine($"{cmdName} = {cmd.CommandIndex},"); var boundResponseSender = ""; // Don't allow workers to send command responses for system commands. if (!type.IsRestricted) { boundResponseSender = $@"public void Send{cmdName}Response(long id, {response} response) {{ {type.Fqn()}.Send{cmdName}Response(connection, id, response); }}"; } bindingMethods.AppendLine($@" /// <summary> /// Sends a command to the worker that is authoritative over the bound entityId./>. /// </summary> /// <param name=""request""> The request payload.</param> /// <param name=""cancellation""> A token used to mark the task as cancelled. /// Cancelling will NOT cancel the sending of the command to the authoritative worker; it WILL be processed by the runtime and the target worker. /// It only marks the task as cancelled locally. Use this for flow control or cleanup of state. /// </param> /// <param name=""timeout""> The amount of time that must pass before a command response is considered ""timed out"".</param> /// <param name=""commandParameters""> Options used to configure how the command is sent. </param> /// <returns> A Task containing response payload. </returns> public global::System.Threading.Tasks.Task<{response}> Send{cmdName}Async({request} request, {CancellationTokenType} cancellation = default, uint? timeout = null, global::Improbable.Worker.CInterop.CommandParameters? commandParameters = null) {{ return {type.Fqn()}.Send{cmdName}Async(connection, entityId, request, cancellation, timeout, commandParameters); }} {boundResponseSender}"); var responseSender = ""; // Don't allow workers to send command responses for system commands. if (!type.IsRestricted) { responseSender = $@"public static void Send{cmdName}Response({WorkerConnectionType} connection, long id, {response} response) {{ var schemaResponse = global::Improbable.Worker.CInterop.SchemaCommandResponse.Create(); response.ApplyToSchemaObject(schemaResponse.GetObject()); connection.SendCommandResponse(id, {type.Fqn()}.ComponentId, {cmd.CommandIndex}, schemaResponse); }} "; } text.AppendLine($@"{responseSender} /// <summary> /// Sends a command to the worker that is authoritative over <paramref name=""entityId""/>. /// </summary> /// <param name=""connection""> </param> /// <param name=""entityId""> </param> /// <param name=""request""> The request payload.</param> /// <param name=""cancellation""> A token used to mark the task as cancelled. /// Cancelling will NOT cancel the sending of the command to the authoritative worker; it WILL be processed by the runtime and the target worker. /// It only marks the task as cancelled locally. Use this for flow control or cleanup of state. /// </param> /// <param name=""timeout""> The amount of time that must pass before a command response is considered ""timed out"".</param> /// <param name=""commandParameters""> Options used to configure how the command is sent. </param> /// <param name=""taskOptions""> Options that control how the task is scheduled and executed. </param> /// <returns> A Task containing response payload. </returns> public static global::System.Threading.Tasks.Task<{response}> Send{cmdName}Async({WorkerConnectionType} connection, {Types.EntityIdType} entityId, {request} request, {CancellationTokenType} cancellation = default, uint? timeout = null, global::Improbable.Worker.CInterop.CommandParameters? commandParameters = null, global::System.Threading.Tasks.TaskCreationOptions taskOptions = global::System.Threading.Tasks.TaskCreationOptions.RunContinuationsAsynchronously) {{ var schemaRequest = global::Improbable.Worker.CInterop.SchemaCommandRequest.Create(); request.ApplyToSchemaObject(schemaRequest.GetObject()); var completion = new global::System.Threading.Tasks.TaskCompletionSource<{response}>(taskOptions); if (cancellation.CanBeCanceled) {{ cancellation.Register(() => completion.TrySetCanceled(cancellation)); }} void Complete(global::Improbable.Stdlib.WorkerConnection.CommandResponses r) {{ var result = new {response}(r.UserCommand.Response.SchemaData.Value.GetObject()); completion.TrySetResult(result); }} void Fail(global::Improbable.Worker.CInterop.StatusCode code, string message) {{ completion.TrySetException(new global::Improbable.Stdlib.CommandFailedException(code, message)); }} connection.Send(entityId, {type.Fqn()}.ComponentId, {cmd.CommandIndex}, schemaRequest, timeout, commandParameters, Complete, Fail); return completion.Task; }}"); } if (commandIndices.Length > 0) { text.Append($@" public enum Commands {{ {Indent(1, commandIndices.ToString().TrimEnd())} }} public static Commands GetCommandType(global::Improbable.Worker.CInterop.CommandRequestOp request) {{ if (request.Request.ComponentId != ComponentId) {{ throw new global::System.InvalidOperationException($""Mismatch of ComponentId (expected {{ComponentId}} but got {{request.Request.ComponentId}}""); }} return (Commands) request.Request.CommandIndex; }} public readonly struct CommandSenderBinding {{ private readonly {WorkerConnectionType} connection; private readonly {Types.EntityIdType} entityId; public CommandSenderBinding({WorkerConnectionType} connection, {Types.EntityIdType} entityId) {{ this.connection = connection; this.entityId = entityId; }} {Indent(1, bindingMethods.ToString().TrimEnd())} }} public static CommandSenderBinding Bind({WorkerConnectionType} connection, {Types.EntityIdType} entityId) {{ return new CommandSenderBinding(connection, entityId); }} "); } return(text.ToString()); }
public static string GenerateSendUpdate(TypeDescription type) { return($@"public static void SendUpdate({WorkerConnectionType} connection, {Types.EntityIdType} entityId, {$"{type.Fqn()}.Update"} update, global::Improbable.Worker.CInterop.UpdateParameters? updateParams = null) {{ connection.SendComponentUpdate(entityId.Value, ComponentId, update.ToSchemaUpdate(), updateParams); }} "); }